Browse Source
* web: add a tutorial for DomSideEffect * web: add a tutorial for DomSideEffect (edits) Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>pull/987/head
Oleksandr Karpovich
3 years ago
committed by
GitHub
2 changed files with 226 additions and 5 deletions
@ -0,0 +1,187 @@
|
||||
# Using Effects in Compose Web |
||||
**The API is not finalized, and breaking changes can be expected** |
||||
|
||||
## Introduction |
||||
Compose for Web introduces a few dom-specific effects on top of [existing effects from Compose](https://developer.android.com/jetpack/compose/side-effects). |
||||
|
||||
|
||||
### ref in AttrsBuilder |
||||
|
||||
Under the hood, `ref` uses [DisposableEffect](https://developer.android.com/jetpack/compose/side-effects#disposableeffect) |
||||
|
||||
`ref` can be used to retrieve a reference to a html element. |
||||
The lambda that `ref` takes in is not Composable. It will be called only once when an element added into a composition. |
||||
Likewise, the lambda passed in `onDispose` will be called only once when an element leaves the composition. |
||||
|
||||
``` kotlin |
||||
Div(attrs = { |
||||
ref { htmlDivElement -> |
||||
// htmlDivElement is a reference to the HTMLDivElement |
||||
onDispose { |
||||
// add clean up code here |
||||
} |
||||
} |
||||
}) { |
||||
// Content() |
||||
} |
||||
``` |
||||
|
||||
Only one `ref` can be used per element. Calling it more than once will dismiss earlier calls. |
||||
|
||||
For example, `ref` can be used to add and remove some event listeners not provided by compose-web from the box. |
||||
|
||||
### DisposableRefEffect |
||||
|
||||
Under the hood, `DisposableRefEffect` uses [DisposableEffect](https://developer.android.com/jetpack/compose/side-effects#disposableeffect) |
||||
|
||||
`DisposableRefEffect` is similar to `ref`, since it also provides a reference to an element. At the same time it has few differences. |
||||
|
||||
- `DisposableRefEffect` can be added only within a content lambda of an element, while `ref` can be used only in `attrs` scope. |
||||
- Unlike `ref`, `DisposableRefEffect` can be used as many times as needed and every effect will be unique. |
||||
- DisposableRefEffect can be used with a `key` and without it. When it's used with a `key: Any`, the effect will be disposed and reset when `key` value changes. When it's used without a key, then it behaves like `ref` - the effect gets called only once when an element enters the composition, and it's disposed only when the element leaves the composition. |
||||
|
||||
|
||||
``` kotlin |
||||
Div { |
||||
// without a key |
||||
DisposableRefEffect { htmlDivElement -> |
||||
// htmlDivElement is a reference to the HTMLDivElement |
||||
onDispose { |
||||
// add clean up code here |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
var state by remember { mutableStateOf(1) } |
||||
|
||||
Div { |
||||
// with a key. |
||||
// The effect will be called for every new state's value |
||||
DisposableRefEffect(state) { htmlDivElement -> |
||||
// htmlDivElement is a reference to the HTMLDivElement |
||||
onDispose { |
||||
// add clean up code here |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### DomSideEffect |
||||
|
||||
Under the hood, `DomSideEffect` uses [SideEffect](https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish) |
||||
|
||||
`DomSideEffect` as well as `DisposableRefEffect` can be used with a key and without it. |
||||
|
||||
Unlike `DisposableRefEffect`, `DomSideEffect` without a key is invoked on every successful recomposition. |
||||
With a `key`, it will be invoked only when the `key` value changes. |
||||
|
||||
Same as [SideEffect](https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish), `DomSideEffect` can be helpful when there is a need to update objects not managed by Compose. |
||||
In case of web, it often involves updating HTML nodes, therefore `DomSideEffect` provides a reference to an element in the lambda. |
||||
|
||||
### Code Sample using effects |
||||
|
||||
The code below showcases how it's possible to use non-composable components in Compose by applying `DomSideEffect` and `DisposableRefEffect`. |
||||
|
||||
```kotlin |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.setValue |
||||
import androidx.compose.runtime.Composable |
||||
import kotlinx.browser.document |
||||
import org.jetbrains.compose.web.css.* |
||||
import org.jetbrains.compose.web.dom.* |
||||
import org.jetbrains.compose.web.renderComposable |
||||
import org.w3c.dom.HTMLElement |
||||
import org.w3c.dom.HTMLParagraphElement |
||||
|
||||
|
||||
// Here we pretend that `RedBoldTextNotComposableRenderer` |
||||
// wraps a UI logic provided by 3rd party library that doesn't use Compose |
||||
|
||||
object RedBoldTextNotComposableRenderer { |
||||
fun unmountFrom(root: HTMLElement) { |
||||
root.removeChild(root.firstChild!!) |
||||
} |
||||
|
||||
fun mountIn(root: HTMLElement) { |
||||
val pElement = document.createElement("p") as HTMLParagraphElement |
||||
pElement.setAttribute("style", "color: red; font-weight: bold;") |
||||
root.appendChild(pElement) |
||||
} |
||||
|
||||
fun renderIn(root: HTMLElement, text: String) { |
||||
(root.firstChild as HTMLParagraphElement).innerText = text |
||||
} |
||||
} |
||||
|
||||
// Here we define a Composable wrapper for the above code. Here we use DomSideEffect and DisposableRefEffect. |
||||
@Composable // @param `show: Boolean` was left here intentionally for the sake of the example |
||||
fun ComposableWrapperForRedBoldTextFrom3rdPartyLib(state: Int, show: Boolean) { |
||||
Div(attrs = { |
||||
style { |
||||
backgroundColor(Color.lightgray) |
||||
width(100.px) |
||||
minHeight(40.px) |
||||
padding(30.px) |
||||
} |
||||
}) { |
||||
if (!show) { |
||||
Text("No content rendered by the 3rd party library") |
||||
} |
||||
|
||||
Div { |
||||
if (show) { |
||||
// Update the content rendered by "non-compose library" according to the `state` |
||||
DomSideEffect(state) { div -> |
||||
RedBoldTextNotComposableRenderer.renderIn(div, "Value = $state") |
||||
} |
||||
} |
||||
|
||||
DisposableRefEffect(show) { div -> |
||||
if (show) { |
||||
// Let "non-compose library" control the part of the page. |
||||
// The content of this div is independent of Compose. |
||||
// It will be managed by RedBoldTextNotComposableRenderer |
||||
RedBoldTextNotComposableRenderer.mountIn(div) |
||||
} |
||||
onDispose { |
||||
if (show) { |
||||
// Clean up the html created/managed by "non-compose library" |
||||
RedBoldTextNotComposableRenderer.unmountFrom(div) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun main() { |
||||
var state by mutableStateOf(0) |
||||
var showUncontrolledElements by mutableStateOf(false) |
||||
|
||||
renderComposable(rootElementId = "root") { |
||||
|
||||
ComposableWrapperForRedBoldTextFrom3rdPartyLib(state = state, show = showUncontrolledElements) |
||||
|
||||
Div { |
||||
Label(forId = "checkbox") { |
||||
Text("Show/hide text rendered by 3rd party library") |
||||
} |
||||
|
||||
CheckboxInput(checked = false) { |
||||
id("checkbox") |
||||
onInput { |
||||
showUncontrolledElements = it.value |
||||
} |
||||
} |
||||
} |
||||
|
||||
Button(attrs = { |
||||
onClick { state += 1 } |
||||
}) { |
||||
Text("Incr. count ($state)") |
||||
} |
||||
} |
||||
} |
||||
``` |
Loading…
Reference in new issue