mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 13:36: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
|
|
@ -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
|
||||
|
||||
import zio.Chunk
|
||||
import zio.json.ast.Json
|
||||
|
||||
object DiffBuilder:
|
||||
def build(
|
||||
static: Seq[String],
|
||||
dynamic: Seq[LiveDyn[?]],
|
||||
includeUnchanged: Boolean = false
|
||||
): Diff.Tag =
|
||||
Diff.Tag(
|
||||
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
|
||||
)
|
||||
)
|
||||
def build(rendered: Rendered, fingerprint: Fingerprint): Json =
|
||||
val nestedFingerprintIter = fingerprint.nested.iterator
|
||||
Json.Obj(
|
||||
Option
|
||||
.when(rendered.static.nonEmpty && fingerprint.value != rendered.fingerprint)(
|
||||
"s" -> Json.Arr(rendered.static.map(Json.Str(_))*)
|
||||
)
|
||||
.to(Chunk)
|
||||
.appendedAll(
|
||||
mod.removedIndexes
|
||||
.map(i => Diff.Dynamic(i, Diff.Deleted))
|
||||
rendered.dynamic.zipWithIndex
|
||||
.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
|
||||
|
|
|
|||
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:
|
||||
|
||||
def build(lv: LiveView[?]): String =
|
||||
def build(rendered: Rendered, isRoot: Boolean = false): String =
|
||||
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()
|
||||
|
||||
private def build(
|
||||
static: Seq[String],
|
||||
dynamic: Seq[LiveDyn[?]],
|
||||
dynamic: Seq[Boolean => RenderedDyn],
|
||||
strw: StringWriter
|
||||
): Unit =
|
||||
for i <- dynamic.indices do
|
||||
strw.append(static(i))
|
||||
dynamic(i) match
|
||||
case mod: LiveDyn.Value[?, ?] =>
|
||||
strw.append(mod.currentValue.toString)
|
||||
case mod: LiveDyn.When[?] => build(mod, strw)
|
||||
case mod: LiveDyn.Split[?, ?] => build(mod, strw)
|
||||
dynamic(i)(false).foreach {
|
||||
case s: String => strw.append(s)
|
||||
case r: Rendered => build(r)
|
||||
case c: Comprehension => build(c, strw)
|
||||
}
|
||||
strw.append(static.last)
|
||||
|
||||
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: LiveDyn.Split[?, ?], strw: StringWriter): Unit =
|
||||
mod.dynamic.foreach(entry => build(mod.static, entry, strw))
|
||||
private def build(comp: Comprehension, strw: StringWriter): Unit =
|
||||
comp.entries.foreach(entry => build(comp.static, entry(false).map(d => _ => d), 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
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.collection.immutable.ArraySeq
|
||||
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
|
||||
trait LiveView:
|
||||
def render: HtmlElement
|
||||
|
|
|
|||
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
|
||||
|
||||
import scalive.defs.tags.HtmlTags
|
||||
import scalive.defs.attrs.HtmlAttrs
|
||||
import scalive.defs.complex.ComplexHtmlKeys
|
||||
import scalive.defs.tags.HtmlTags
|
||||
|
||||
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
|
||||
|
||||
import zio.json.*
|
||||
|
||||
@main
|
||||
def main =
|
||||
val lv =
|
||||
LiveView(
|
||||
TestView,
|
||||
val s = Socket(
|
||||
TestView,
|
||||
LiveState.empty.set(
|
||||
TestView.model,
|
||||
MyModel(
|
||||
List(
|
||||
NestedModel("a", 10),
|
||||
|
|
@ -15,69 +14,104 @@ def main =
|
|||
)
|
||||
)
|
||||
)
|
||||
println(lv.fullDiff.toJsonPretty)
|
||||
println(HtmlBuilder.build(lv))
|
||||
)
|
||||
println("Init")
|
||||
println(s.renderHtml)
|
||||
s.syncClient
|
||||
s.syncClient
|
||||
|
||||
println("Edit first and last")
|
||||
lv.update(
|
||||
MyModel(
|
||||
List(
|
||||
NestedModel("x", 10),
|
||||
NestedModel("b", 15),
|
||||
NestedModel("c", 99)
|
||||
s.updateState(
|
||||
_.set(
|
||||
TestView.model,
|
||||
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))
|
||||
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 View[MyModel]:
|
||||
val root: HtmlElement[MyModel] =
|
||||
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.splitByIndex(_.elems)(elem =>
|
||||
model(_.elems).splitByIndex(elem =>
|
||||
li(
|
||||
"Nom: ",
|
||||
elem(_.name),
|
||||
|
|
|
|||
|
|
@ -14,316 +14,320 @@ object LiveViewSpec extends TestSuite:
|
|||
items: List[NestedModel] = List.empty)
|
||||
final case class NestedModel(name: String, age: Int)
|
||||
|
||||
def assertEqualsJson(actual: Diff, expected: Json) =
|
||||
assert(actual.toJsonPretty == expected.toJsonPretty)
|
||||
// def assertEqualsJson(actual: Diff, expected: Json) =
|
||||
// assert(actual.toJsonPretty == expected.toJsonPretty)
|
||||
|
||||
val emptyDiff = Json.Obj.empty
|
||||
|
||||
val tests = Tests {
|
||||
|
||||
test("Static only") {
|
||||
val lv =
|
||||
LiveView(
|
||||
new View[Unit]:
|
||||
val root: HtmlElement[Unit] =
|
||||
div("Static string")
|
||||
,
|
||||
()
|
||||
)
|
||||
test("init") {
|
||||
assertEqualsJson(
|
||||
lv.fullDiff,
|
||||
Json.Obj(
|
||||
"s" -> Json.Arr(Json.Str("<div>Static string</div>"))
|
||||
)
|
||||
)
|
||||
}
|
||||
test("diff") {
|
||||
assertEqualsJson(lv.diff, emptyDiff)
|
||||
}
|
||||
}
|
||||
|
||||
test("Dynamic string") {
|
||||
val lv =
|
||||
LiveView(
|
||||
new View[TestModel]:
|
||||
val root: HtmlElement[TestModel] =
|
||||
div(model(_.title))
|
||||
,
|
||||
TestModel()
|
||||
)
|
||||
test("init") {
|
||||
assertEqualsJson(
|
||||
lv.fullDiff,
|
||||
Json
|
||||
.Obj(
|
||||
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
||||
"0" -> Json.Str("title value")
|
||||
)
|
||||
)
|
||||
}
|
||||
test("diff no update") {
|
||||
assertEqualsJson(lv.diff, emptyDiff)
|
||||
}
|
||||
test("diff with update") {
|
||||
lv.update(TestModel(title = "title updated"))
|
||||
assertEqualsJson(
|
||||
lv.diff,
|
||||
Json.Obj("0" -> Json.Str("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)
|
||||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
val lv =
|
||||
LiveView(
|
||||
new View[TestModel]:
|
||||
val root: HtmlElement[TestModel] =
|
||||
div(
|
||||
model.when(_.bool)(
|
||||
div("static string", model(_.nestedTitle))
|
||||
)
|
||||
)
|
||||
,
|
||||
TestModel()
|
||||
)
|
||||
test("init") {
|
||||
assertEqualsJson(
|
||||
lv.fullDiff,
|
||||
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") {
|
||||
lv.update(TestModel(title = "title updated"))
|
||||
assertEqualsJson(lv.diff, emptyDiff)
|
||||
}
|
||||
test("diff when true") {
|
||||
lv.update(TestModel(bool = true))
|
||||
assertEqualsJson(
|
||||
lv.diff,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
.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(
|
||||
lv.diff,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
.Obj(
|
||||
"0" -> Json.Str("nested title updated")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("splitByIndex mod") {
|
||||
val initModel =
|
||||
TestModel(
|
||||
items = List(
|
||||
NestedModel("a", 10),
|
||||
NestedModel("b", 15),
|
||||
NestedModel("c", 20)
|
||||
)
|
||||
)
|
||||
val lv =
|
||||
LiveView(
|
||||
new View[TestModel]:
|
||||
val root: HtmlElement[TestModel] =
|
||||
div(
|
||||
ul(
|
||||
model.splitByIndex(_.items)(elem =>
|
||||
li(
|
||||
"Nom: ",
|
||||
elem(_.name),
|
||||
" Age: ",
|
||||
elem(_.age.toString)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
,
|
||||
initModel
|
||||
)
|
||||
test("init") {
|
||||
assertEqualsJson(
|
||||
lv.fullDiff,
|
||||
Json
|
||||
.Obj(
|
||||
"s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
|
||||
"0" -> Json.Obj(
|
||||
"s" -> Json.Arr(
|
||||
Json.Str("<li>Nom: "),
|
||||
Json.Str(" Age: "),
|
||||
Json.Str("</li>")
|
||||
),
|
||||
"d" -> Json.Obj(
|
||||
"0" -> Json.Obj(
|
||||
"0" -> Json.Str("a"),
|
||||
"1" -> Json.Str("10")
|
||||
),
|
||||
"1" -> Json.Obj(
|
||||
"0" -> Json.Str("b"),
|
||||
"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"))
|
||||
assertEqualsJson(lv.diff, emptyDiff)
|
||||
}
|
||||
test("diff with item changed") {
|
||||
lv.update(
|
||||
initModel.copy(items = initModel.items.updated(2, NestedModel("c", 99)))
|
||||
)
|
||||
assertEqualsJson(
|
||||
lv.diff,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
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(
|
||||
lv.diff,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
.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(
|
||||
lv.diff,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
.Obj(
|
||||
"d" -> Json.Obj(
|
||||
"0" -> Json.Obj(
|
||||
"0" -> Json.Str("b"),
|
||||
"1" -> Json.Str("15")
|
||||
),
|
||||
"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(
|
||||
"0" ->
|
||||
Json
|
||||
.Obj(
|
||||
"d" -> Json.Obj(
|
||||
"0" -> Json.Bool(false),
|
||||
"1" -> Json.Bool(false),
|
||||
"2" -> Json.Bool(false)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
// test("Static only") {
|
||||
// val lv =
|
||||
// LiveView(
|
||||
// new View:
|
||||
// type Model = Unit
|
||||
// val render = div("Static string")
|
||||
// ,
|
||||
// ()
|
||||
// )
|
||||
// test("init") {
|
||||
// assertEqualsJson(
|
||||
// lv.fullDiff,
|
||||
// Json.Obj(
|
||||
// "s" -> Json.Arr(Json.Str("<div>Static string</div>"))
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// test("diff") {
|
||||
// assertEqualsJson(lv.diff, emptyDiff)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// test("Dynamic string") {
|
||||
// val lv =
|
||||
// LiveView(
|
||||
// new View:
|
||||
// type Model = TestModel
|
||||
// val render =
|
||||
// div(model(_.title))
|
||||
// ,
|
||||
// TestModel()
|
||||
// )
|
||||
// test("init") {
|
||||
// assertEqualsJson(
|
||||
// lv.fullDiff,
|
||||
// Json
|
||||
// .Obj(
|
||||
// "s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
||||
// "0" -> Json.Str("title value")
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// test("diff no update") {
|
||||
// assertEqualsJson(lv.diff, emptyDiff)
|
||||
// }
|
||||
// test("diff with update") {
|
||||
// lv.update(TestModel(title = "title updated"))
|
||||
// assertEqualsJson(
|
||||
// lv.diff,
|
||||
// Json.Obj("0" -> Json.Str("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)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// test("Dynamic attribute") {
|
||||
// val lv =
|
||||
// LiveView(
|
||||
// new View:
|
||||
// type Model = TestModel
|
||||
// val render =
|
||||
// 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") {
|
||||
// val lv =
|
||||
// LiveView(
|
||||
// new View:
|
||||
// type Model = TestModel
|
||||
// val render =
|
||||
// div(
|
||||
// model.when(_.bool)(
|
||||
// div("static string", model(_.nestedTitle))
|
||||
// )
|
||||
// )
|
||||
// ,
|
||||
// TestModel()
|
||||
// )
|
||||
// test("init") {
|
||||
// assertEqualsJson(
|
||||
// lv.fullDiff,
|
||||
// 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") {
|
||||
// lv.update(TestModel(title = "title updated"))
|
||||
// assertEqualsJson(lv.diff, emptyDiff)
|
||||
// }
|
||||
// test("diff when true") {
|
||||
// lv.update(TestModel(bool = true))
|
||||
// assertEqualsJson(
|
||||
// lv.diff,
|
||||
// Json.Obj(
|
||||
// "0" ->
|
||||
// Json
|
||||
// .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(
|
||||
// lv.diff,
|
||||
// Json.Obj(
|
||||
// "0" ->
|
||||
// Json
|
||||
// .Obj(
|
||||
// "0" -> Json.Str("nested title updated")
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// test("splitByIndex mod") {
|
||||
// val initModel =
|
||||
// TestModel(
|
||||
// items = List(
|
||||
// NestedModel("a", 10),
|
||||
// NestedModel("b", 15),
|
||||
// NestedModel("c", 20)
|
||||
// )
|
||||
// )
|
||||
// val lv =
|
||||
// LiveView(
|
||||
// new View:
|
||||
// type Model = TestModel
|
||||
// val render =
|
||||
// div(
|
||||
// ul(
|
||||
// model.splitByIndex(_.items)(elem =>
|
||||
// li(
|
||||
// "Nom: ",
|
||||
// elem(_.name),
|
||||
// " Age: ",
|
||||
// elem(_.age.toString)
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// ,
|
||||
// initModel
|
||||
// )
|
||||
// test("init") {
|
||||
// assertEqualsJson(
|
||||
// lv.fullDiff,
|
||||
// Json
|
||||
// .Obj(
|
||||
// "s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
|
||||
// "0" -> Json.Obj(
|
||||
// "s" -> Json.Arr(
|
||||
// Json.Str("<li>Nom: "),
|
||||
// Json.Str(" Age: "),
|
||||
// Json.Str("</li>")
|
||||
// ),
|
||||
// "d" -> Json.Obj(
|
||||
// "0" -> Json.Obj(
|
||||
// "0" -> Json.Str("a"),
|
||||
// "1" -> Json.Str("10")
|
||||
// ),
|
||||
// "1" -> Json.Obj(
|
||||
// "0" -> Json.Str("b"),
|
||||
// "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"))
|
||||
// assertEqualsJson(lv.diff, emptyDiff)
|
||||
// }
|
||||
// test("diff with item changed") {
|
||||
// lv.update(
|
||||
// initModel.copy(items = initModel.items.updated(2, NestedModel("c", 99)))
|
||||
// )
|
||||
// assertEqualsJson(
|
||||
// lv.diff,
|
||||
// Json.Obj(
|
||||
// "0" ->
|
||||
// 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(
|
||||
// lv.diff,
|
||||
// Json.Obj(
|
||||
// "0" ->
|
||||
// Json
|
||||
// .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(
|
||||
// lv.diff,
|
||||
// Json.Obj(
|
||||
// "0" ->
|
||||
// Json
|
||||
// .Obj(
|
||||
// "d" -> Json.Obj(
|
||||
// "0" -> Json.Obj(
|
||||
// "0" -> Json.Str("b"),
|
||||
// "1" -> Json.Str("15")
|
||||
// ),
|
||||
// "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(
|
||||
// "0" ->
|
||||
// Json
|
||||
// .Obj(
|
||||
// "d" -> Json.Obj(
|
||||
// "0" -> Json.Bool(false),
|
||||
// "1" -> Json.Bool(false),
|
||||
// "2" -> Json.Bool(false)
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
//
|
||||
// }
|
||||
// }
|
||||
}
|
||||
end LiveViewSpec
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue