diff --git a/core/src/scalive/DiffBuilder.scala b/core/src/scalive/DiffBuilder.scala index 689dc08..a5ae4d6 100644 --- a/core/src/scalive/DiffBuilder.scala +++ b/core/src/scalive/DiffBuilder.scala @@ -3,7 +3,7 @@ package scalive object DiffBuilder: def build( static: Seq[String], - dynamic: Seq[LiveMod[?]], + dynamic: Seq[LiveDyn[?]], includeUnchanged: Boolean = false ): Diff.Tag = Diff.Tag( @@ -11,16 +11,16 @@ object DiffBuilder: dynamic = dynamic.zipWithIndex .filter(includeUnchanged || _._1.wasUpdated) .map { - case (v: LiveMod.Dynamic[?, ?], i) => + case (v: LiveDyn.Value[?, ?], i) => Diff.Dynamic(i, Diff.Static(v.currentValue.toString)) - case (v: LiveMod.When[?], i) => build(v, i, includeUnchanged) - case (v: LiveMod.Split[?, ?], i) => + case (v: LiveDyn.When[?], i) => build(v, i, includeUnchanged) + case (v: LiveDyn.Split[?, ?], i) => Diff.Dynamic(i, build(v, includeUnchanged)) } ) private def build( - mod: LiveMod.When[?], + mod: LiveDyn.When[?], index: Int, includeUnchanged: Boolean ): Diff.Dynamic = @@ -46,7 +46,7 @@ object DiffBuilder: else Diff.Dynamic(index, Diff.Deleted) private def build( - mod: LiveMod.Split[?, ?], + mod: LiveDyn.Split[?, ?], includeUnchanged: Boolean ): Diff.Split = Diff.Split( diff --git a/core/src/scalive/HtmlBuilder.scala b/core/src/scalive/HtmlBuilder.scala index 5d08d61..8242a35 100644 --- a/core/src/scalive/HtmlBuilder.scala +++ b/core/src/scalive/HtmlBuilder.scala @@ -11,20 +11,20 @@ object HtmlBuilder: private def build( static: Seq[String], - dynamic: Seq[LiveMod[?]], + dynamic: Seq[LiveDyn[?]], strw: StringWriter ): Unit = for i <- dynamic.indices do strw.append(static(i)) dynamic(i) match - case mod: LiveMod.Dynamic[?, ?] => + case mod: LiveDyn.Value[?, ?] => strw.append(mod.currentValue.toString) - case mod: LiveMod.When[?] => build(mod, strw) - case mod: LiveMod.Split[?, ?] => build(mod, strw) + case mod: LiveDyn.When[?] => build(mod, strw) + case mod: LiveDyn.Split[?, ?] => build(mod, strw) strw.append(static.last) - private def build(mod: LiveMod.When[?], strw: StringWriter): Unit = + private def build(mod: LiveDyn.When[?], strw: StringWriter): Unit = if mod.displayed then build(mod.nested.static, mod.nested.dynamic, strw) - private def build(mod: LiveMod.Split[?, ?], strw: StringWriter): Unit = + private def build(mod: LiveDyn.Split[?, ?], strw: StringWriter): Unit = mod.dynamic.foreach(entry => build(mod.static, entry, strw)) diff --git a/core/src/scalive/LiveMod.scala b/core/src/scalive/LiveDyn.scala similarity index 64% rename from core/src/scalive/LiveMod.scala rename to core/src/scalive/LiveDyn.scala index 2fdafa2..7f21cae 100644 --- a/core/src/scalive/LiveMod.scala +++ b/core/src/scalive/LiveDyn.scala @@ -3,14 +3,14 @@ package scalive import scala.collection.immutable.ArraySeq import scala.collection.mutable.ArrayBuffer -sealed trait LiveMod[Model]: +sealed trait LiveDyn[Model]: def update(model: Model): Unit def wasUpdated: Boolean -object LiveMod: +object LiveDyn: - class Dynamic[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false) - extends LiveMod[I]: + class Value[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false) + extends LiveDyn[I]: private var value: O = d.run(init) private var updated: Boolean = startsUpdated def wasUpdated: Boolean = updated @@ -24,11 +24,11 @@ object LiveMod: class When[Model]( dynCond: Dyn[Model, Boolean], - tag: HtmlTag[Model], + el: HtmlElement[Model], init: Model - ) extends LiveMod[Model]: - val cond = LiveMod.Dynamic(dynCond, init) - val nested = LiveView.render(tag, init) + ) extends LiveDyn[Model]: + val cond = LiveDyn.Value(dynCond, init) + val nested = LiveView.render(el, init) def displayed: Boolean = cond.currentValue def wasUpdated: Boolean = cond.wasUpdated || nested.wasUpdated def update(model: Model): Unit = @@ -37,13 +37,16 @@ object LiveMod: class Split[Model, Item]( dynList: Dyn[Model, List[Item]], - project: Dyn[Item, Item] => HtmlTag[Item], + project: Dyn[Item, Item] => HtmlElement[Item], init: Model - ) extends LiveMod[Model]: - private val tag = project(Dyn.id) - val static: ArraySeq[String] = LiveView.buildStatic(tag) - val dynamic: ArrayBuffer[ArraySeq[LiveMod[Item]]] = - dynList.run(init).map(LiveView.buildDynamic(tag, _)).to(ArrayBuffer) + ) extends LiveDyn[Model]: + private val el = project(Dyn.id) + val static: ArraySeq[String] = LiveView.buildStatic(el) + val dynamic: ArrayBuffer[ArraySeq[LiveDyn[Item]]] = + dynList + .run(init) + .map(LiveView.buildDynamic(el, _).to(ArraySeq)) + .to(ArrayBuffer) var removedIndexes: Seq[Int] = Seq.empty def wasUpdated: Boolean = @@ -57,6 +60,8 @@ object LiveMod: dynamic.takeInPlace(items.size) items.zipWithIndex.map((item, i) => if i >= dynamic.size then - dynamic.append(LiveView.buildDynamic(tag, item, startsUpdated = true)) + dynamic.append( + LiveView.buildDynamic(el, item, startsUpdated = true).to(ArraySeq) + ) else dynamic(i).foreach(_.update(item)) ) diff --git a/core/src/scalive/LiveView.scala b/core/src/scalive/LiveView.scala index 259c777..566eb5d 100644 --- a/core/src/scalive/LiveView.scala +++ b/core/src/scalive/LiveView.scala @@ -6,7 +6,7 @@ import scala.collection.mutable.ListBuffer class LiveView[Model] private ( val static: ArraySeq[String], - val dynamic: ArraySeq[LiveMod[Model]] + val dynamic: ArraySeq[LiveDyn[Model]] ): def update(model: Model): Unit = dynamic.foreach(_.update(model)) @@ -21,65 +21,79 @@ class LiveView[Model] private ( object LiveView: - def apply[Model]( + inline def apply[Model]( lv: View[Model], model: Model ): LiveView[Model] = - render(lv.view, model) + render(lv.root, model) - def buildStatic[Model](tag: HtmlTag[Model]): ArraySeq[String] = - buildNestedStatic(tag).flatten.to(ArraySeq) + def render[Model]( + tag: HtmlElement[Model], + model: Model + ): LiveView[Model] = + new LiveView(buildStatic(tag), buildDynamic(tag, model).to(ArraySeq)) - private def buildNestedStatic[Model]( - tag: HtmlTag[Model] + def buildStatic[Model](el: HtmlElement[Model]): ArraySeq[String] = + buildStaticFragments(el).flatten.to(ArraySeq) + + private def buildStaticFragments[Model]( + el: HtmlElement[Model] ): Seq[Option[String]] = + val (attrs, children) = buildStaticFragmentsByType(el) val static = ListBuffer.empty[Option[String]] - var staticFragment = s"<${tag.name}>" - for mod <- tag.mods.flatMap(buildStatic) do - mod match + var staticFragment = s"<${el.tag.name}" + for attr <- attrs do + attr match case Some(s) => staticFragment += s case None => static.append(Some(staticFragment)) static.append(None) staticFragment = "" - staticFragment += s"" + staticFragment += s">" + for child <- children do + child match + case Some(s) => + staticFragment += s + case None => + static.append(Some(staticFragment)) + static.append(None) + staticFragment = "" + staticFragment += s"" static.append(Some(staticFragment)) static.toSeq - def buildStatic[Model](mod: Mod[Model]): Seq[Option[String]] = - mod match - case Mod.Tag(tag) => buildNestedStatic(tag) - case Mod.Text(text) => List(Some(text)) - case Mod.DynText(_) => List(None) - case Mod.When(_, _) => List(None) - case Mod.Split(_, _) => List(None) - - def buildDynamic[Model]( - tag: HtmlTag[Model], - model: Model, - startsUpdated: Boolean = false - ): ArraySeq[LiveMod[Model]] = - tag.mods.flatMap(buildDynamic(_, model, startsUpdated)).to(ArraySeq) + @nowarn("cat=unchecked") + private def buildStaticFragmentsByType[Model]( + el: HtmlElement[Model] + ): (attrs: Seq[Option[String]], children: Seq[Option[String]]) = + val (attrs, children) = el.mods.partitionMap { + case Mod.StaticAttr(attr, value) => + Left(List(Some(s""" ${attr.name}="$value""""))) + case Mod.Tag(el) => Right(buildStaticFragments(el)) + case Mod.Text(text) => Right(List(Some(text))) + case Mod.DynText[Model](_) => Right(List(None)) + case Mod.When[Model](_, _) => Right(List(None)) + case Mod.Split[Model, Any](_, _) => Right(List(None)) + } + (attrs.flatten, children.flatten) @nowarn("cat=unchecked") def buildDynamic[Model]( - mod: Mod[Model], + el: HtmlElement[Model], model: Model, - startsUpdated: Boolean - ): Seq[LiveMod[Model]] = - mod match - case Mod.Tag(tag) => buildDynamic(tag, model, startsUpdated) - case Mod.Text(text) => List.empty + startsUpdated: Boolean = false + ): Seq[LiveDyn[Model]] = + val (attrs, children) = el.mods.partitionMap { + case Mod.StaticAttr(_, _) => Left(List.empty) + case Mod.Text(_) => Right(List.empty) + case Mod.Tag(el) => + Right(buildDynamic(el, model, startsUpdated)) case Mod.DynText[Model](dynText) => - List(LiveMod.Dynamic(dynText, model, startsUpdated)) - case Mod.When[Model](dynCond, tag) => - List(LiveMod.When(dynCond, tag, model)) + Right(List(LiveDyn.Value(dynText, model, startsUpdated))) + case Mod.When[Model](dynCond, el) => + Right(List(LiveDyn.When(dynCond, el, model))) case Mod.Split[Model, Any](dynList, project) => - List(LiveMod.Split(dynList, project, model)) - - def render[Model]( - tag: HtmlTag[Model], - model: Model - ): LiveView[Model] = - new LiveView(buildStatic(tag), buildDynamic(tag, model)) + Right(List(LiveDyn.Split(dynList, project, model))) + } + attrs.flatten ++ children.flatten diff --git a/core/src/scalive/View.scala b/core/src/scalive/View.scala index 7a58181..a3e567f 100644 --- a/core/src/scalive/View.scala +++ b/core/src/scalive/View.scala @@ -2,20 +2,20 @@ package scalive trait View[Model]: val model: Dyn[Model, Model] = Dyn.id - def view: HtmlTag[Model] + def root: HtmlElement[Model] opaque type Dyn[I, O] = I => O extension [I, O](d: Dyn[I, O]) def apply[O2](f: O => O2): Dyn[I, O2] = d.andThen(f) - def when(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] = - Mod.When(d.andThen(f), tag) + def when(f: O => Boolean)(el: HtmlElement[I]): Mod.When[I] = + Mod.When(d.andThen(f), el) - inline def whenNot(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] = - when(f.andThen(!_))(tag) + inline def whenNot(f: O => Boolean)(el: HtmlElement[I]): Mod.When[I] = + when(f.andThen(!_))(el) def splitByIndex[O2](f: O => List[O2])( - project: Dyn[O2, O2] => HtmlTag[O2] + project: Dyn[O2, O2] => HtmlElement[O2] ): Mod.Split[I, O2] = Mod.Split(d.andThen(f), project) @@ -25,26 +25,31 @@ object Dyn: def id[T]: Dyn[T, T] = identity enum Mod[T]: - case Tag(tag: HtmlTag[T]) + case StaticAttr(attr: HtmlAttr, value: String) + case Tag(el: HtmlElement[T]) case Text(text: String) case DynText(dynText: Dyn[T, String]) - case When(dynCond: Dyn[T, Boolean], tag: HtmlTag[T]) + case When(dynCond: Dyn[T, Boolean], el: HtmlElement[T]) case Split[T, O]( dynList: Dyn[T, List[O]], - project: Dyn[O, O] => HtmlTag[O] + project: Dyn[O, O] => HtmlElement[O] ) extends Mod[T] -given [T]: Conversion[HtmlTag[T], Mod[T]] = Mod.Tag(_) +given [T]: Conversion[HtmlElement[T], Mod[T]] = Mod.Tag(_) given [T]: Conversion[String, Mod[T]] = Mod.Text(_) given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_) -trait HtmlTag[Model](val name: String): - def mods: List[Mod[Model]] +class HtmlTag(val name: String): + def apply[Model](mods: Mod[Model]*): HtmlElement[Model] = + HtmlElement(this, mods.toVector) -class Div[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("div") -class Ul[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("ul") -class Li[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("li") +class HtmlElement[Model](val tag: HtmlTag, val mods: Vector[Mod[Model]]) -def div[Model](mods: Mod[Model]*): Div[Model] = Div(mods.toList) -def ul[Model](mods: Mod[Model]*): Ul[Model] = Ul(mods.toList) -def li[Model](mods: Mod[Model]*): Li[Model] = Li(mods.toList) +val div = HtmlTag("div") +val ul = HtmlTag("ul") +val li = HtmlTag("li") + +class HtmlAttr(val name: String): + def :=[T](value: String): Mod.StaticAttr[T] = Mod.StaticAttr(this, value) + +val idAttr = HtmlAttr("id") diff --git a/core/src/scalive/main.scala b/core/src/scalive/main.scala index 36c8ca7..2de2482 100644 --- a/core/src/scalive/main.scala +++ b/core/src/scalive/main.scala @@ -66,8 +66,9 @@ final case class MyModel(elems: List[NestedModel]) final case class NestedModel(name: String, age: Int) object TestView extends View[MyModel]: - val view: HtmlTag[MyModel] = + val root: HtmlElement[MyModel] = div( + idAttr := "42", ul( model.splitByIndex(_.elems)(elem => li( diff --git a/core/test/src/scalive/LiveViewSpec.scala b/core/test/src/scalive/LiveViewSpec.scala index bcae4ba..a483101 100644 --- a/core/test/src/scalive/LiveViewSpec.scala +++ b/core/test/src/scalive/LiveViewSpec.scala @@ -25,7 +25,7 @@ object LiveViewSpec extends TestSuite: val lv = LiveView( new View[Unit]: - val view: HtmlTag[Unit] = + val root: HtmlElement[Unit] = div("Static string") , () @@ -47,7 +47,7 @@ object LiveViewSpec extends TestSuite: val lv = LiveView( new View[TestModel]: - val view: HtmlTag[TestModel] = + val root: HtmlElement[TestModel] = div(model(_.title)) , TestModel() @@ -83,7 +83,7 @@ object LiveViewSpec extends TestSuite: val lv = LiveView( new View[TestModel]: - val view: HtmlTag[TestModel] = + val root: HtmlElement[TestModel] = div( model.when(_.bool)( div("static string", model(_.nestedTitle)) @@ -152,7 +152,7 @@ object LiveViewSpec extends TestSuite: val lv = LiveView( new View[TestModel]: - val view: HtmlTag[TestModel] = + val root: HtmlElement[TestModel] = div( ul( model.splitByIndex(_.items)(elem =>