Add dynamic attributes

This commit is contained in:
Paul-Henri Froidmont 2025-08-15 01:04:01 +02:00
parent 82c0922cfa
commit 62c1a8a9f4
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
4 changed files with 53 additions and 3 deletions

View file

@ -8,6 +8,10 @@ class LiveView[Model] private (
val static: ArraySeq[String], val static: ArraySeq[String],
val dynamic: ArraySeq[LiveDyn[Model]] val dynamic: ArraySeq[LiveDyn[Model]]
): ):
assert(
static.size == dynamic.size + 1,
s"Static size : ${static.size}, Dynamic size : ${dynamic.size}"
)
def update(model: Model): Unit = def update(model: Model): Unit =
dynamic.foreach(_.update(model)) dynamic.foreach(_.update(model))
@ -70,6 +74,8 @@ object LiveView:
val (attrs, children) = el.mods.partitionMap { val (attrs, children) = el.mods.partitionMap {
case Mod.StaticAttr(attr, value) => case Mod.StaticAttr(attr, value) =>
Left(List(Some(s""" ${attr.name}="$value""""))) Left(List(Some(s""" ${attr.name}="$value"""")))
case Mod.DynAttr(attr, _) =>
Left(List(Some(s""" ${attr.name}=""""), None, Some('"'.toString)))
case Mod.Tag(el) => Right(buildStaticFragments(el)) case Mod.Tag(el) => Right(buildStaticFragments(el))
case Mod.Text(text) => Right(List(Some(text))) case Mod.Text(text) => Right(List(Some(text)))
case Mod.DynText[Model](_) => Right(List(None)) case Mod.DynText[Model](_) => Right(List(None))
@ -85,8 +91,10 @@ object LiveView:
startsUpdated: Boolean = false startsUpdated: Boolean = false
): Seq[LiveDyn[Model]] = ): Seq[LiveDyn[Model]] =
val (attrs, children) = el.mods.partitionMap { val (attrs, children) = el.mods.partitionMap {
case Mod.StaticAttr(_, _) => Left(List.empty)
case Mod.Text(_) => Right(List.empty) case Mod.Text(_) => Right(List.empty)
case Mod.StaticAttr(_, _) => Left(List.empty)
case Mod.DynAttr(_, value) =>
Right(List(LiveDyn.Value(value, model, startsUpdated)))
case Mod.Tag(el) => case Mod.Tag(el) =>
Right(buildDynamic(el, model, startsUpdated)) Right(buildDynamic(el, model, startsUpdated))
case Mod.DynText[Model](dynText) => case Mod.DynText[Model](dynText) =>

View file

@ -26,6 +26,7 @@ object Dyn:
enum Mod[T]: enum Mod[T]:
case StaticAttr(attr: HtmlAttr, value: String) case StaticAttr(attr: HtmlAttr, value: String)
case DynAttr(attr: HtmlAttr, value: Dyn[T, String])
case Tag(el: HtmlElement[T]) case Tag(el: HtmlElement[T])
case Text(text: String) case Text(text: String)
case DynText(dynText: Dyn[T, String]) case DynText(dynText: Dyn[T, String])
@ -51,5 +52,7 @@ val li = HtmlTag("li")
class HtmlAttr(val name: String): class HtmlAttr(val name: String):
def :=[T](value: String): Mod.StaticAttr[T] = Mod.StaticAttr(this, value) def :=[T](value: String): Mod.StaticAttr[T] = Mod.StaticAttr(this, value)
def :=[T](value: Dyn[T, String]): Mod.DynAttr[T] = Mod.DynAttr(this, value)
val idAttr = HtmlAttr("id") val idAttr = HtmlAttr("id")
val cls = HtmlAttr("class")

View file

@ -58,17 +58,18 @@ def main =
println("Remove all") println("Remove all")
lv.update( lv.update(
MyModel(List.empty) MyModel(List.empty, "text-lg")
) )
println(lv.diff.toJsonPretty) println(lv.diff.toJsonPretty)
final case class MyModel(elems: List[NestedModel]) final case class MyModel(elems: List[NestedModel], cls: String = "text-xs")
final case class NestedModel(name: String, age: Int) final case class NestedModel(name: String, age: Int)
object TestView extends View[MyModel]: object TestView extends View[MyModel]:
val root: HtmlElement[MyModel] = val root: HtmlElement[MyModel] =
div( div(
idAttr := "42", idAttr := "42",
cls := model(_.cls),
ul( ul(
model.splitByIndex(_.elems)(elem => model.splitByIndex(_.elems)(elem =>
li( li(

View file

@ -10,6 +10,7 @@ object LiveViewSpec extends TestSuite:
title: String = "title value", title: String = "title value",
bool: Boolean = false, bool: Boolean = false,
nestedTitle: String = "nested title value", nestedTitle: String = "nested title value",
cls: String = "text-sm",
items: List[NestedModel] = List.empty items: List[NestedModel] = List.empty
) )
final case class NestedModel(name: String, age: Int) final case class NestedModel(name: String, age: Int)
@ -79,6 +80,43 @@ object LiveViewSpec extends TestSuite:
} }
} }
test("Dynamic attribute") {
val lv =
LiveView(
new View[TestModel]:
val root: HtmlElement[TestModel] =
div(cls := model(_.cls))
,
TestModel()
)
test("init") {
assertEqualsJson(
lv.fullDiff,
Json
.Obj(
"s" -> Json
.Arr(Json.Str("<div class=\""), Json.Str("\"></div>")),
"0" -> Json.Str("text-sm")
)
)
}
test("diff no update") {
assertEqualsJson(lv.diff, emptyDiff)
}
test("diff with update") {
lv.update(TestModel(cls = "text-md"))
assertEqualsJson(
lv.diff,
Json.Obj("0" -> Json.Str("text-md"))
)
}
test("diff with update and no change") {
lv.update(TestModel(cls = "text-md"))
lv.update(TestModel(cls = "text-md"))
assertEqualsJson(lv.diff, emptyDiff)
}
}
test("when mod") { test("when mod") {
val lv = val lv =
LiveView( LiveView(