Skip to content

Commit

Permalink
web: update TagElement with tagName: String (#1827)
Browse files Browse the repository at this point in the history
* 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 <oleksandr.karpovich@jetbrains.com>
(cherry picked from commit 5b496eb)
  • Loading branch information
eymar authored and Oleksandr Karpovich committed Feb 21, 2022
1 parent dff17ef commit ee9bba2
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 8 deletions.
Expand Up @@ -148,14 +148,26 @@ fun <TElement : Element> 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 <TElement : Element> TagElement(
tagName: String,
applyAttrs: (AttrsScope<TElement>.() -> Unit)?,
content: (@Composable ElementScope<TElement>.() -> Unit)?
) = TagElement(
elementBuilder = ElementBuilder.createBuilder(tagName),
applyAttrs = applyAttrs,
content = content
)
) {
key(tagName) {
TagElement(
elementBuilder = ElementBuilder.createBuilder(tagName),
applyAttrs = applyAttrs,
content = content
)
}
}
Expand Up @@ -152,14 +152,20 @@ private val Td: ElementBuilder<HTMLTableCellElement> = ElementBuilderImplementat
private val Tbody: ElementBuilder<HTMLTableSectionElement> = ElementBuilderImplementation("tbody")
private val Tfoot: ElementBuilder<HTMLTableSectionElement> = ElementBuilderImplementation("tfoot")

val Style: ElementBuilder<HTMLStyleElement> = ElementBuilderImplementation("style")
internal val Style: ElementBuilder<HTMLStyleElement> = ElementBuilderImplementation("style")

fun interface ElementBuilder<TElement : Element> {
fun create(): TElement

companion object {
// it's internal only for testing purposes
internal val buildersCache = mutableMapOf<String, ElementBuilder<*>>()

fun <TElement : Element> createBuilder(tagName: String): ElementBuilder<TElement> {
return object : ElementBuilderImplementation<TElement>(tagName) {}
val tagLowercase = tagName.lowercase()
return buildersCache.getOrPut(tagLowercase) {
ElementBuilderImplementation<TElement>(tagLowercase)
}.unsafeCast<ElementBuilder<TElement>>()
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions web/core/src/jsTest/kotlin/elements/ElementsTests.kt
Expand Up @@ -135,6 +135,56 @@ class ElementsTests {
assertEquals("<div><custom id=\"container\">CUSTOM</custom></div>", root.outerHTML)
}

@Test
fun testElementBuilderCreate() {
val custom = ElementBuilder.createBuilder<HTMLElement>("custom")
val div = ElementBuilder.createBuilder<HTMLElement>("div")
val b = ElementBuilder.createBuilder<HTMLElement>("b")
val abc = ElementBuilder.createBuilder<HTMLElement>("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<HTMLElement>.() -> Unit,
content: ContentBuilder<HTMLElement>? = null
) {
TagElement(
tagName = tagName,
applyAttrs = attrs,
content
)
}

var tagName by mutableStateOf("custom")

composition {
CustomElement(tagName, {
id("container")
}) {
Text("CUSTOM")
}
}

assertEquals("<div><custom id=\"container\">CUSTOM</custom></div>", root.outerHTML)

tagName = "anothercustom"
waitForRecompositionComplete()

assertEquals("<div><anothercustom id=\"container\">CUSTOM</anothercustom></div>", root.outerHTML)
}

@Test
fun elementBuilderShouldBeCalledOnce() = runTest {
var counter = 0
Expand Down

0 comments on commit ee9bba2

Please sign in to comment.