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 androidx.compose.web.elements.setVariable
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.dom.clear import kotlinx.dom.clear
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.get 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)) constructor(tag: String) : this(document.createElement(tag))
private var currentListeners: List<WrappedEventListener<*>> = emptyList() private var currentListeners = emptyList<WrappedEventListener<*>>()
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) }
}
fun updateEventListeners(list: List<WrappedEventListener<*>>) { fun updateEventListeners(list: List<WrappedEventListener<*>>) {
val htmlElement = node as? HTMLElement ?: return 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) { fun insert(index: Int, nodeWrapper: DomNodeWrapper) {
val length = node.childNodes.length val length = node.childNodes.length
if (index < length) { if (index < length) {
@ -119,23 +87,36 @@ class DomNodeWrapper(val node: Node) {
node.insertBefore(child, node.childNodes[toIndex]!!) 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 = { fun updateAttrs(attrs: Map<String, String?>) {
this.updateAttrs(it) currentAttrs.forEach {
node.removeAttribute(it.key)
} }
val UpdateListeners: DomNodeWrapper.(List<WrappedEventListener<*>>) -> Unit = { currentAttrs = attrs
this.updateEventListeners(it) 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 = { style?.variables?.forEach { (name, value) ->
this.updateStyleDeclarations(it) 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.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.w3c.dom.Element
import org.w3c.dom.HTMLBodyElement import org.w3c.dom.HTMLBodyElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get
/** /**
* Use this method to mount the composition at the certain [root] * 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 * @param content - the Composable lambda that defines the composition content
* *
* @return the instance of the [Composition] * @return the instance of the [Composition]
*/ */
fun <THTMLElement : HTMLElement> renderComposable( fun <TElement : Element> renderComposable(
root: THTMLElement, root: TElement,
content: @Composable DOMScope<THTMLElement>.() -> Unit content: @Composable DOMScope<TElement>.() -> Unit
): Composition { ): Composition {
GlobalSnapshotManager.ensureStarted() GlobalSnapshotManager.ensureStarted()
@ -33,7 +33,7 @@ fun <THTMLElement : HTMLElement> renderComposable(
applier = DomApplier(DomNodeWrapper(root)), applier = DomApplier(DomNodeWrapper(root)),
parent = recomposer parent = recomposer
) )
val scope = object : DOMScope<THTMLElement> {} val scope = object : DOMScope<TElement> {}
composition.setContent @Composable { composition.setContent @Composable {
content(scope) content(scope)
} }
@ -47,7 +47,7 @@ fun <THTMLElement : HTMLElement> renderComposable(
/** /**
* Use this method to mount the composition at the element with id - [rootElementId]. * 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 * by Compose
* @param content - the Composable lambda that defines the composition content * @param content - the Composable lambda that defines the composition content
* *
@ -56,9 +56,9 @@ fun <THTMLElement : HTMLElement> renderComposable(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun renderComposable( fun renderComposable(
rootElementId: String, rootElementId: String,
content: @Composable DOMScope<HTMLElement>.() -> Unit content: @Composable DOMScope<Element>.() -> Unit
): Composition = renderComposable( ): Composition = renderComposable(
root = document.getElementById(rootElementId) as HTMLElement, root = document.getElementById(rootElementId)!!,
content = content 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 import org.w3c.dom.HTMLInputElement
open class Tag { interface Tag {
object Div : Tag() object Div : Tag
object A : Tag() object A : Tag
object Button : Tag() object Button : Tag
object Form : Tag() object Form : Tag
object Input : Tag() object Input : Tag
object Select : Tag() object Select : Tag
object Option : Tag() object Option : Tag
object OptGroup : Tag() object OptGroup : Tag
object H : Tag() object H : Tag
object Ul : Tag() object Ul : Tag
object Ol : Tag() object Ol : Tag
object Li : Tag() object Li : Tag
object Img : Tag() object Img : Tag
object TextArea : Tag() object TextArea : Tag
object Nav : Tag() object Nav : Tag
object Span : Tag() object Span : Tag
object P : Tag() object P : Tag
object Br : Tag() object Br : Tag
object Style : Tag() object Style : Tag
object Pre : Tag() object Pre : Tag
object Code : Tag() object Code : Tag
object Label : Tag() object Label : Tag
object Table : Tag() object Table : Tag
object Caption : Tag() object Caption : Tag
object Col : Tag() object Col : Tag
object Colgroup : Tag() object Colgroup : Tag
object Tr : Tag() object Tr : Tag
object Thead : Tag() object Thead : Tag
object Th : Tag() object Th : Tag
object Td : Tag() object Td : Tag
object Tbody : Tag() object Tbody : Tag
object Tfoot : Tag() object Tfoot : Tag
} }
/* Anchor <a> attributes */ /* 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.runtime.DisposableEffectScope
import androidx.compose.web.css.StyleBuilder import androidx.compose.web.css.StyleBuilder
import androidx.compose.web.css.StyleBuilderImpl import androidx.compose.web.css.StyleBuilderImpl
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
class AttrsBuilder<TTag : Tag> : EventsListenerBuilder() { class AttrsBuilder<TTag : Tag> : EventsListenerBuilder() {
private val attributesMap = mutableMapOf<String, String>() private val attributesMap = mutableMapOf<String, String>()
val styleBuilder = StyleBuilderImpl() val styleBuilder = StyleBuilderImpl()
val propertyUpdates = mutableListOf<Pair<(HTMLElement, Any) -> Unit, Any>>() val propertyUpdates = mutableListOf<Pair<(Element, Any) -> Unit, Any>>()
var refEffect: (DisposableEffectScope.(HTMLElement) -> DisposableEffectResult)? = null var refEffect: (DisposableEffectScope.(Element) -> DisposableEffectResult)? = null
fun style(builder: StyleBuilder.() -> Unit) { fun style(builder: StyleBuilder.() -> Unit) {
styleBuilder.apply(builder) styleBuilder.apply(builder)
@ -32,7 +33,7 @@ class AttrsBuilder<TTag : Tag> : EventsListenerBuilder() {
fun tabIndex(value: Int) = attr(TAB_INDEX, value.toString()) fun tabIndex(value: Int) = attr(TAB_INDEX, value.toString())
fun spellCheck(value: Boolean) = attr(SPELLCHECK, 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 this.refEffect = effect
} }
@ -48,7 +49,7 @@ class AttrsBuilder<TTag : Tag> : EventsListenerBuilder() {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <E : HTMLElement, V> prop(update: (E, V) -> Unit, value: V) { 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> { 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.currentComposer
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.web.DomApplier 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.AttrsBuilder
import androidx.compose.web.attributes.Tag import androidx.compose.web.attributes.Tag
import androidx.compose.web.css.StyleBuilder
import androidx.compose.web.css.StyleBuilderImpl
import kotlinx.browser.document import kotlinx.browser.document
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
@OptIn(ComposeCompilerApi::class) @OptIn(ComposeCompilerApi::class)
@ -47,22 +46,22 @@ inline fun <TScope, T, reified E : Applier<*>> ComposeDomNode(
} }
class DisposableEffectHolder( class DisposableEffectHolder(
var effect: (DisposableEffectScope.(HTMLElement) -> DisposableEffectResult)? = null var effect: (DisposableEffectScope.(Element) -> DisposableEffectResult)? = null
) )
@Composable @Composable
inline fun <TTag : Tag, THTMLElement : HTMLElement> TagElement( inline fun <TTag : Tag, TElement : Element> TagElement(
tagName: String, tagName: String,
crossinline applyAttrs: AttrsBuilder<TTag>.() -> Unit, 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() } val refEffect = remember { DisposableEffectHolder() }
ComposeDomNode<ElementScope<THTMLElement>, DomNodeWrapper, DomApplier>( ComposeDomNode<ElementScope<TElement>, DomElementWrapper, DomApplier>(
factory = { factory = {
DomNodeWrapper(document.createElement(tagName)).also { DomElementWrapper(document.createElement(tagName) as HTMLElement).also {
scope.element = it.node.unsafeCast<THTMLElement>() scope.element = it.node.unsafeCast<TElement>()
} }
}, },
attrsSkippableUpdate = { attrsSkippableUpdate = {
@ -72,10 +71,10 @@ inline fun <TTag : Tag, THTMLElement : HTMLElement> TagElement(
val events = attrsApplied.collectListeners() val events = attrsApplied.collectListeners()
update { update {
set(attrsCollected, DomNodeWrapper.UpdateAttrs) set(attrsCollected, DomElementWrapper::updateAttrs)
set(events, DomNodeWrapper.UpdateListeners) set(events, DomElementWrapper::updateEventListeners)
set(attrsApplied.propertyUpdates, DomNodeWrapper.UpdateProperties) set(attrsApplied.propertyUpdates, DomElementWrapper::updateProperties)
set(attrsApplied.styleBuilder, DomNodeWrapper.UpdateStyleDeclarations) set(attrsApplied.styleBuilder, DomElementWrapper::updateStyleDeclarations)
} }
}, },
elementScope = scope, 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.SideEffect
import androidx.compose.runtime.currentComposer import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement 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 @Composable
@NonRestartableComposable @NonRestartableComposable
fun DisposableRefEffect( fun DisposableRefEffect(
key: Any?, key: Any?,
effect: DisposableEffectScope.(THTMLElement) -> DisposableEffectResult effect: DisposableEffectScope.(TElement) -> DisposableEffectResult
) )
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun DisposableRefEffect( fun DisposableRefEffect(
effect: DisposableEffectScope.(THTMLElement) -> DisposableEffectResult effect: DisposableEffectScope.(TElement) -> DisposableEffectResult
) { ) {
DisposableRefEffect(null, effect) DisposableRefEffect(null, effect)
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun DomSideEffect(key: Any?, effect: DomEffectScope.(THTMLElement) -> Unit) fun DomSideEffect(key: Any?, effect: DomEffectScope.(TElement) -> Unit)
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun DomSideEffect(effect: DomEffectScope.(THTMLElement) -> Unit) fun DomSideEffect(effect: DomEffectScope.(TElement) -> Unit)
} }
abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<THTMLElement> { abstract class ElementScopeBase<out TElement : Element> : ElementScope<TElement> {
abstract val element: THTMLElement abstract val element: TElement
private var nextDisposableDomEffectKey = 0 private var nextDisposableDomEffectKey = 0
@ -49,7 +50,7 @@ abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<T
@NonRestartableComposable @NonRestartableComposable
override fun DisposableRefEffect( override fun DisposableRefEffect(
key: Any?, key: Any?,
effect: DisposableEffectScope.(THTMLElement) -> DisposableEffectResult effect: DisposableEffectScope.(TElement) -> DisposableEffectResult
) { ) {
DisposableEffect(key) { effect(element) } DisposableEffect(key) { effect(element) }
} }
@ -59,7 +60,7 @@ abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<T
@OptIn(ComposeCompilerApi::class) @OptIn(ComposeCompilerApi::class)
override fun DomSideEffect( override fun DomSideEffect(
key: Any?, key: Any?,
effect: DomEffectScope.(THTMLElement) -> Unit effect: DomEffectScope.(TElement) -> Unit
) { ) {
val changed = currentComposer.changed(key) val changed = currentComposer.changed(key)
val effectHolder = remember(key) { val effectHolder = remember(key) {
@ -72,23 +73,23 @@ abstract class ElementScopeBase<out THTMLElement : HTMLElement> : ElementScope<T
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
override fun DomSideEffect(effect: DomEffectScope.(THTMLElement) -> Unit) { override fun DomSideEffect(effect: DomEffectScope.(TElement) -> Unit) {
DomSideEffect(nextDisposableDomEffectKey++, effect) DomSideEffect(nextDisposableDomEffectKey++, effect)
} }
} }
open class ElementScopeImpl<THTMLElement : HTMLElement> : ElementScopeBase<THTMLElement>() { open class ElementScopeImpl<TElement : Element> : ElementScopeBase<TElement>() {
public override lateinit var element: THTMLElement public override lateinit var element: TElement
} }
interface DomEffectScope { interface DomEffectScope {
fun onDispose(disposeEffect: (HTMLElement) -> Unit) fun onDispose(disposeEffect: (Element) -> Unit)
} }
private class DomDisposableEffectHolder( private class DomDisposableEffectHolder(
val elementScope: ElementScopeBase<HTMLElement> val elementScope: ElementScopeBase<Element>
) : RememberObserver, DomEffectScope { ) : RememberObserver, DomEffectScope {
var onDispose: ((HTMLElement) -> Unit)? = null var onDispose: ((Element) -> Unit)? = null
override fun onRemembered() {} override fun onRemembered() {}
@ -98,7 +99,7 @@ private class DomDisposableEffectHolder(
override fun onAbandoned() {} override fun onAbandoned() {}
override fun onDispose(disposeEffect: (HTMLElement) -> Unit) { override fun onDispose(disposeEffect: (Element) -> Unit) {
onDispose = disposeEffect onDispose = disposeEffect
} }
} }
Loading…
Cancel
Save