From 5b496eb1913c9cd21136a65f9959dde7e2072038 Mon Sep 17 00:00:00 2001 From: Oleksandr Karpovich Date: Wed, 16 Feb 2022 11:37:53 +0100 Subject: [PATCH] web: update TagElement with tagName: String (#1827) * web: update TagElement with tagName: String Enable changing the tagName value. This will delete the initial html element and create a new one with a new tagName. Cache ElementBuilder instances. * add comments * update PR according to discussion Co-authored-by: Oleksandr Karpovich --- .../jetbrains/compose/web/elements/Base.kt | 24 ++++++--- .../compose/web/elements/Elements.kt | 10 +++- .../jsTest/kotlin/elements/ElementsTests.kt | 50 +++++++++++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt index 3b117ec0544..78862f84dfe 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Base.kt @@ -148,14 +148,26 @@ fun TagElement( } } +/** + * @param tagName - the name of the tag that needs to be created. + * It's best to use constant values for [tagName]. + * If variable [tagName] needed, consider wrapping TagElement calls into an if...else: + * + * ``` + * if (useDiv) TagElement("div",...) else TagElement("span", ...) + * ``` + */ @Composable -@ExperimentalComposeWebApi fun TagElement( tagName: String, applyAttrs: (AttrsScope.() -> Unit)?, content: (@Composable ElementScope.() -> Unit)? -) = TagElement( - elementBuilder = ElementBuilder.createBuilder(tagName), - applyAttrs = applyAttrs, - content = content -) +) { + key(tagName) { + TagElement( + elementBuilder = ElementBuilder.createBuilder(tagName), + applyAttrs = applyAttrs, + content = content + ) + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt index e42289d9a2c..4d5fed5061c 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt +++ b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt @@ -152,14 +152,20 @@ private val Td: ElementBuilder = ElementBuilderImplementat private val Tbody: ElementBuilder = ElementBuilderImplementation("tbody") private val Tfoot: ElementBuilder = ElementBuilderImplementation("tfoot") -val Style: ElementBuilder = ElementBuilderImplementation("style") +internal val Style: ElementBuilder = ElementBuilderImplementation("style") fun interface ElementBuilder { fun create(): TElement companion object { + // it's internal only for testing purposes + internal val buildersCache = mutableMapOf>() + fun createBuilder(tagName: String): ElementBuilder { - return object : ElementBuilderImplementation(tagName) {} + val tagLowercase = tagName.lowercase() + return buildersCache.getOrPut(tagLowercase) { + ElementBuilderImplementation(tagLowercase) + }.unsafeCast>() } } } diff --git a/web/core/src/jsTest/kotlin/elements/ElementsTests.kt b/web/core/src/jsTest/kotlin/elements/ElementsTests.kt index 23c2b70c110..d0b739dbce9 100644 --- a/web/core/src/jsTest/kotlin/elements/ElementsTests.kt +++ b/web/core/src/jsTest/kotlin/elements/ElementsTests.kt @@ -135,6 +135,56 @@ class ElementsTests { assertEquals("
CUSTOM
", root.outerHTML) } + @Test + fun testElementBuilderCreate() { + val custom = ElementBuilder.createBuilder("custom") + val div = ElementBuilder.createBuilder("div") + val b = ElementBuilder.createBuilder("b") + val abc = ElementBuilder.createBuilder("abc") + + val expectedKeys = setOf("custom", "div", "b", "abc") + assertEquals(expectedKeys, ElementBuilder.buildersCache.keys.intersect(expectedKeys)) + + assertEquals("CUSTOM", custom.create().nodeName) + assertEquals("DIV", div.create().nodeName) + assertEquals("B", b.create().nodeName) + assertEquals("ABC", abc.create().nodeName) + } + + @Test + @OptIn(ExperimentalComposeWebApi::class) + fun rawCreationAndTagChanges() = runTest { + @Composable + fun CustomElement( + tagName: String, + attrs: AttrsScope.() -> Unit, + content: ContentBuilder? = null + ) { + TagElement( + tagName = tagName, + applyAttrs = attrs, + content + ) + } + + var tagName by mutableStateOf("custom") + + composition { + CustomElement(tagName, { + id("container") + }) { + Text("CUSTOM") + } + } + + assertEquals("
CUSTOM
", root.outerHTML) + + tagName = "anothercustom" + waitForRecompositionComplete() + + assertEquals("
CUSTOM
", root.outerHTML) + } + @Test fun elementBuilderShouldBeCalledOnce() = runTest { var counter = 0