mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Complete redesign to allow assigns style state
This commit is contained in:
parent
ae0dc04a9e
commit
bb062b9679
18 changed files with 802 additions and 739 deletions
|
|
@ -7,7 +7,7 @@ import mill.api.Task.Simple
|
||||||
|
|
||||||
trait Common extends ScalaModule:
|
trait Common extends ScalaModule:
|
||||||
def scalaVersion = "3.7.2"
|
def scalaVersion = "3.7.2"
|
||||||
def scalacOptions = Seq("-Wunused:all")
|
def scalacOptions = Seq("-Wunused:all", "-preview", "-feature", "-language:implicitConversions")
|
||||||
|
|
||||||
object core extends Common:
|
object core extends Common:
|
||||||
def mvnDeps = Seq(mvn"dev.zio::zio-json:0.7.44")
|
def mvnDeps = Seq(mvn"dev.zio::zio-json:0.7.44")
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
package scalive
|
|
||||||
|
|
||||||
import zio.Chunk
|
|
||||||
import zio.json.JsonEncoder
|
|
||||||
import zio.json.ast.Json
|
|
||||||
|
|
||||||
enum Diff:
|
|
||||||
case Tag(
|
|
||||||
static: Seq[String] = Seq.empty,
|
|
||||||
dynamic: Seq[Diff.Dynamic] = Seq.empty)
|
|
||||||
case Split(
|
|
||||||
static: Seq[String] = Seq.empty,
|
|
||||||
entries: Seq[Diff.Dynamic] = Seq.empty)
|
|
||||||
case Static(value: String)
|
|
||||||
case Dynamic(index: Int, diff: Diff)
|
|
||||||
case Deleted
|
|
||||||
|
|
||||||
object Diff:
|
|
||||||
given JsonEncoder[Diff] = JsonEncoder[Json].contramap(toJson)
|
|
||||||
|
|
||||||
private def toJson(diff: Diff): Json =
|
|
||||||
diff match
|
|
||||||
case Diff.Tag(static, dynamic) =>
|
|
||||||
Json.Obj(
|
|
||||||
Option
|
|
||||||
.when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*))
|
|
||||||
.to(Chunk)
|
|
||||||
.appendedAll(
|
|
||||||
dynamic.map(d => d.index.toString -> toJson(d.diff))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
case Diff.Split(static, entries) =>
|
|
||||||
Json.Obj(
|
|
||||||
Option
|
|
||||||
.when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*))
|
|
||||||
.to(Chunk)
|
|
||||||
.appendedAll(
|
|
||||||
Option.when(entries.nonEmpty)(
|
|
||||||
"d" ->
|
|
||||||
Json.Obj(
|
|
||||||
entries.map(d => d.index.toString -> toJson(d.diff))*
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
case Diff.Static(value) => Json.Str(value)
|
|
||||||
case Diff.Dynamic(index, diff) =>
|
|
||||||
Json.Obj(index.toString -> toJson(diff))
|
|
||||||
case Diff.Deleted => Json.Bool(false)
|
|
||||||
end Diff
|
|
||||||
|
|
@ -1,71 +1,53 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
|
import zio.Chunk
|
||||||
|
import zio.json.ast.Json
|
||||||
|
|
||||||
object DiffBuilder:
|
object DiffBuilder:
|
||||||
def build(
|
def build(rendered: Rendered, fingerprint: Fingerprint): Json =
|
||||||
static: Seq[String],
|
val nestedFingerprintIter = fingerprint.nested.iterator
|
||||||
dynamic: Seq[LiveDyn[?]],
|
Json.Obj(
|
||||||
includeUnchanged: Boolean = false
|
Option
|
||||||
): Diff.Tag =
|
.when(rendered.static.nonEmpty && fingerprint.value != rendered.fingerprint)(
|
||||||
Diff.Tag(
|
"s" -> Json.Arr(rendered.static.map(Json.Str(_))*)
|
||||||
static = static,
|
|
||||||
dynamic = dynamic.zipWithIndex
|
|
||||||
.filter(includeUnchanged || _._1.wasUpdated)
|
|
||||||
.map {
|
|
||||||
case (v: LiveDyn.Value[?, ?], i) =>
|
|
||||||
Diff.Dynamic(i, Diff.Static(v.currentValue.toString))
|
|
||||||
case (v: LiveDyn.When[?], i) => build(v, i, includeUnchanged)
|
|
||||||
case (v: LiveDyn.Split[?, ?], i) =>
|
|
||||||
Diff.Dynamic(i, build(v, includeUnchanged))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
private def build(
|
|
||||||
mod: LiveDyn.When[?],
|
|
||||||
index: Int,
|
|
||||||
includeUnchanged: Boolean
|
|
||||||
): Diff.Dynamic =
|
|
||||||
if mod.displayed then
|
|
||||||
if includeUnchanged || mod.cond.wasUpdated then
|
|
||||||
Diff.Dynamic(
|
|
||||||
index,
|
|
||||||
build(
|
|
||||||
mod.nested.static,
|
|
||||||
mod.nested.dynamic,
|
|
||||||
includeUnchanged = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Diff.Dynamic(
|
|
||||||
index,
|
|
||||||
build(
|
|
||||||
static = Seq.empty,
|
|
||||||
mod.nested.dynamic,
|
|
||||||
includeUnchanged
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else Diff.Dynamic(index, Diff.Deleted)
|
|
||||||
|
|
||||||
private def build(
|
|
||||||
mod: LiveDyn.Split[?, ?],
|
|
||||||
includeUnchanged: Boolean
|
|
||||||
): Diff.Split =
|
|
||||||
Diff.Split(
|
|
||||||
static = if includeUnchanged then mod.static else Seq.empty,
|
|
||||||
entries = mod.dynamic.toList.zipWithIndex
|
|
||||||
.filter(includeUnchanged || _._1.exists(_.wasUpdated))
|
|
||||||
.map[Diff.Dynamic]((mods, i) =>
|
|
||||||
Diff.Dynamic(
|
|
||||||
i,
|
|
||||||
build(
|
|
||||||
static = Seq.empty,
|
|
||||||
dynamic = mods,
|
|
||||||
includeUnchanged
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
.to(Chunk)
|
||||||
.appendedAll(
|
.appendedAll(
|
||||||
mod.removedIndexes
|
rendered.dynamic.zipWithIndex
|
||||||
.map(i => Diff.Dynamic(i, Diff.Deleted))
|
.map((render, index) => index.toString -> build(render(true), nestedFingerprintIter))
|
||||||
|
).filterNot(_._2 == Json.Obj.empty)
|
||||||
|
)
|
||||||
|
|
||||||
|
private def build(comp: Comprehension, fingerprint: Fingerprint): Json =
|
||||||
|
val nestedFingerprintIter = fingerprint.nested.iterator
|
||||||
|
Json.Obj(
|
||||||
|
Option
|
||||||
|
.when(comp.static.nonEmpty && fingerprint.value != comp.fingerprint)(
|
||||||
|
"s" -> Json.Arr(comp.static.map(Json.Str(_))*)
|
||||||
|
)
|
||||||
|
.to(Chunk)
|
||||||
|
.appendedAll(
|
||||||
|
Option.when(comp.entries.nonEmpty)(
|
||||||
|
"d" ->
|
||||||
|
Json.Arr(
|
||||||
|
comp.entries.map(render =>
|
||||||
|
Json.Obj(
|
||||||
|
render(true).zipWithIndex
|
||||||
|
.map((dyn, index) =>
|
||||||
|
index.toString -> build(dyn, nestedFingerprintIter)
|
||||||
|
).filterNot(_._2 == Json.Obj.empty)*
|
||||||
|
)
|
||||||
|
)*
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private def build(dyn: RenderedDyn, fingerprintIter: Iterator[Fingerprint]): Json =
|
||||||
|
dyn match
|
||||||
|
case Some(s: String) => Json.Str(s)
|
||||||
|
case Some(r: Rendered) => build(r, fingerprintIter.nextOption.getOrElse(Fingerprint.empty))
|
||||||
|
case Some(c: Comprehension) =>
|
||||||
|
build(c, fingerprintIter.nextOption.getOrElse(Fingerprint.empty))
|
||||||
|
case None => Json.Obj.empty
|
||||||
end DiffBuilder
|
end DiffBuilder
|
||||||
|
|
|
||||||
53
core/src/scalive/Fingerprint.scala
Normal file
53
core/src/scalive/Fingerprint.scala
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
final case class Fingerprint(value: Long, nested: Seq[Fingerprint])
|
||||||
|
|
||||||
|
object Fingerprint:
|
||||||
|
val empty = Fingerprint(0, Seq.empty)
|
||||||
|
def apply(rendered: Rendered): Fingerprint =
|
||||||
|
Fingerprint(
|
||||||
|
rendered.fingerprint,
|
||||||
|
rendered.dynamic.flatMap(render => apply(render(false)))
|
||||||
|
)
|
||||||
|
def apply(comp: Comprehension): Fingerprint = Fingerprint(comp.fingerprint, Seq.empty)
|
||||||
|
def apply(dyn: RenderedDyn): Option[Fingerprint] =
|
||||||
|
dyn match
|
||||||
|
case Some(r: Rendered) => Some(apply(r))
|
||||||
|
case Some(c: Comprehension) => Some(apply(c))
|
||||||
|
case Some(_: String) => None
|
||||||
|
case None => None
|
||||||
|
|
||||||
|
val dynText = digest("text")
|
||||||
|
val dynAttr = digest("attr")
|
||||||
|
val dynAttrValueAsPresence = digest("attrValueAsPresence")
|
||||||
|
val dynWhen = digest("when")
|
||||||
|
val dynSplit = digest("split")
|
||||||
|
|
||||||
|
def digest(s: String): Array[Byte] =
|
||||||
|
MessageDigest.getInstance("MD5").digest(s.getBytes)
|
||||||
|
|
||||||
|
def digest(el: HtmlElement): Array[Byte] =
|
||||||
|
val md = MessageDigest.getInstance("MD5")
|
||||||
|
el.static.foreach(s => md.update(s.getBytes))
|
||||||
|
el.mods.foreach {
|
||||||
|
case Mod.Text(_) => ()
|
||||||
|
case Mod.StaticAttr(_, _) => ()
|
||||||
|
case Mod.StaticAttrValueAsPresence(_, _) => ()
|
||||||
|
case Mod.DynAttr(_, _) => md.update(Fingerprint.dynAttr)
|
||||||
|
case Mod.DynAttrValueAsPresence(_, _) => md.update(Fingerprint.dynAttrValueAsPresence)
|
||||||
|
case Mod.DynText(_) => md.update(Fingerprint.dynText)
|
||||||
|
case Mod.When(_, _) => md.update(Fingerprint.dynWhen)
|
||||||
|
case Mod.Split(_, project) => md.update(digest(project(Dyn.dummy)))
|
||||||
|
case Mod.Tag(el) => Right(digest(el))
|
||||||
|
}
|
||||||
|
md.digest()
|
||||||
|
|
||||||
|
def apply(el: HtmlElement): Long = digestToFingerprint(digest(el))
|
||||||
|
|
||||||
|
private def digestToFingerprint(digest: Array[Byte]): Long =
|
||||||
|
ByteBuffer.wrap(digest, 0, 8).getLong
|
||||||
|
|
||||||
|
end Fingerprint
|
||||||
|
|
@ -4,27 +4,25 @@ import java.io.StringWriter
|
||||||
|
|
||||||
object HtmlBuilder:
|
object HtmlBuilder:
|
||||||
|
|
||||||
def build(lv: LiveView[?]): String =
|
def build(rendered: Rendered, isRoot: Boolean = false): String =
|
||||||
val strw = new StringWriter()
|
val strw = new StringWriter()
|
||||||
build(lv.static, lv.dynamic, strw)
|
if isRoot then strw.append("<!doctype html>")
|
||||||
|
build(rendered.static, rendered.dynamic, strw)
|
||||||
strw.toString()
|
strw.toString()
|
||||||
|
|
||||||
private def build(
|
private def build(
|
||||||
static: Seq[String],
|
static: Seq[String],
|
||||||
dynamic: Seq[LiveDyn[?]],
|
dynamic: Seq[Boolean => RenderedDyn],
|
||||||
strw: StringWriter
|
strw: StringWriter
|
||||||
): Unit =
|
): Unit =
|
||||||
for i <- dynamic.indices do
|
for i <- dynamic.indices do
|
||||||
strw.append(static(i))
|
strw.append(static(i))
|
||||||
dynamic(i) match
|
dynamic(i)(false).foreach {
|
||||||
case mod: LiveDyn.Value[?, ?] =>
|
case s: String => strw.append(s)
|
||||||
strw.append(mod.currentValue.toString)
|
case r: Rendered => build(r)
|
||||||
case mod: LiveDyn.When[?] => build(mod, strw)
|
case c: Comprehension => build(c, strw)
|
||||||
case mod: LiveDyn.Split[?, ?] => build(mod, strw)
|
}
|
||||||
strw.append(static.last)
|
strw.append(static.last)
|
||||||
|
|
||||||
private def build(mod: LiveDyn.When[?], strw: StringWriter): Unit =
|
private def build(comp: Comprehension, strw: StringWriter): Unit =
|
||||||
if mod.displayed then build(mod.nested.static, mod.nested.dynamic, strw)
|
comp.entries.foreach(entry => build(comp.static, entry(false).map(d => _ => d), strw))
|
||||||
|
|
||||||
private def build(mod: LiveDyn.Split[?, ?], strw: StringWriter): Unit =
|
|
||||||
mod.dynamic.foreach(entry => build(mod.static, entry, strw))
|
|
||||||
|
|
|
||||||
68
core/src/scalive/HtmlElement.scala
Normal file
68
core/src/scalive/HtmlElement.scala
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
import scalive.codecs.BooleanAsAttrPresenceCodec
|
||||||
|
import scalive.codecs.Codec
|
||||||
|
|
||||||
|
class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
|
||||||
|
lazy val static = Rendered.buildStatic(this)
|
||||||
|
|
||||||
|
class HtmlTag(val name: String, val void: Boolean = false):
|
||||||
|
def apply(mods: Mod*): HtmlElement = HtmlElement(this, mods.toVector)
|
||||||
|
|
||||||
|
class HtmlAttr[V](val name: String, val codec: Codec[V, String]):
|
||||||
|
private inline def isBooleanAsAttrPresence = codec == BooleanAsAttrPresenceCodec
|
||||||
|
|
||||||
|
def :=(value: V): Mod =
|
||||||
|
if isBooleanAsAttrPresence then
|
||||||
|
Mod.StaticAttrValueAsPresence(
|
||||||
|
this.asInstanceOf[HtmlAttr[Boolean]],
|
||||||
|
value.asInstanceOf[Boolean]
|
||||||
|
)
|
||||||
|
else Mod.StaticAttr(this, codec.encode(value))
|
||||||
|
|
||||||
|
def :=(value: Dyn[V]): Mod =
|
||||||
|
if isBooleanAsAttrPresence then
|
||||||
|
Mod.DynAttrValueAsPresence(
|
||||||
|
this.asInstanceOf[HtmlAttr[Boolean]],
|
||||||
|
value.asInstanceOf[Dyn[Boolean]]
|
||||||
|
)
|
||||||
|
else Mod.DynAttr(this, value)
|
||||||
|
|
||||||
|
enum Mod:
|
||||||
|
case Text(text: String)
|
||||||
|
case StaticAttr(attr: HtmlAttr[?], value: String)
|
||||||
|
case StaticAttrValueAsPresence(attr: HtmlAttr[Boolean], value: Boolean)
|
||||||
|
case DynAttr[T](attr: HtmlAttr[T], value: Dyn[T])
|
||||||
|
case DynAttrValueAsPresence(attr: HtmlAttr[Boolean], value: Dyn[Boolean])
|
||||||
|
case Tag(el: HtmlElement)
|
||||||
|
case DynText(dyn: Dyn[String])
|
||||||
|
case When(dyn: Dyn[Boolean], el: HtmlElement)
|
||||||
|
case Split[T](
|
||||||
|
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)
|
||||||
|
if !trackUpdates | entry.changed then Some(f(entry.value))
|
||||||
|
else None
|
||||||
|
|
||||||
|
def map[T2](f2: T => T2): Dyn[T2] = Dyn(key, f.andThen(f2))
|
||||||
|
|
||||||
|
inline def apply[T2](f2: T => T2): Dyn[T2] = map(f2)
|
||||||
|
|
||||||
|
def when(f2: T => Boolean)(el: HtmlElement): Mod.When = Mod.When(map(f2), el)
|
||||||
|
|
||||||
|
inline def whenNot(f2: T => Boolean)(el: HtmlElement): Mod.When =
|
||||||
|
when(f2.andThen(!_))(el)
|
||||||
|
|
||||||
|
extension [T](dyn: Dyn[List[T]])
|
||||||
|
def splitByIndex(project: Dyn[T] => HtmlElement): Mod.Split[T] =
|
||||||
|
Mod.Split(dyn, project)
|
||||||
|
|
||||||
|
object Dyn:
|
||||||
|
def dummy[T] = Dyn(LiveState.Key[T], identity)
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
package scalive
|
|
||||||
|
|
||||||
import scala.collection.immutable.ArraySeq
|
|
||||||
import scala.collection.mutable.ArrayBuffer
|
|
||||||
|
|
||||||
sealed trait LiveDyn[Model]:
|
|
||||||
def update(model: Model): Unit
|
|
||||||
def wasUpdated: Boolean
|
|
||||||
|
|
||||||
object LiveDyn:
|
|
||||||
|
|
||||||
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
|
|
||||||
def currentValue: O = value
|
|
||||||
def update(v: I): Unit =
|
|
||||||
val newValue = d.run(v)
|
|
||||||
if value == newValue then updated = false
|
|
||||||
else
|
|
||||||
value = newValue
|
|
||||||
updated = true
|
|
||||||
|
|
||||||
class When[Model](
|
|
||||||
dynCond: Dyn[Model, Boolean],
|
|
||||||
el: HtmlElement[Model],
|
|
||||||
init: Model)
|
|
||||||
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 =
|
|
||||||
cond.update(model)
|
|
||||||
nested.update(model)
|
|
||||||
|
|
||||||
class Split[Model, Item](
|
|
||||||
dynList: Dyn[Model, List[Item]],
|
|
||||||
project: Dyn[Item, Item] => HtmlElement[Item],
|
|
||||||
init: Model)
|
|
||||||
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 =
|
|
||||||
removedIndexes.nonEmpty || dynamic.exists(_.exists(_.wasUpdated))
|
|
||||||
|
|
||||||
def update(model: Model): Unit =
|
|
||||||
val items = dynList.run(model)
|
|
||||||
removedIndexes =
|
|
||||||
if items.size < dynamic.size then items.size until dynamic.size
|
|
||||||
else Seq.empty
|
|
||||||
dynamic.takeInPlace(items.size)
|
|
||||||
items.zipWithIndex.map((item, i) =>
|
|
||||||
if i >= dynamic.size then
|
|
||||||
dynamic.append(
|
|
||||||
LiveView.buildDynamic(el, item, startsUpdated = true).to(ArraySeq)
|
|
||||||
)
|
|
||||||
else dynamic(i).foreach(_.update(item))
|
|
||||||
)
|
|
||||||
end Split
|
|
||||||
end LiveDyn
|
|
||||||
31
core/src/scalive/LiveState.scala
Normal file
31
core/src/scalive/LiveState.scala
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
final case class LiveState private (val data: Map[LiveState.Key, LiveState.Entry[Any]]):
|
||||||
|
def get(k: LiveState.Key): Option[LiveState.Entry[k.Type]] =
|
||||||
|
data.get(k).asInstanceOf[Option[LiveState.Entry[k.Type]]]
|
||||||
|
def set(k: LiveState.Key, v: k.Type): LiveState =
|
||||||
|
copy(data = data.updated(k, LiveState.Entry(true, v)))
|
||||||
|
def apply(k: LiveState.Key): LiveState.Entry[k.Type] = get(k).get
|
||||||
|
def update(k: LiveState.Key, update: k.Type => k.Type): LiveState =
|
||||||
|
copy(data =
|
||||||
|
data.updatedWith(k)(
|
||||||
|
_.asInstanceOf[Option[LiveState.Entry[k.Type]]]
|
||||||
|
.map(e => LiveState.Entry(true, update(e.value)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def remove(k: LiveState.Key): LiveState = copy(data = data - k)
|
||||||
|
def setAllUnchanged: LiveState =
|
||||||
|
LiveState(data.view.mapValues(_.copy(changed = false)).toMap)
|
||||||
|
|
||||||
|
object LiveState:
|
||||||
|
final case class Entry[T](changed: Boolean, value: T)
|
||||||
|
|
||||||
|
val empty = LiveState(Map.empty)
|
||||||
|
|
||||||
|
class Key:
|
||||||
|
type Type
|
||||||
|
def id: Dyn[Type] = Dyn(this, identity)
|
||||||
|
def apply[T](f: Type => T): Dyn[T] = Dyn(this, f)
|
||||||
|
object Key:
|
||||||
|
def apply[T] = new Key:
|
||||||
|
type Type = T
|
||||||
|
|
@ -1,114 +1,4 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
import scala.annotation.nowarn
|
trait LiveView:
|
||||||
import scala.collection.immutable.ArraySeq
|
def render: HtmlElement
|
||||||
import scala.collection.mutable.ListBuffer
|
|
||||||
import scalive.codecs.BooleanAsAttrPresenceCodec
|
|
||||||
|
|
||||||
class LiveView[Model] private (
|
|
||||||
val static: ArraySeq[String],
|
|
||||||
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 =
|
|
||||||
dynamic.foreach(_.update(model))
|
|
||||||
|
|
||||||
def wasUpdated: Boolean = dynamic.exists(_.wasUpdated)
|
|
||||||
|
|
||||||
def fullDiff: Diff =
|
|
||||||
DiffBuilder.build(static, dynamic, includeUnchanged = true)
|
|
||||||
|
|
||||||
def diff: Diff =
|
|
||||||
DiffBuilder.build(static = Seq.empty, dynamic)
|
|
||||||
|
|
||||||
object LiveView:
|
|
||||||
|
|
||||||
inline def apply[Model](
|
|
||||||
lv: View[Model],
|
|
||||||
model: Model
|
|
||||||
): LiveView[Model] =
|
|
||||||
render(lv.root, model)
|
|
||||||
|
|
||||||
def render[Model](
|
|
||||||
tag: HtmlElement[Model],
|
|
||||||
model: Model
|
|
||||||
): LiveView[Model] =
|
|
||||||
new LiveView(buildStatic(tag), buildDynamic(tag, model).to(ArraySeq))
|
|
||||||
|
|
||||||
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"<${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 += (if el.tag.void then "/>" else ">")
|
|
||||||
for child <- children do
|
|
||||||
child match
|
|
||||||
case Some(s) =>
|
|
||||||
staticFragment += s
|
|
||||||
case None =>
|
|
||||||
static.append(Some(staticFragment))
|
|
||||||
static.append(None)
|
|
||||||
staticFragment = ""
|
|
||||||
staticFragment += (if el.tag.void then "" else s"</${el.tag.name}>")
|
|
||||||
static.append(Some(staticFragment))
|
|
||||||
static.toSeq
|
|
||||||
|
|
||||||
@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.StaticAttrValueAsPresence(attr, true) => Left(List(Some(s" ${attr.name}")))
|
|
||||||
case Mod.StaticAttrValueAsPresence(attr, false) => Left(List.empty)
|
|
||||||
case Mod.DynAttr(attr, _) =>
|
|
||||||
Left(List(Some(s""" ${attr.name}=""""), None, Some('"'.toString)))
|
|
||||||
case Mod.DynAttrValueAsPresence(attr, _) =>
|
|
||||||
Left(List(Some(""), None, Some("")))
|
|
||||||
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](
|
|
||||||
el: HtmlElement[Model],
|
|
||||||
model: Model,
|
|
||||||
startsUpdated: Boolean = false
|
|
||||||
): Seq[LiveDyn[Model]] =
|
|
||||||
val (attrs, children) = el.mods.partitionMap {
|
|
||||||
case Mod.Text(_) => Right(List.empty)
|
|
||||||
case Mod.StaticAttr(_, _) => Left(List.empty)
|
|
||||||
case Mod.StaticAttrValueAsPresence(_, _) => Left(List.empty)
|
|
||||||
case Mod.DynAttr(attr, value) =>
|
|
||||||
Right(List(LiveDyn.Value(value(attr.codec.encode), model, startsUpdated)))
|
|
||||||
case Mod.DynAttrValueAsPresence(attr, value) =>
|
|
||||||
Right(List(LiveDyn.Value(value(if _ then s" ${attr.name}" else ""), model, startsUpdated)))
|
|
||||||
case Mod.Tag(el) =>
|
|
||||||
Right(buildDynamic(el, model, startsUpdated))
|
|
||||||
case Mod.DynText[Model](dynText) =>
|
|
||||||
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) =>
|
|
||||||
Right(List(LiveDyn.Split(dynList, project, model)))
|
|
||||||
}
|
|
||||||
attrs.flatten ++ children.flatten
|
|
||||||
end LiveView
|
|
||||||
|
|
|
||||||
122
core/src/scalive/Rendered.scala
Normal file
122
core/src/scalive/Rendered.scala
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
import scala.annotation.nowarn
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
|
type RenderedDyn = Option[String | Rendered | Comprehension]
|
||||||
|
|
||||||
|
final case class Rendered(
|
||||||
|
// val root: Boolean,
|
||||||
|
val fingerprint: Long,
|
||||||
|
val static: Seq[String],
|
||||||
|
val dynamic: Seq[Boolean => RenderedDyn])
|
||||||
|
|
||||||
|
final case class Comprehension(
|
||||||
|
// val hasKey: Boolean,
|
||||||
|
val fingerprint: Long,
|
||||||
|
val static: Seq[String],
|
||||||
|
val entries: Seq[Boolean => Seq[RenderedDyn]])
|
||||||
|
|
||||||
|
object Rendered:
|
||||||
|
|
||||||
|
def render(el: HtmlElement, state: LiveState): Rendered =
|
||||||
|
Rendered(Fingerprint.apply(el), el.static, buildDynamicRendered(el, state))
|
||||||
|
|
||||||
|
def render[T](mod: Mod.DynAttr[T], state: LiveState): Boolean => RenderedDyn =
|
||||||
|
trackUpdates => mod.value.render(state, trackUpdates).map(mod.attr.codec.encode)
|
||||||
|
def render(mod: Mod.DynAttrValueAsPresence, state: LiveState): Boolean => RenderedDyn =
|
||||||
|
trackUpdates =>
|
||||||
|
mod.value.render(state, trackUpdates).map(if _ then s" ${mod.attr.name}" else "")
|
||||||
|
def render(mod: Mod.DynText, state: LiveState): Boolean => RenderedDyn =
|
||||||
|
trackUpdates => mod.dyn.render(state, trackUpdates)
|
||||||
|
def render(mod: Mod.When, state: LiveState): Boolean => RenderedDyn =
|
||||||
|
trackUpdates =>
|
||||||
|
mod.dyn
|
||||||
|
.render(state, trackUpdates)
|
||||||
|
.collect { case true => render(mod.el, state) }
|
||||||
|
def render(mod: Mod.Split[Any], state: LiveState): Boolean => RenderedDyn =
|
||||||
|
trackUpdates =>
|
||||||
|
mod.dynList
|
||||||
|
.render(state, trackUpdates)
|
||||||
|
.collect {
|
||||||
|
case items if items.nonEmpty =>
|
||||||
|
val el = mod.project(Dyn.dummy)
|
||||||
|
Comprehension(
|
||||||
|
Fingerprint.apply(el),
|
||||||
|
el.static,
|
||||||
|
items.map(item =>
|
||||||
|
val localKey = LiveState.Key[Any]
|
||||||
|
val localState = LiveState.empty.set(localKey, item)
|
||||||
|
val localElem = mod.project(localKey.id)
|
||||||
|
trackElemUpdates =>
|
||||||
|
buildDynamicRendered(localElem, localState).map(_(trackElemUpdates))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def buildStatic(el: HtmlElement): ArraySeq[String] =
|
||||||
|
buildStaticFragments(el).flatten.to(ArraySeq)
|
||||||
|
|
||||||
|
private def buildStaticFragments(el: HtmlElement): Seq[Option[String]] =
|
||||||
|
val (attrs, children) = buildStaticFragmentsByType(el)
|
||||||
|
val static = ListBuffer.empty[Option[String]]
|
||||||
|
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 += (if el.tag.void then "/>" else ">")
|
||||||
|
for child <- children do
|
||||||
|
child match
|
||||||
|
case Some(s) =>
|
||||||
|
staticFragment += s
|
||||||
|
case None =>
|
||||||
|
static.append(Some(staticFragment))
|
||||||
|
static.append(None)
|
||||||
|
staticFragment = ""
|
||||||
|
staticFragment += (if el.tag.void then "" else s"</${el.tag.name}>")
|
||||||
|
static.append(Some(staticFragment))
|
||||||
|
static.toSeq
|
||||||
|
|
||||||
|
@nowarn("cat=unchecked")
|
||||||
|
private def buildStaticFragmentsByType(el: HtmlElement)
|
||||||
|
: (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.StaticAttrValueAsPresence(attr, true) => Left(List(Some(s" ${attr.name}")))
|
||||||
|
case Mod.StaticAttrValueAsPresence(attr, false) => Left(List.empty)
|
||||||
|
case Mod.DynAttr(attr, _) =>
|
||||||
|
Left(List(Some(s""" ${attr.name}=""""), None, Some('"'.toString)))
|
||||||
|
case Mod.DynAttrValueAsPresence(attr, _) =>
|
||||||
|
Left(List(Some(""), None, Some("")))
|
||||||
|
case Mod.Tag(el) => Right(buildStaticFragments(el))
|
||||||
|
case Mod.Text(text) => Right(List(Some(text)))
|
||||||
|
case Mod.DynText(_) => Right(List(None))
|
||||||
|
case Mod.When(_, _) => Right(List(None))
|
||||||
|
case Mod.Split[Any](_, _) => Right(List(None))
|
||||||
|
}
|
||||||
|
(attrs.flatten, children.flatten)
|
||||||
|
|
||||||
|
@nowarn("cat=unchecked")
|
||||||
|
def buildDynamicRendered(
|
||||||
|
el: HtmlElement,
|
||||||
|
state: LiveState
|
||||||
|
): Seq[Boolean => RenderedDyn] =
|
||||||
|
val (attrs, children) = el.mods.partitionMap {
|
||||||
|
case Mod.Text(_) => Right(List.empty)
|
||||||
|
case Mod.StaticAttr(_, _) => Left(List.empty)
|
||||||
|
case Mod.StaticAttrValueAsPresence(_, _) => Left(List.empty)
|
||||||
|
case mod: Mod.DynAttr[?] => Right(List(Rendered.render(mod, state)))
|
||||||
|
case mod: Mod.DynAttrValueAsPresence => Right(List(Rendered.render(mod, state)))
|
||||||
|
case Mod.Tag(el) => Right(buildDynamicRendered(el, state))
|
||||||
|
case mod: Mod.DynText => Right(List(Rendered.render(mod, state)))
|
||||||
|
case mod: Mod.When => Right(List(Rendered.render(mod, state)))
|
||||||
|
case mod: Mod.Split[Any] => Right(List(Rendered.render(mod, state)))
|
||||||
|
}
|
||||||
|
attrs.flatten ++ children.flatten
|
||||||
|
end Rendered
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
import scalive.defs.tags.HtmlTags
|
|
||||||
import scalive.defs.attrs.HtmlAttrs
|
import scalive.defs.attrs.HtmlAttrs
|
||||||
import scalive.defs.complex.ComplexHtmlKeys
|
import scalive.defs.complex.ComplexHtmlKeys
|
||||||
|
import scalive.defs.tags.HtmlTags
|
||||||
|
|
||||||
object Scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys
|
object Scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys
|
||||||
|
|
||||||
|
|
|
||||||
19
core/src/scalive/Socket.scala
Normal file
19
core/src/scalive/Socket.scala
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
import zio.json.*
|
||||||
|
|
||||||
|
final case class Socket(view: LiveView, initState: LiveState):
|
||||||
|
private var state: LiveState = initState
|
||||||
|
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 syncClient: Unit =
|
||||||
|
val r = Rendered.render(view.render, state)
|
||||||
|
println(DiffBuilder.build(r, fingerprint).toJsonPretty)
|
||||||
|
fingerprint = Fingerprint(r)
|
||||||
|
println(fingerprint)
|
||||||
|
state = state.setAllUnchanged
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package scalive
|
|
||||||
|
|
||||||
import scalive.codecs.Codec
|
|
||||||
import scalive.codecs.BooleanAsAttrPresenceCodec
|
|
||||||
|
|
||||||
trait View[Model]:
|
|
||||||
val model: Dyn[Model, Model] = Dyn.id
|
|
||||||
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)(el: HtmlElement[I]): Mod.When[I] =
|
|
||||||
Mod.When(d.andThen(f), el)
|
|
||||||
|
|
||||||
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] => HtmlElement[O2]
|
|
||||||
): Mod.Split[I, O2] =
|
|
||||||
Mod.Split(d.andThen(f), project)
|
|
||||||
|
|
||||||
def run(v: I): O = d(v)
|
|
||||||
|
|
||||||
object Dyn:
|
|
||||||
def id[T]: Dyn[T, T] = identity
|
|
||||||
|
|
||||||
enum Mod[T]:
|
|
||||||
case StaticAttr(attr: HtmlAttr[?], value: String)
|
|
||||||
case StaticAttrValueAsPresence(attr: HtmlAttr[?], value: Boolean)
|
|
||||||
case DynAttr[T, V](attr: HtmlAttr[V], value: Dyn[T, V]) extends Mod[T]
|
|
||||||
case DynAttrValueAsPresence[T, V](attr: HtmlAttr[V], value: Dyn[T, Boolean]) extends Mod[T]
|
|
||||||
case Tag(el: HtmlElement[T])
|
|
||||||
case Text(text: String)
|
|
||||||
case DynText(dynText: Dyn[T, String])
|
|
||||||
case When(dynCond: Dyn[T, Boolean], el: HtmlElement[T])
|
|
||||||
case Split[T, O](
|
|
||||||
dynList: Dyn[T, List[O]],
|
|
||||||
project: Dyn[O, O] => HtmlElement[O]) extends Mod[T]
|
|
||||||
|
|
||||||
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(_)
|
|
||||||
|
|
||||||
class HtmlTag(val name: String, val void: Boolean = false):
|
|
||||||
def apply[Model](mods: Mod[Model]*): HtmlElement[Model] =
|
|
||||||
HtmlElement(this, mods.toVector)
|
|
||||||
|
|
||||||
class HtmlElement[Model](val tag: HtmlTag, val mods: Vector[Mod[Model]])
|
|
||||||
|
|
||||||
class HtmlAttr[V](val name: String, val codec: Codec[V, String]):
|
|
||||||
val isBooleanAsAttrPresence = codec == BooleanAsAttrPresenceCodec
|
|
||||||
def :=[T](value: V): Mod[T] =
|
|
||||||
val stringValue = codec.encode(value)
|
|
||||||
if isBooleanAsAttrPresence then
|
|
||||||
Mod.StaticAttrValueAsPresence(this, BooleanAsAttrPresenceCodec.decode(stringValue))
|
|
||||||
else Mod.StaticAttr(this, stringValue)
|
|
||||||
def :=[T](value: Dyn[T, V]): Mod[T] =
|
|
||||||
val stringDyn = value(codec.encode)
|
|
||||||
if isBooleanAsAttrPresence then
|
|
||||||
Mod.DynAttrValueAsPresence(this, stringDyn(BooleanAsAttrPresenceCodec.decode))
|
|
||||||
else Mod.DynAttr(this, value)
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
import zio.json.*
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
def main =
|
def main =
|
||||||
val lv =
|
val s = Socket(
|
||||||
LiveView(
|
|
||||||
TestView,
|
TestView,
|
||||||
|
LiveState.empty.set(
|
||||||
|
TestView.model,
|
||||||
MyModel(
|
MyModel(
|
||||||
List(
|
List(
|
||||||
NestedModel("a", 10),
|
NestedModel("a", 10),
|
||||||
|
|
@ -15,11 +14,16 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
println(lv.fullDiff.toJsonPretty)
|
)
|
||||||
println(HtmlBuilder.build(lv))
|
println("Init")
|
||||||
|
println(s.renderHtml)
|
||||||
|
s.syncClient
|
||||||
|
s.syncClient
|
||||||
|
|
||||||
println("Edit first and last")
|
println("Edit first and last")
|
||||||
lv.update(
|
s.updateState(
|
||||||
|
_.set(
|
||||||
|
TestView.model,
|
||||||
MyModel(
|
MyModel(
|
||||||
List(
|
List(
|
||||||
NestedModel("x", 10),
|
NestedModel("x", 10),
|
||||||
|
|
@ -28,56 +32,86 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
)
|
s.syncClient
|
||||||
)
|
// val lv =
|
||||||
println(lv.diff.toJsonPretty)
|
// LiveView(
|
||||||
println(HtmlBuilder.build(lv))
|
// TestView,
|
||||||
|
// LiveState.empty.set(
|
||||||
println("Remove first")
|
// TestView.model,
|
||||||
lv.update(
|
// MyModel(
|
||||||
MyModel(
|
// List(
|
||||||
List(
|
// NestedModel("a", 10),
|
||||||
NestedModel("b", 15),
|
// NestedModel("b", 15),
|
||||||
NestedModel("c", 99),
|
// NestedModel("c", 20)
|
||||||
NestedModel("d", 35)
|
// )
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
println(lv.diff.toJsonPretty)
|
// println(lv.fullDiff.toJsonPretty)
|
||||||
println(HtmlBuilder.build(lv))
|
// println(HtmlBuilder.build(lv))
|
||||||
|
//
|
||||||
println("Remove all")
|
// println("Edit first and last")
|
||||||
lv.update(
|
// lv.update(
|
||||||
MyModel(List.empty, "text-lg", bool = false)
|
// MyModel(
|
||||||
)
|
// List(
|
||||||
println(lv.diff.toJsonPretty)
|
// NestedModel("x", 10),
|
||||||
println(HtmlBuilder.build(lv))
|
// 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
|
end main
|
||||||
|
|
||||||
final case class MyModel(elems: List[NestedModel], cls: String = "text-xs", bool: Boolean = true)
|
final case class MyModel(elems: List[NestedModel], cls: String = "text-xs", bool: Boolean = true)
|
||||||
final case class NestedModel(name: String, age: Int)
|
final case class NestedModel(name: String, age: Int)
|
||||||
|
|
||||||
object TestView extends View[MyModel]:
|
object TestView extends LiveView:
|
||||||
val root: HtmlElement[MyModel] =
|
val model = LiveState.Key[MyModel]
|
||||||
|
val render =
|
||||||
div(
|
div(
|
||||||
idAttr := "42",
|
idAttr := "42",
|
||||||
cls := model(_.cls),
|
cls := model(_.cls),
|
||||||
draggable := model(_.bool),
|
draggable := model(_.bool),
|
||||||
disabled := model(_.bool),
|
disabled := model(_.bool),
|
||||||
ul(
|
ul(
|
||||||
model.splitByIndex(_.elems)(elem =>
|
model(_.elems).splitByIndex(elem =>
|
||||||
li(
|
li(
|
||||||
"Nom: ",
|
"Nom: ",
|
||||||
elem(_.name),
|
elem(_.name),
|
||||||
|
|
|
||||||
|
|
@ -14,316 +14,320 @@ object LiveViewSpec extends TestSuite:
|
||||||
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)
|
||||||
|
|
||||||
def assertEqualsJson(actual: Diff, expected: Json) =
|
// def assertEqualsJson(actual: Diff, expected: Json) =
|
||||||
assert(actual.toJsonPretty == expected.toJsonPretty)
|
// assert(actual.toJsonPretty == expected.toJsonPretty)
|
||||||
|
|
||||||
val emptyDiff = Json.Obj.empty
|
val emptyDiff = Json.Obj.empty
|
||||||
|
|
||||||
val tests = Tests {
|
val tests = Tests {
|
||||||
|
|
||||||
test("Static only") {
|
// test("Static only") {
|
||||||
val lv =
|
// val lv =
|
||||||
LiveView(
|
// LiveView(
|
||||||
new View[Unit]:
|
// new View:
|
||||||
val root: HtmlElement[Unit] =
|
// type Model = Unit
|
||||||
div("Static string")
|
// val render = div("Static string")
|
||||||
,
|
// ,
|
||||||
()
|
// ()
|
||||||
)
|
// )
|
||||||
test("init") {
|
// test("init") {
|
||||||
assertEqualsJson(
|
// assertEqualsJson(
|
||||||
lv.fullDiff,
|
// lv.fullDiff,
|
||||||
Json.Obj(
|
// Json.Obj(
|
||||||
"s" -> Json.Arr(Json.Str("<div>Static string</div>"))
|
// "s" -> Json.Arr(Json.Str("<div>Static string</div>"))
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
test("diff") {
|
// test("diff") {
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
test("Dynamic string") {
|
// test("Dynamic string") {
|
||||||
val lv =
|
// val lv =
|
||||||
LiveView(
|
// LiveView(
|
||||||
new View[TestModel]:
|
// new View:
|
||||||
val root: HtmlElement[TestModel] =
|
// type Model = TestModel
|
||||||
div(model(_.title))
|
// val render =
|
||||||
,
|
// div(model(_.title))
|
||||||
TestModel()
|
// ,
|
||||||
)
|
// TestModel()
|
||||||
test("init") {
|
// )
|
||||||
assertEqualsJson(
|
// test("init") {
|
||||||
lv.fullDiff,
|
// assertEqualsJson(
|
||||||
Json
|
// lv.fullDiff,
|
||||||
.Obj(
|
// Json
|
||||||
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
// .Obj(
|
||||||
"0" -> Json.Str("title value")
|
// "s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
||||||
)
|
// "0" -> Json.Str("title value")
|
||||||
)
|
// )
|
||||||
}
|
// )
|
||||||
test("diff no update") {
|
// }
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// test("diff no update") {
|
||||||
}
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
test("diff with update") {
|
// }
|
||||||
lv.update(TestModel(title = "title updated"))
|
// test("diff with update") {
|
||||||
assertEqualsJson(
|
// lv.update(TestModel(title = "title updated"))
|
||||||
lv.diff,
|
// assertEqualsJson(
|
||||||
Json.Obj("0" -> Json.Str("title updated"))
|
// lv.diff,
|
||||||
)
|
// Json.Obj("0" -> Json.Str("title updated"))
|
||||||
}
|
// )
|
||||||
test("diff with update and no change") {
|
// }
|
||||||
lv.update(TestModel(title = "title updated"))
|
// test("diff with update and no change") {
|
||||||
lv.update(TestModel(title = "title updated"))
|
// lv.update(TestModel(title = "title updated"))
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// lv.update(TestModel(title = "title updated"))
|
||||||
}
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
test("Dynamic attribute") {
|
//
|
||||||
val lv =
|
// test("Dynamic attribute") {
|
||||||
LiveView(
|
// val lv =
|
||||||
new View[TestModel]:
|
// LiveView(
|
||||||
val root: HtmlElement[TestModel] =
|
// new View:
|
||||||
div(cls := model(_.cls))
|
// type Model = TestModel
|
||||||
,
|
// val render =
|
||||||
TestModel()
|
// div(cls := model(_.cls))
|
||||||
)
|
// ,
|
||||||
test("init") {
|
// TestModel()
|
||||||
assertEqualsJson(
|
// )
|
||||||
lv.fullDiff,
|
// test("init") {
|
||||||
Json
|
// assertEqualsJson(
|
||||||
.Obj(
|
// lv.fullDiff,
|
||||||
"s" -> Json
|
// Json
|
||||||
.Arr(Json.Str("<div class=\""), Json.Str("\"></div>")),
|
// .Obj(
|
||||||
"0" -> Json.Str("text-sm")
|
// "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 no update") {
|
||||||
test("diff with update") {
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
lv.update(TestModel(cls = "text-md"))
|
// }
|
||||||
assertEqualsJson(
|
// test("diff with update") {
|
||||||
lv.diff,
|
// lv.update(TestModel(cls = "text-md"))
|
||||||
Json.Obj("0" -> Json.Str("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"))
|
// test("diff with update and no change") {
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// lv.update(TestModel(cls = "text-md"))
|
||||||
}
|
// lv.update(TestModel(cls = "text-md"))
|
||||||
}
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
|
// }
|
||||||
test("when mod") {
|
// }
|
||||||
val lv =
|
//
|
||||||
LiveView(
|
// test("when mod") {
|
||||||
new View[TestModel]:
|
// val lv =
|
||||||
val root: HtmlElement[TestModel] =
|
// LiveView(
|
||||||
div(
|
// new View:
|
||||||
model.when(_.bool)(
|
// type Model = TestModel
|
||||||
div("static string", model(_.nestedTitle))
|
// val render =
|
||||||
)
|
// div(
|
||||||
)
|
// model.when(_.bool)(
|
||||||
,
|
// div("static string", model(_.nestedTitle))
|
||||||
TestModel()
|
// )
|
||||||
)
|
// )
|
||||||
test("init") {
|
// ,
|
||||||
assertEqualsJson(
|
// TestModel()
|
||||||
lv.fullDiff,
|
// )
|
||||||
Json
|
// test("init") {
|
||||||
.Obj(
|
// assertEqualsJson(
|
||||||
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
// lv.fullDiff,
|
||||||
"0" -> Json.Bool(false)
|
// Json
|
||||||
)
|
// .Obj(
|
||||||
)
|
// "s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
||||||
}
|
// "0" -> Json.Bool(false)
|
||||||
test("diff no update") {
|
// )
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// )
|
||||||
}
|
// }
|
||||||
test("diff with unrelated update") {
|
// test("diff no update") {
|
||||||
lv.update(TestModel(title = "title updated"))
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// }
|
||||||
}
|
// test("diff with unrelated update") {
|
||||||
test("diff when true") {
|
// lv.update(TestModel(title = "title updated"))
|
||||||
lv.update(TestModel(bool = true))
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
assertEqualsJson(
|
// }
|
||||||
lv.diff,
|
// test("diff when true") {
|
||||||
Json.Obj(
|
// lv.update(TestModel(bool = true))
|
||||||
"0" ->
|
// assertEqualsJson(
|
||||||
Json
|
// lv.diff,
|
||||||
.Obj(
|
// Json.Obj(
|
||||||
"s" -> Json
|
// "0" ->
|
||||||
.Arr(Json.Str("<div>static string"), Json.Str("</div>")),
|
// Json
|
||||||
"0" -> Json.Str("nested title value")
|
// .Obj(
|
||||||
)
|
// "s" -> Json
|
||||||
)
|
// .Arr(Json.Str("<div>static string"), Json.Str("</div>")),
|
||||||
)
|
// "0" -> Json.Str("nested title value")
|
||||||
}
|
// )
|
||||||
test("diff when nested change") {
|
// )
|
||||||
lv.update(TestModel(bool = true))
|
// )
|
||||||
lv.update(TestModel(bool = true, nestedTitle = "nested title updated"))
|
// }
|
||||||
assertEqualsJson(
|
// test("diff when nested change") {
|
||||||
lv.diff,
|
// lv.update(TestModel(bool = true))
|
||||||
Json.Obj(
|
// lv.update(TestModel(bool = true, nestedTitle = "nested title updated"))
|
||||||
"0" ->
|
// assertEqualsJson(
|
||||||
Json
|
// lv.diff,
|
||||||
.Obj(
|
// Json.Obj(
|
||||||
"0" -> Json.Str("nested title updated")
|
// "0" ->
|
||||||
)
|
// Json
|
||||||
)
|
// .Obj(
|
||||||
)
|
// "0" -> Json.Str("nested title updated")
|
||||||
}
|
// )
|
||||||
}
|
// )
|
||||||
|
// )
|
||||||
test("splitByIndex mod") {
|
// }
|
||||||
val initModel =
|
// }
|
||||||
TestModel(
|
//
|
||||||
items = List(
|
// test("splitByIndex mod") {
|
||||||
NestedModel("a", 10),
|
// val initModel =
|
||||||
NestedModel("b", 15),
|
// TestModel(
|
||||||
NestedModel("c", 20)
|
// items = List(
|
||||||
)
|
// NestedModel("a", 10),
|
||||||
)
|
// NestedModel("b", 15),
|
||||||
val lv =
|
// NestedModel("c", 20)
|
||||||
LiveView(
|
// )
|
||||||
new View[TestModel]:
|
// )
|
||||||
val root: HtmlElement[TestModel] =
|
// val lv =
|
||||||
div(
|
// LiveView(
|
||||||
ul(
|
// new View:
|
||||||
model.splitByIndex(_.items)(elem =>
|
// type Model = TestModel
|
||||||
li(
|
// val render =
|
||||||
"Nom: ",
|
// div(
|
||||||
elem(_.name),
|
// ul(
|
||||||
" Age: ",
|
// model.splitByIndex(_.items)(elem =>
|
||||||
elem(_.age.toString)
|
// li(
|
||||||
)
|
// "Nom: ",
|
||||||
)
|
// elem(_.name),
|
||||||
)
|
// " Age: ",
|
||||||
)
|
// elem(_.age.toString)
|
||||||
,
|
// )
|
||||||
initModel
|
// )
|
||||||
)
|
// )
|
||||||
test("init") {
|
// )
|
||||||
assertEqualsJson(
|
// ,
|
||||||
lv.fullDiff,
|
// initModel
|
||||||
Json
|
// )
|
||||||
.Obj(
|
// test("init") {
|
||||||
"s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
|
// assertEqualsJson(
|
||||||
"0" -> Json.Obj(
|
// lv.fullDiff,
|
||||||
"s" -> Json.Arr(
|
// Json
|
||||||
Json.Str("<li>Nom: "),
|
// .Obj(
|
||||||
Json.Str(" Age: "),
|
// "s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
|
||||||
Json.Str("</li>")
|
// "0" -> Json.Obj(
|
||||||
),
|
// "s" -> Json.Arr(
|
||||||
"d" -> Json.Obj(
|
// Json.Str("<li>Nom: "),
|
||||||
"0" -> Json.Obj(
|
// Json.Str(" Age: "),
|
||||||
"0" -> Json.Str("a"),
|
// Json.Str("</li>")
|
||||||
"1" -> Json.Str("10")
|
// ),
|
||||||
),
|
// "d" -> Json.Obj(
|
||||||
"1" -> Json.Obj(
|
// "0" -> Json.Obj(
|
||||||
"0" -> Json.Str("b"),
|
// "0" -> Json.Str("a"),
|
||||||
"1" -> Json.Str("15")
|
// "1" -> Json.Str("10")
|
||||||
),
|
// ),
|
||||||
"2" -> Json.Obj(
|
// "1" -> Json.Obj(
|
||||||
"0" -> Json.Str("c"),
|
// "0" -> Json.Str("b"),
|
||||||
"1" -> Json.Str("20")
|
// "1" -> Json.Str("15")
|
||||||
)
|
// ),
|
||||||
)
|
// "2" -> Json.Obj(
|
||||||
)
|
// "0" -> Json.Str("c"),
|
||||||
)
|
// "1" -> Json.Str("20")
|
||||||
)
|
// )
|
||||||
}
|
// )
|
||||||
test("diff no update") {
|
// )
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// )
|
||||||
}
|
// )
|
||||||
test("diff with unrelated update") {
|
// }
|
||||||
lv.update(initModel.copy(title = "title updated"))
|
// test("diff no update") {
|
||||||
assertEqualsJson(lv.diff, emptyDiff)
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
// }
|
||||||
test("diff with item changed") {
|
// test("diff with unrelated update") {
|
||||||
lv.update(
|
// lv.update(initModel.copy(title = "title updated"))
|
||||||
initModel.copy(items = initModel.items.updated(2, NestedModel("c", 99)))
|
// assertEqualsJson(lv.diff, emptyDiff)
|
||||||
)
|
// }
|
||||||
assertEqualsJson(
|
// test("diff with item changed") {
|
||||||
lv.diff,
|
// lv.update(
|
||||||
Json.Obj(
|
// initModel.copy(items = initModel.items.updated(2, NestedModel("c", 99)))
|
||||||
"0" ->
|
// )
|
||||||
Json
|
// assertEqualsJson(
|
||||||
.Obj(
|
// lv.diff,
|
||||||
"d" -> Json.Obj(
|
// Json.Obj(
|
||||||
"2" -> Json.Obj(
|
// "0" ->
|
||||||
"1" -> Json.Str("99")
|
// Json
|
||||||
)
|
// .Obj(
|
||||||
)
|
// "d" -> Json.Obj(
|
||||||
)
|
// "2" -> Json.Obj(
|
||||||
)
|
// "1" -> Json.Str("99")
|
||||||
)
|
// )
|
||||||
}
|
// )
|
||||||
test("diff with item added") {
|
// )
|
||||||
lv.update(
|
// )
|
||||||
initModel.copy(items = initModel.items.appended(NestedModel("d", 35)))
|
// )
|
||||||
)
|
// }
|
||||||
assertEqualsJson(
|
// test("diff with item added") {
|
||||||
lv.diff,
|
// lv.update(
|
||||||
Json.Obj(
|
// initModel.copy(items = initModel.items.appended(NestedModel("d", 35)))
|
||||||
"0" ->
|
// )
|
||||||
Json
|
// assertEqualsJson(
|
||||||
.Obj(
|
// lv.diff,
|
||||||
"d" -> Json.Obj(
|
// Json.Obj(
|
||||||
"3" -> Json.Obj(
|
// "0" ->
|
||||||
"0" -> Json.Str("d"),
|
// Json
|
||||||
"1" -> Json.Str("35")
|
// .Obj(
|
||||||
)
|
// "d" -> Json.Obj(
|
||||||
)
|
// "3" -> Json.Obj(
|
||||||
)
|
// "0" -> Json.Str("d"),
|
||||||
)
|
// "1" -> Json.Str("35")
|
||||||
)
|
// )
|
||||||
}
|
// )
|
||||||
test("diff with first item removed") {
|
// )
|
||||||
lv.update(
|
// )
|
||||||
initModel.copy(items = initModel.items.tail)
|
// )
|
||||||
)
|
// }
|
||||||
assertEqualsJson(
|
// test("diff with first item removed") {
|
||||||
lv.diff,
|
// lv.update(
|
||||||
Json.Obj(
|
// initModel.copy(items = initModel.items.tail)
|
||||||
"0" ->
|
// )
|
||||||
Json
|
// assertEqualsJson(
|
||||||
.Obj(
|
// lv.diff,
|
||||||
"d" -> Json.Obj(
|
// Json.Obj(
|
||||||
"0" -> Json.Obj(
|
// "0" ->
|
||||||
"0" -> Json.Str("b"),
|
// Json
|
||||||
"1" -> Json.Str("15")
|
// .Obj(
|
||||||
),
|
// "d" -> Json.Obj(
|
||||||
"1" -> Json.Obj(
|
// "0" -> Json.Obj(
|
||||||
"0" -> Json.Str("c"),
|
// "0" -> Json.Str("b"),
|
||||||
"1" -> Json.Str("20")
|
// "1" -> Json.Str("15")
|
||||||
),
|
// ),
|
||||||
"2" -> Json.Bool(false)
|
// "1" -> Json.Obj(
|
||||||
)
|
// "0" -> Json.Str("c"),
|
||||||
)
|
// "1" -> Json.Str("20")
|
||||||
)
|
// ),
|
||||||
)
|
// "2" -> Json.Bool(false)
|
||||||
}
|
// )
|
||||||
test("diff all removed") {
|
// )
|
||||||
lv.update(initModel.copy(items = List.empty))
|
// )
|
||||||
assertEqualsJson(
|
// )
|
||||||
lv.diff,
|
// }
|
||||||
Json.Obj(
|
// test("diff all removed") {
|
||||||
"0" ->
|
// lv.update(initModel.copy(items = List.empty))
|
||||||
Json
|
// assertEqualsJson(
|
||||||
.Obj(
|
// lv.diff,
|
||||||
"d" -> Json.Obj(
|
// Json.Obj(
|
||||||
"0" -> Json.Bool(false),
|
// "0" ->
|
||||||
"1" -> Json.Bool(false),
|
// Json
|
||||||
"2" -> Json.Bool(false)
|
// .Obj(
|
||||||
)
|
// "d" -> Json.Obj(
|
||||||
)
|
// "0" -> Json.Bool(false),
|
||||||
)
|
// "1" -> Json.Bool(false),
|
||||||
)
|
// "2" -> Json.Bool(false)
|
||||||
|
// )
|
||||||
}
|
// )
|
||||||
}
|
// )
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
end LiveViewSpec
|
end LiveViewSpec
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ import zio.http.template.Html
|
||||||
|
|
||||||
object Example extends ZIOAppDefault:
|
object Example extends ZIOAppDefault:
|
||||||
|
|
||||||
val lv =
|
val s = Socket(
|
||||||
LiveView(
|
|
||||||
TestView,
|
TestView,
|
||||||
|
LiveState.empty.set(
|
||||||
|
TestView.model,
|
||||||
MyModel(
|
MyModel(
|
||||||
List(
|
List(
|
||||||
NestedModel("a", 10),
|
NestedModel("a", 10),
|
||||||
|
|
@ -20,6 +21,7 @@ object Example extends ZIOAppDefault:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val socketApp: WebSocketApp[Any] =
|
val socketApp: WebSocketApp[Any] =
|
||||||
Handler.webSocket { channel =>
|
Handler.webSocket { channel =>
|
||||||
|
|
@ -66,7 +68,7 @@ object Example extends ZIOAppDefault:
|
||||||
val routes: Routes[Any, Response] =
|
val routes: Routes[Any, Response] =
|
||||||
Routes(
|
Routes(
|
||||||
Method.GET / "" -> handler { (_: Request) =>
|
Method.GET / "" -> handler { (_: Request) =>
|
||||||
Response.html(Html.raw(HtmlBuilder.build(lv)))
|
Response.html(Html.raw(s.renderHtml))
|
||||||
},
|
},
|
||||||
Method.GET / "live" / "ws" -> handler(socketApp.toResponse)
|
Method.GET / "live" / "ws" -> handler(socketApp.toResponse)
|
||||||
)
|
)
|
||||||
|
|
@ -77,13 +79,14 @@ end Example
|
||||||
final case class MyModel(elems: List[NestedModel], cls: String = "text-xs")
|
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 LiveView:
|
||||||
val root: HtmlElement[MyModel] =
|
val model = LiveState.Key[MyModel]
|
||||||
|
val render =
|
||||||
div(
|
div(
|
||||||
idAttr := "42",
|
idAttr := "42",
|
||||||
cls := model(_.cls),
|
cls := model(_.cls),
|
||||||
ul(
|
ul(
|
||||||
model.splitByIndex(_.elems)(elem =>
|
model(_.elems).splitByIndex(elem =>
|
||||||
li(
|
li(
|
||||||
"Nom: ",
|
"Nom: ",
|
||||||
elem(_.name),
|
elem(_.name),
|
||||||
|
|
|
||||||
13
zio/src/scalive/RootLayout.scala
Normal file
13
zio/src/scalive/RootLayout.scala
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
import scalive.HtmlElement
|
||||||
|
|
||||||
|
object RootLayout:
|
||||||
|
def apply[RootModel](content: HtmlElement): HtmlElement =
|
||||||
|
htmlRootTag(
|
||||||
|
lang := "en",
|
||||||
|
metaTag(charset := "utf-8"),
|
||||||
|
bodyTag(
|
||||||
|
// content
|
||||||
|
)
|
||||||
|
)
|
||||||
30
zio/src/scalive/ScaliveZio.scala
Normal file
30
zio/src/scalive/ScaliveZio.scala
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
import zio.http.Response
|
||||||
|
import zio.http.template.Html
|
||||||
|
|
||||||
|
// trait LiveRouter:
|
||||||
|
// type RootModel
|
||||||
|
// private lazy val viewsMap: Map[String, View] = views.map(r => (r.name, r.view)).toMap
|
||||||
|
// def rootLayout: HtmlElement[RootModel]
|
||||||
|
// def views: Seq[LiveRoute]
|
||||||
|
//
|
||||||
|
// final case class LiveRoute(name: String, view: View)
|
||||||
|
|
||||||
|
object ZioLiveApp:
|
||||||
|
// 1 Request to live route
|
||||||
|
// 2 Create live view with stateless token containing user id if connected, http params, live view id
|
||||||
|
// 3 Response with HTML and token
|
||||||
|
// 4 Websocket connection with token
|
||||||
|
// 5 Recreate exact same liveview as before using token data
|
||||||
|
|
||||||
|
// val testRoute = LiveRoute("test", TestView)
|
||||||
|
// val router = new LiveRouter:
|
||||||
|
// val rootLayout = htmlRootTag()
|
||||||
|
// val views = Seq(testRoute)
|
||||||
|
|
||||||
|
// def htmlRender(v: View, model: v.Model) =
|
||||||
|
// val lv = LiveView(v, model)
|
||||||
|
// Response.html(Html.raw(HtmlBuilder.build(lv, isRoot = true)))
|
||||||
|
|
||||||
|
private val socketApp = ???
|
||||||
Loading…
Add table
Add a link
Reference in a new issue