Browse Source

[web] Preprations for using DomApplier with Element

pull/708/head
Shagen Ogandzhanian 3 years ago
parent
commit
526053086b
  1. 77
      web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt
  2. 18
      web/core/src/jsMain/kotlin/androidx/compose/web/RenderComposable.kt
  3. 66
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt
  4. 9
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt
  5. 27
      web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt
  6. 35
      web/core/src/jsMain/kotlin/androidx/compose/web/elements/ElementScope.kt

77
web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt

@ -8,6 +8,7 @@ import androidx.compose.web.elements.setProperty
import androidx.compose.web.elements.setVariable
import kotlinx.browser.document
import kotlinx.dom.clear
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node
import org.w3c.dom.get
@ -38,20 +39,10 @@ class DomApplier(
}
}
class DomNodeWrapper(val node: Node) {
open class DomNodeWrapper(open val node: Node) {
constructor(tag: String) : this(document.createElement(tag))
private var currentListeners: List<WrappedEventListener<*>> = emptyList()
private var currentAttrs: Map<String, String?> = emptyMap()
fun updateProperties(list: List<Pair<(HTMLElement, Any) -> Unit, Any>>) {
val htmlElement = node as? HTMLElement ?: return
if (node.className.isNotEmpty()) node.className = ""
list.forEach { it.first(htmlElement, it.second) }
}
private var currentListeners = emptyList<WrappedEventListener<*>>()
fun updateEventListeners(list: List<WrappedEventListener<*>>) {
val htmlElement = node as? HTMLElement ?: return
@ -67,29 +58,6 @@ class DomNodeWrapper(val node: Node) {
}
}
fun updateAttrs(attrs: Map<String, String?>) {
val htmlElement = node as? HTMLElement ?: return
currentAttrs.forEach {
htmlElement.removeAttribute(it.key)
}
currentAttrs = attrs
currentAttrs.forEach {
if (it.value != null) htmlElement.setAttribute(it.key, it.value ?: "")
}
}
fun updateStyleDeclarations(style: StyleHolder?) {
val htmlElement = node as? HTMLElement ?: return
htmlElement.removeAttribute("style")
style?.properties?.forEach { (name, value) ->
setProperty(htmlElement.attributeStyleMap, name, value)
}
style?.variables?.forEach { (name, value) ->
setVariable(htmlElement.style, name, value)
}
}
fun insert(index: Int, nodeWrapper: DomNodeWrapper) {
val length = node.childNodes.length
if (index < length) {
@ -119,23 +87,36 @@ class DomNodeWrapper(val node: Node) {
node.insertBefore(child, node.childNodes[toIndex]!!)
}
}
}
companion object {
class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) {
private var currentAttrs = emptyMap<String, String?>()
val UpdateAttrs: DomNodeWrapper.(Map<String, String?>) -> Unit = {
this.updateAttrs(it)
fun updateAttrs(attrs: Map<String, String?>) {
currentAttrs.forEach {
node.removeAttribute(it.key)
}
val UpdateListeners: DomNodeWrapper.(List<WrappedEventListener<*>>) -> Unit = {
this.updateEventListeners(it)
currentAttrs = attrs
currentAttrs.forEach {
if (it.value != null) node.setAttribute(it.key, it.value ?: "")
}
val UpdateProperties: DomNodePropertiesUpdater = {
this.updateProperties(it)
}
fun updateProperties(list: List<Pair<(Element, Any) -> Unit, Any>>) {
if (node.className.isNotEmpty()) node.className = ""
list.forEach { it.first(node, it.second) }
}
fun updateStyleDeclarations(style: StyleHolder?) {
node.removeAttribute("style")
style?.properties?.forEach { (name, value) ->
setProperty(node.attributeStyleMap, name, value)
}
val UpdateStyleDeclarations: DomNodeWrapper.(StyleHolder?) -> Unit = {
this.updateStyleDeclarations(it)
style?.variables?.forEach { (name, value) ->
setVariable(node.style, name, value)
}
}
}
typealias DomNodePropertiesUpdater =
DomNodeWrapper.(List<Pair<(HTMLElement, Any) -> Unit, Any>>) -> Unit
}

18
web/core/src/jsMain/kotlin/androidx/compose/web/RenderComposable.kt

@ -10,20 +10,20 @@ import kotlinx.browser.document
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import org.w3c.dom.Element
import org.w3c.dom.HTMLBodyElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.get
/**
* Use this method to mount the composition at the certain [root]
*
* @param root - the [HTMLElement] that will be the root of the DOM tree managed by Compose
* @param root - the [Element] that will be the root of the DOM tree managed by Compose
* @param content - the Composable lambda that defines the composition content
*
* @return the instance of the [Composition]
*/
fun <THTMLElement : HTMLElement> renderComposable(
root: THTMLElement,
content: @Composable DOMScope<THTMLElement>.() -> Unit
fun <TElement : Element> renderComposable(
root: TElement,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
@ -33,7 +33,7 @@ fun <THTMLElement : HTMLElement> renderComposable(
applier = DomApplier(DomNodeWrapper(root)),
parent = recomposer
)
val scope = object : DOMScope<THTMLElement> {}
val scope = object : DOMScope<TElement> {}
composition.setContent @Composable {
content(scope)
}
@ -47,7 +47,7 @@ fun <THTMLElement : HTMLElement> renderComposable(
/**
* Use this method to mount the composition at the element with id - [rootElementId].
*
* @param rootElementId - the id of the [HTMLElement] that will be the root of the DOM tree managed
* @param rootElementId - the id of the [Element] that will be the root of the DOM tree managed
* by Compose
* @param content - the Composable lambda that defines the composition content
*
@ -56,9 +56,9 @@ fun <THTMLElement : HTMLElement> renderComposable(
@Suppress("UNCHECKED_CAST")
fun renderComposable(
rootElementId: String,
content: @Composable DOMScope<HTMLElement>.() -> Unit
content: @Composable DOMScope<Element>.() -> Unit
): Composition = renderComposable(
root = document.getElementById(rootElementId) as HTMLElement,
root = document.getElementById(rootElementId)!!,
content = content
)

66
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt

@ -2,39 +2,39 @@ package androidx.compose.web.attributes
import org.w3c.dom.HTMLInputElement
open class Tag {
object Div : Tag()
object A : Tag()
object Button : Tag()
object Form : Tag()
object Input : Tag()
object Select : Tag()
object Option : Tag()
object OptGroup : Tag()
object H : Tag()
object Ul : Tag()
object Ol : Tag()
object Li : Tag()
object Img : Tag()
object TextArea : Tag()
object Nav : Tag()
object Span : Tag()
object P : Tag()
object Br : Tag()
object Style : Tag()
object Pre : Tag()
object Code : Tag()
object Label : Tag()
object Table : Tag()
object Caption : Tag()
object Col : Tag()
object Colgroup : Tag()
object Tr : Tag()
object Thead : Tag()
object Th : Tag()
object Td : Tag()
object Tbody : Tag()
object Tfoot : Tag()
interface Tag {
object Div : Tag
object A : Tag
object Button : Tag
object Form : Tag
object Input : Tag
object Select : Tag
object Option : Tag
object OptGroup : Tag
object H : Tag
object Ul : Tag
object Ol : Tag
object Li : Tag
object Img : Tag
object TextArea : Tag
object Nav : Tag
object Span : Tag
object P : Tag
object Br : Tag
object Style : Tag
object Pre : Tag
object Code : Tag
object Label : Tag
object Table : Tag
object Caption : Tag
object Col : Tag
object Colgroup : Tag
object Tr : Tag
object Thead : Tag
object Th : Tag
object Td : Tag
object Tbody : Tag
object Tfoot : Tag
}
/* Anchor <a> attributes */

9
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt

@ -4,14 +4,15 @@ import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.web.css.StyleBuilder
import androidx.compose.web.css.StyleBuilderImpl
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
class AttrsBuilder<TTag : Tag> : EventsListenerBuilder() {
private val attributesMap = mutableMapOf<String, String>()
val styleBuilder = StyleBuilderImpl()
val propertyUpdates = mutableListOf<Pair<(HTMLElement, Any) -> Unit, Any>>()
var refEffect: (DisposableEffectScope.(HTMLElement) -> DisposableEffectResult)? = null
val propertyUpdates = mutableListOf<Pair<(Element, Any) -> Unit, Any>>()
var refEffect: (DisposableEffectScope.(Element) -> DisposableEffectResult)? = null
fun style(builder: StyleBuilder.() -> Unit) {
styleBuilder.apply(builder)
@ -32,7 +33,7 @@ class AttrsBuilder<TTag : Tag> : EventsListenerBuilder() {
fun tabIndex(value: Int) = attr(TAB_INDEX, value.toString())
fun spellCheck(value: Boolean) = attr(SPELLCHECK, value.toString())
fun ref(effect: DisposableEffectScope.(HTMLElement) -> DisposableEffectResult) {
fun ref(effect: DisposableEffectScope.(Element) -> DisposableEffectResult) {
this.refEffect = effect
}
@ -48,7 +49,7 @@ class AttrsBuilder<TTag : Tag> : EventsListenerBuilder() {
@Suppress("UNCHECKED_CAST")
fun <E : HTMLElement, V> prop(update: (E, V) -> Unit, value: V) {
propertyUpdates.add((update to value) as Pair<(HTMLElement, Any) -> Unit, Any>)
propertyUpdates.add((update to value) as Pair<(Element, Any) -> Unit, Any>)
}
fun collect(): Map<String, String> {

27
web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt

@ -11,12 +11,11 @@ import androidx.compose.runtime.SkippableUpdater
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember
import androidx.compose.web.DomApplier
import androidx.compose.web.DomNodeWrapper
import androidx.compose.web.DomElementWrapper
import androidx.compose.web.attributes.AttrsBuilder
import androidx.compose.web.attributes.Tag
import androidx.compose.web.css.StyleBuilder
import androidx.compose.web.css.StyleBuilderImpl
import kotlinx.browser.document
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
@OptIn(ComposeCompilerApi::class)
@ -47,22 +46,22 @@ inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode(
}
class DisposableEffectHolder(
var effect: (DisposableEffectScope.(HTMLElement) -> DisposableEffectResult)? = null
var effect: (DisposableEffectScope.(Element) -> DisposableEffectResult)? = null
)
@Composable
inline fun <TTag : Tag, THTMLElement : HTMLElement> TagElement(
inline fun <TTag : Tag, TElement : Element> TagElement(
tagName: String,
crossinline applyAttrs: AttrsBuilder<TTag>.() -> Unit,
content: @Composable ElementScope<THTMLElement>.() -> Unit
content: @Composable ElementScope<TElement>.() -> Unit
) {
val scope = remember { ElementScopeImpl<THTMLElement>() }
val scope = remember { ElementScopeImpl<TElement>() }
val refEffect = remember { DisposableEffectHolder() }
ComposeDomNode<ElementScope<THTMLElement>, DomNodeWrapper, DomApplier>(
ComposeDomNode<ElementScope<TElement>, DomElementWrapper, DomApplier>(
factory = {
DomNodeWrapper(document.createElement(tagName)).also {
scope.element = it.node.unsafeCast<THTMLElement>()
DomElementWrapper(document.createElement(tagName) as HTMLElement).also {
scope.element = it.node.unsafeCast<TElement>()
}
},
attrsSkippableUpdate = {
@ -72,10 +71,10 @@ inline fun <TTag : Tag, THTMLElement : HTMLElement> TagElement(
val events = attrsApplied.collectListeners()
update {
set(attrsCollected, DomNodeWrapper.UpdateAttrs)
set(events, DomNodeWrapper.UpdateListeners)
set(attrsApplied.propertyUpdates, DomNodeWrapper.UpdateProperties)
set(attrsApplied.styleBuilder, DomNodeWrapper.UpdateStyleDeclarations)
set(attrsCollected, DomElementWrapper::updateAttrs)
set(events, DomElementWrapper::updateEventListeners)
set(attrsApplied.propertyUpdates, DomElementWrapper::updateProperties)
set(attrsApplied.styleBuilder, DomElementWrapper::updateStyleDeclarations)
}
},
elementScope = scope,

35
web/core/src/jsMain/kotlin/androidx/compose/web/elements/ElementScope.kt

@ -10,38 +10,39 @@ import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
interface DOMScope<out THTMLElement : HTMLElement>
interface DOMScope<out TElement : Element>
interface ElementScope<out THTMLElement : HTMLElement> : DOMScope<THTMLElement> {
interface ElementScope<out TElement : Element> : DOMScope<TElement> {
@Composable
@NonRestartableComposable
fun DisposableRefEffect(
key: Any?,
effect: DisposableEffectScope.(THTMLElement) -> DisposableEffectResult
effect: DisposableEffectScope.(TElement) -> DisposableEffectResult
)
@Composable
@NonRestartableComposable
fun DisposableRefEffect(
effect: DisposableEffectScope.(THTMLElement) -> DisposableEffectResult
effect: DisposableEffectScope.(TElement) -> DisposableEffectResult
) {
DisposableRefEffect(null, effect)
}
@Composable
@NonRestartableComposable
fun DomSideEffect(key: Any?, effect: DomEffectScope.(THTMLElement) -> Unit)
fun DomSideEffect(key: Any?, effect: DomEffectScope.(TElement) -> Unit)
@Composable
@NonRestartableComposable
fun DomSideEffect(effect: DomEffectScope.(THTMLElement) -> Unit)
fun DomSideEffect(effect: DomEffectScope.(TElement) -> Unit)
}
abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<THTMLElement> {
abstract val element: THTMLElement
abstract class ElementScopeBase<out TElement : Element> : ElementScope<TElement> {
abstract val element: TElement
private var nextDisposableDomEffectKey = 0
@ -49,7 +50,7 @@ abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<T
@NonRestartableComposable
override fun DisposableRefEffect(
key: Any?,
effect: DisposableEffectScope.(THTMLElement) -> DisposableEffectResult
effect: DisposableEffectScope.(TElement) -> DisposableEffectResult
) {
DisposableEffect(key) { effect(element) }
}
@ -59,7 +60,7 @@ abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<T
@OptIn(ComposeCompilerApi::class)
override fun DomSideEffect(
key: Any?,
effect: DomEffectScope.(THTMLElement) -> Unit
effect: DomEffectScope.(TElement) -> Unit
) {
val changed = currentComposer.changed(key)
val effectHolder = remember(key) {
@ -72,23 +73,23 @@ abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<T
@Composable
@NonRestartableComposable
override fun DomSideEffect(effect: DomEffectScope.(THTMLElement) -> Unit) {
override fun DomSideEffect(effect: DomEffectScope.(TElement) -> Unit) {
DomSideEffect(nextDisposableDomEffectKey++, effect)
}
}
open class ElementScopeImpl<THTMLElement : HTMLElement> : ElementScopeBase<THTMLElement>() {
public override lateinit var element: THTMLElement
open class ElementScopeImpl<TElement : Element> : ElementScopeBase<TElement>() {
public override lateinit var element: TElement
}
interface DomEffectScope {
fun onDispose(disposeEffect: (HTMLElement) -> Unit)
fun onDispose(disposeEffect: (Element) -> Unit)
}
private class DomDisposableEffectHolder(
val elementScope: ElementScopeBase<HTMLElement>
val elementScope: ElementScopeBase<Element>
) : RememberObserver, DomEffectScope {
var onDispose: ((HTMLElement) -> Unit)? = null
var onDispose: ((Element) -> Unit)? = null
override fun onRemembered() {}
@ -98,7 +99,7 @@ private class DomDisposableEffectHolder(
override fun onAbandoned() {}
override fun onDispose(disposeEffect: (HTMLElement) -> Unit) {
override fun onDispose(disposeEffect: (Element) -> Unit) {
onDispose = disposeEffect
}
}
Loading…
Cancel
Save