diff --git a/core/src/TestLiveView.scala b/core/src/TestLiveView.scala new file mode 100644 index 0000000..ca676c6 --- /dev/null +++ b/core/src/TestLiveView.scala @@ -0,0 +1,54 @@ +import scalive.* + +final case class MyModel(cls: String = "text-xs", bool: Boolean = true) +final case class Elem(name: String, age: Int) + +class TestView extends LiveView[TestView.Cmd]: + import TestView.Cmd.* + + private val textCls = LiveState.Key[String] + private val someBool = LiveState.Key[Boolean] + private val elems = LiveState.Key[List[Elem]] + + def mount(state: LiveState): LiveState = + state + .set(textCls, "text-xs") + .set(someBool, true) + .set( + elems, + List( + Elem("a", 10), + Elem("b", 15), + Elem("c", 20) + ) + ) + + def handleCommand(cmd: TestView.Cmd, state: LiveState): LiveState = cmd match + case UpdateElems(es) => state.set(elems, es) + case UpdateBool(b) => state.set(someBool, b) + case UpdateTextCls(cls) => state.set(textCls, cls) + + val render = + div( + idAttr := "42", + cls := textCls, + draggable := someBool, + disabled := someBool, + ul( + elems.splitByIndex(elem => + li( + "Nom: ", + elem(_.name), + " Age: ", + elem(_.age.toString) + ) + ) + ) + ) +end TestView + +object TestView: + enum Cmd: + case UpdateElems(es: List[Elem]) + case UpdateBool(b: Boolean) + case UpdateTextCls(cls: String) diff --git a/core/src/main.scala b/core/src/main.scala new file mode 100644 index 0000000..3de870b --- /dev/null +++ b/core/src/main.scala @@ -0,0 +1,62 @@ +import scalive.* + +@main +def main = + val s = Socket(TestView()) + println("Init") + println(s.renderHtml) + s.syncClient + s.syncClient + + println("Edit first and last") + s.receiveCommand( + TestView.Cmd.UpdateTextCls("text-lg") + ) + s.syncClient + + println("Edit first and last") + s.receiveCommand( + TestView.Cmd.UpdateElems( + List( + Elem("x", 10), + Elem("b", 15), + Elem("c", 99) + ) + ) + ) + s.syncClient + + println("Add one") + s.receiveCommand( + TestView.Cmd.UpdateElems( + List( + Elem("x", 10), + Elem("b", 15), + Elem("c", 99), + Elem("d", 35) + ) + ) + ) + s.syncClient + + // + // println("Remove first") + // lv.update( + // MyModel( + // List( + // NestedModel("b", 15), + // NestedModel("c", 99), + // NestedModel("d", 35) + // ) + // ) + // ) + // println(lv.diff.toJsonPretty) + // println(HtmlBuilder.build(lv)) + // + // println("Remove all") + // lv.update( + // MyModel(List.empty, "text-lg", bool = false) + // ) + // println(lv.diff.toJsonPretty) + // println(HtmlBuilder.build(lv)) +end main diff --git a/core/src/scalive/HtmlElement.scala b/core/src/scalive/HtmlElement.scala index 4c479b0..5bfaeec 100644 --- a/core/src/scalive/HtmlElement.scala +++ b/core/src/scalive/HtmlElement.scala @@ -41,10 +41,6 @@ enum Mod: dynList: Dyn[List[T]], project: Dyn[T] => HtmlElement) -given [T]: Conversion[HtmlElement, Mod] = Mod.Tag(_) -given [T]: Conversion[String, Mod] = Mod.Text(_) -given [T]: Conversion[Dyn[String], Mod] = Mod.DynText(_) - final case class Dyn[T](key: LiveState.Key, f: key.Type => T): def render(state: LiveState, trackUpdates: Boolean): Option[T] = val entry = state(key) diff --git a/core/src/scalive/LiveView.scala b/core/src/scalive/LiveView.scala index fa6c9a0..48e2f27 100644 --- a/core/src/scalive/LiveView.scala +++ b/core/src/scalive/LiveView.scala @@ -1,4 +1,6 @@ package scalive -trait LiveView: +trait LiveView[Cmd]: + def mount(state: LiveState): LiveState + def handleCommand(cmd: Cmd, state: LiveState): LiveState def render: HtmlElement diff --git a/core/src/scalive/Scalive.scala b/core/src/scalive/Scalive.scala index 4513774..fddebea 100644 --- a/core/src/scalive/Scalive.scala +++ b/core/src/scalive/Scalive.scala @@ -1,9 +1,9 @@ -package scalive - import scalive.defs.attrs.HtmlAttrs import scalive.defs.complex.ComplexHtmlKeys import scalive.defs.tags.HtmlTags -object Scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys - -export Scalive.* +package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys: + implicit def stringToMod(v: String): Mod = Mod.Text(v) + implicit def htmlElementToMod(el: HtmlElement): Mod = Mod.Tag(el) + implicit def dynStringToMod(d: Dyn[String]): Mod = Mod.DynText(d) + implicit def keyToDyn(k: LiveState.Key): Dyn[k.Type] = k.id diff --git a/core/src/scalive/Socket.scala b/core/src/scalive/Socket.scala index 7bce940..bd58672 100644 --- a/core/src/scalive/Socket.scala +++ b/core/src/scalive/Socket.scala @@ -2,18 +2,19 @@ package scalive import zio.json.* -final case class Socket(view: LiveView, initState: LiveState): - private var state: LiveState = initState +final case class Socket[Cmd](lv: LiveView[Cmd]): + private var state: LiveState = lv.mount(LiveState.empty) private var fingerprint: Fingerprint = Fingerprint.empty val id: String = "scl-123" - def setState(newState: LiveState): Unit = state = newState - def updateState(f: LiveState => LiveState): Unit = state = f(state) - def renderHtml: String = - HtmlBuilder.build(Rendered.render(view.render, state), isRoot = true) + def receiveCommand(cmd: Cmd): Unit = + state = lv.handleCommand(cmd, state) + + def renderHtml: String = + HtmlBuilder.build(Rendered.render(lv.render, state), isRoot = true) + def syncClient: Unit = - val r = Rendered.render(view.render, state) + val r = Rendered.render(lv.render, state) println(DiffBuilder.build(r, fingerprint).toJsonPretty) fingerprint = Fingerprint(r) - println(fingerprint) state = state.setAllUnchanged diff --git a/core/src/scalive/main.scala b/core/src/scalive/main.scala deleted file mode 100644 index 3b7fa79..0000000 --- a/core/src/scalive/main.scala +++ /dev/null @@ -1,123 +0,0 @@ -package scalive - -@main -def main = - val s = Socket( - TestView, - LiveState.empty.set( - TestView.model, - MyModel( - List( - NestedModel("a", 10), - NestedModel("b", 15), - NestedModel("c", 20) - ) - ) - ) - ) - println("Init") - println(s.renderHtml) - s.syncClient - s.syncClient - - println("Edit first and last") - s.updateState( - _.set( - TestView.model, - MyModel( - List( - NestedModel("x", 10), - NestedModel("b", 15), - NestedModel("c", 99) - ) - ) - ) - ) - s.syncClient - // val lv = - // LiveView( - // TestView, - // LiveState.empty.set( - // TestView.model, - // MyModel( - // List( - // NestedModel("a", 10), - // NestedModel("b", 15), - // NestedModel("c", 20) - // ) - // ) - // ) - // ) - // println(lv.fullDiff.toJsonPretty) - // println(HtmlBuilder.build(lv)) - // - // println("Edit first and last") - // lv.update( - // MyModel( - // List( - // NestedModel("x", 10), - // NestedModel("b", 15), - // NestedModel("c", 99) - // ) - // ) - // ) - // println(lv.diff.toJsonPretty) - // println(HtmlBuilder.build(lv)) - // - // println("Add one") - // lv.update( - // MyModel( - // List( - // NestedModel("x", 10), - // NestedModel("b", 15), - // NestedModel("c", 99), - // NestedModel("d", 35) - // ) - // ) - // ) - // println(lv.diff.toJsonPretty) - // println(HtmlBuilder.build(lv)) - // - // println("Remove first") - // lv.update( - // MyModel( - // List( - // NestedModel("b", 15), - // NestedModel("c", 99), - // NestedModel("d", 35) - // ) - // ) - // ) - // println(lv.diff.toJsonPretty) - // println(HtmlBuilder.build(lv)) - // - // println("Remove all") - // lv.update( - // MyModel(List.empty, "text-lg", bool = false) - // ) - // println(lv.diff.toJsonPretty) - // println(HtmlBuilder.build(lv)) -end main - -final case class MyModel(elems: List[NestedModel], cls: String = "text-xs", bool: Boolean = true) -final case class NestedModel(name: String, age: Int) - -object TestView extends LiveView: - val model = LiveState.Key[MyModel] - val render = - div( - idAttr := "42", - cls := model(_.cls), - draggable := model(_.bool), - disabled := model(_.bool), - ul( - model(_.elems).splitByIndex(elem => - li( - "Nom: ", - elem(_.name), - " Age: ", - elem(_.age.toString) - ) - ) - ) - )