mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Generate dom elements with scala-dom-types
This commit is contained in:
parent
f53a1cab66
commit
ae0dc04a9e
15 changed files with 407 additions and 108 deletions
|
|
@ -1,2 +1,24 @@
|
||||||
version = "3.7.17"
|
version = "3.9.9"
|
||||||
runner.dialect = scala3
|
runner.dialect = scala3
|
||||||
|
|
||||||
|
preset=defaultWithAlign
|
||||||
|
|
||||||
|
assumeStandardLibraryStripMargin = true
|
||||||
|
maxColumn = 100
|
||||||
|
continuationIndent.callSite = 2
|
||||||
|
continuationIndent.defnSite = 2
|
||||||
|
align.arrowEnumeratorGenerator = true
|
||||||
|
align.openParenDefnSite = false
|
||||||
|
align.stripMargin = true
|
||||||
|
rewrite.rules = [RedundantBraces, Imports, RedundantParens, SortModifiers, PreferCurlyFors]
|
||||||
|
rewrite.redundantBraces.ifElseExpressions = true
|
||||||
|
rewrite.redundantBraces.stringInterpolation = true
|
||||||
|
verticalMultiline.atDefnSite = true
|
||||||
|
verticalMultiline.newlineAfterOpenParen = true
|
||||||
|
optIn.breaksInsideChains = true
|
||||||
|
lineEndings = unix
|
||||||
|
|
||||||
|
rewrite.scala3.convertToNewSyntax = true
|
||||||
|
rewrite.scala3.removeOptionalBraces = yes
|
||||||
|
rewrite.scala3.insertEndMarkerMinLines = 30
|
||||||
|
rewrite.scala3.removeEndMarkerMaxLines = 29
|
||||||
|
|
|
||||||
155
DomDefsGenerator.mill
Normal file
155
DomDefsGenerator.mill
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
import com.raquo.domtypes.codegen.*
|
||||||
|
import com.raquo.domtypes.codegen.DefType.LazyVal
|
||||||
|
import com.raquo.domtypes.defs.styles.StyleTraitDefs
|
||||||
|
import com.raquo.domtypes.codegen.generators.*
|
||||||
|
import com.raquo.domtypes.common.*
|
||||||
|
import com.raquo.domtypes.defs.reflectedAttrs.ReflectedHtmlAttrDefs
|
||||||
|
|
||||||
|
class DomDefsGenerator(baseOutputDirectoryPath: String):
|
||||||
|
|
||||||
|
private object generator
|
||||||
|
extends CanonicalGenerator(
|
||||||
|
baseOutputDirectoryPath = baseOutputDirectoryPath,
|
||||||
|
basePackagePath = "scalive",
|
||||||
|
standardTraitCommentLines = List(
|
||||||
|
"#NOTE: GENERATED CODE",
|
||||||
|
s" - This file is generated at compile time from the data in Scala DOM Types",
|
||||||
|
" - See `DomDefsGenerator.mill` for code generation params",
|
||||||
|
" - Contribute to https://github.com/raquo/scala-dom-types to add missing tags / attrs / props / etc."
|
||||||
|
),
|
||||||
|
format = CodeFormatting()
|
||||||
|
):
|
||||||
|
|
||||||
|
override def settersPackagePath: String = basePackagePath + ".modifiers.KeySetter"
|
||||||
|
override def scalaJsElementTypeParam: String = "Ref"
|
||||||
|
override def scalaJsDomImport: String = ""
|
||||||
|
override def tagKeysPackagePath: String = "scalive"
|
||||||
|
override def keysPackagePath: String = "scalive"
|
||||||
|
override def generateTagsTrait(
|
||||||
|
tagType: TagType,
|
||||||
|
defGroups: List[(String, List[TagDef])],
|
||||||
|
printDefGroupComments: Boolean,
|
||||||
|
traitCommentLines: List[String],
|
||||||
|
traitModifiers: List[String],
|
||||||
|
traitName: String,
|
||||||
|
keyKind: String,
|
||||||
|
baseImplDefComments: List[String],
|
||||||
|
keyImplName: String,
|
||||||
|
defType: DefType
|
||||||
|
): String =
|
||||||
|
val (defs, defGroupComments) = defsAndGroupComments(defGroups, printDefGroupComments)
|
||||||
|
|
||||||
|
val baseImplDef =
|
||||||
|
if tagType == HtmlTagType then
|
||||||
|
List(
|
||||||
|
s"def $keyImplName($keyImplNameArgName: String, void: Boolean = false): $keyKind = ${keyKindConstructor(keyKind)}($keyImplNameArgName, void)"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
List(
|
||||||
|
s"def $keyImplName($keyImplNameArgName: String): $keyKind = ${keyKindConstructor(keyKind)}($keyImplNameArgName)"
|
||||||
|
)
|
||||||
|
|
||||||
|
val headerLines = List(
|
||||||
|
s"package $tagDefsPackagePath",
|
||||||
|
"",
|
||||||
|
tagKeyTypeImport(keyKind),
|
||||||
|
scalaJsDomImport,
|
||||||
|
""
|
||||||
|
) ++ standardTraitCommentLines.map("// " + _)
|
||||||
|
|
||||||
|
new TagsTraitGenerator(
|
||||||
|
defs = defs,
|
||||||
|
defGroupComments = defGroupComments,
|
||||||
|
headerLines = headerLines,
|
||||||
|
traitCommentLines = traitCommentLines,
|
||||||
|
traitModifiers = traitModifiers,
|
||||||
|
traitName = traitName,
|
||||||
|
traitExtends = Nil,
|
||||||
|
traitThisType = None,
|
||||||
|
defType = _ => defType,
|
||||||
|
keyType = tag => keyKind,
|
||||||
|
keyImplName = _ => keyImplName,
|
||||||
|
keyImplNameArgName = keyImplNameArgName,
|
||||||
|
baseImplDefComments = baseImplDefComments,
|
||||||
|
baseImplDef = baseImplDef,
|
||||||
|
outputImplDefs = true,
|
||||||
|
format = format
|
||||||
|
).printTrait().getOutput()
|
||||||
|
end generateTagsTrait
|
||||||
|
end generator
|
||||||
|
|
||||||
|
def generate(): Unit =
|
||||||
|
val defGroups = new CanonicalDefGroups()
|
||||||
|
|
||||||
|
// -- HTML tags --
|
||||||
|
|
||||||
|
{
|
||||||
|
val traitName = "HtmlTags"
|
||||||
|
|
||||||
|
val fileContent = generator.generateTagsTrait(
|
||||||
|
tagType = HtmlTagType,
|
||||||
|
defGroups = defGroups.htmlTagsDefGroups,
|
||||||
|
printDefGroupComments = true,
|
||||||
|
traitCommentLines = Nil,
|
||||||
|
traitModifiers = Nil,
|
||||||
|
traitName = traitName,
|
||||||
|
keyKind = "HtmlTag",
|
||||||
|
baseImplDefComments = List(
|
||||||
|
"Create HTML tag",
|
||||||
|
"",
|
||||||
|
"Note: this simply creates an instance of HtmlTag.",
|
||||||
|
" - This does not create the element (to do that, call .apply() on the returned tag instance)",
|
||||||
|
"",
|
||||||
|
"@param name - e.g. \"div\" or \"mwc-input\""
|
||||||
|
),
|
||||||
|
keyImplName = "htmlTag",
|
||||||
|
defType = LazyVal
|
||||||
|
)
|
||||||
|
|
||||||
|
generator.writeToFile(
|
||||||
|
packagePath = generator.tagDefsPackagePath,
|
||||||
|
fileName = traitName,
|
||||||
|
fileContent = fileContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- HTML attributes --
|
||||||
|
|
||||||
|
{
|
||||||
|
val traitName = "HtmlAttrs"
|
||||||
|
|
||||||
|
val fileContent = generator.generateAttrsTrait(
|
||||||
|
defGroups = defGroups.htmlAttrDefGroups.appended(
|
||||||
|
"Reflected Attributes" -> ReflectedHtmlAttrDefs.defs.map(_.toAttrDef)
|
||||||
|
),
|
||||||
|
printDefGroupComments = false,
|
||||||
|
traitCommentLines = Nil,
|
||||||
|
traitModifiers = Nil,
|
||||||
|
traitName = traitName,
|
||||||
|
keyKind = "HtmlAttr",
|
||||||
|
implNameSuffix = "HtmlAttr",
|
||||||
|
baseImplDefComments = List(
|
||||||
|
"Create HTML attribute (Note: for SVG attrs, use L.svg.svgAttr)",
|
||||||
|
"",
|
||||||
|
"@param name - name of the attribute, e.g. \"value\"",
|
||||||
|
"@param codec - used to encode V into String, e.g. StringAsIsCodec",
|
||||||
|
"",
|
||||||
|
"@tparam V - value type for this attr in Scala"
|
||||||
|
),
|
||||||
|
baseImplName = "htmlAttr",
|
||||||
|
namespaceImports = Nil,
|
||||||
|
namespaceImpl = _ => ???,
|
||||||
|
transformAttrDomName = identity,
|
||||||
|
defType = LazyVal
|
||||||
|
)
|
||||||
|
|
||||||
|
generator.writeToFile(
|
||||||
|
packagePath = generator.attrDefsPackagePath,
|
||||||
|
fileName = traitName,
|
||||||
|
fileContent = fileContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end generate
|
||||||
|
end DomDefsGenerator
|
||||||
13
build.mill
13
build.mill
|
|
@ -1,16 +1,25 @@
|
||||||
|
//| mvnDeps : ["com.raquo::domtypes:18.1.0"]
|
||||||
|
|
||||||
package build
|
package build
|
||||||
|
|
||||||
import mill.*, scalalib.*
|
import mill.*, scalalib.*
|
||||||
|
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")
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
def generatedSources = Task {
|
||||||
|
new DomDefsGenerator((Task.dest / "core/src/scalive").toString).generate()
|
||||||
|
super.generatedSources() ++ Seq(PathRef(Task.dest))
|
||||||
|
}
|
||||||
|
|
||||||
object test extends ScalaTests with TestModule.Utest:
|
object test extends ScalaTests with TestModule.Utest:
|
||||||
def utestVersion = "0.9.0"
|
def utestVersion = "0.9.0"
|
||||||
|
|
||||||
object zio extends Common:
|
object zio extends Common:
|
||||||
def mvnDeps = Seq(mvn"dev.zio::zio-http:3.4.0")
|
def mvnDeps = Seq(mvn"dev.zio::zio-http:3.4.0")
|
||||||
override def moduleDeps = Seq(core)
|
override def moduleDeps = Seq(core)
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@ import zio.json.ast.Json
|
||||||
|
|
||||||
enum Diff:
|
enum Diff:
|
||||||
case Tag(
|
case Tag(
|
||||||
static: Seq[String] = Seq.empty,
|
static: Seq[String] = Seq.empty,
|
||||||
dynamic: Seq[Diff.Dynamic] = Seq.empty
|
dynamic: Seq[Diff.Dynamic] = Seq.empty)
|
||||||
)
|
|
||||||
case Split(
|
case Split(
|
||||||
static: Seq[String] = Seq.empty,
|
static: Seq[String] = Seq.empty,
|
||||||
entries: Seq[Diff.Dynamic] = Seq.empty
|
entries: Seq[Diff.Dynamic] = Seq.empty)
|
||||||
)
|
|
||||||
case Static(value: String)
|
case Static(value: String)
|
||||||
case Dynamic(index: Int, diff: Diff)
|
case Dynamic(index: Int, diff: Diff)
|
||||||
case Deleted
|
case Deleted
|
||||||
|
|
@ -45,7 +43,8 @@ object Diff:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
case Diff.Static(value) => Json.Str(value)
|
case Diff.Static(value) => Json.Str(value)
|
||||||
case Diff.Dynamic(index, diff) =>
|
case Diff.Dynamic(index, diff) =>
|
||||||
Json.Obj(index.toString -> toJson(diff))
|
Json.Obj(index.toString -> toJson(diff))
|
||||||
case Diff.Deleted => Json.Bool(false)
|
case Diff.Deleted => Json.Bool(false)
|
||||||
|
end Diff
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ package scalive
|
||||||
|
|
||||||
object DiffBuilder:
|
object DiffBuilder:
|
||||||
def build(
|
def build(
|
||||||
static: Seq[String],
|
static: Seq[String],
|
||||||
dynamic: Seq[LiveDyn[?]],
|
dynamic: Seq[LiveDyn[?]],
|
||||||
includeUnchanged: Boolean = false
|
includeUnchanged: Boolean = false
|
||||||
): Diff.Tag =
|
): Diff.Tag =
|
||||||
Diff.Tag(
|
Diff.Tag(
|
||||||
static = static,
|
static = static,
|
||||||
|
|
@ -13,16 +13,16 @@ object DiffBuilder:
|
||||||
.map {
|
.map {
|
||||||
case (v: LiveDyn.Value[?, ?], i) =>
|
case (v: LiveDyn.Value[?, ?], i) =>
|
||||||
Diff.Dynamic(i, Diff.Static(v.currentValue.toString))
|
Diff.Dynamic(i, Diff.Static(v.currentValue.toString))
|
||||||
case (v: LiveDyn.When[?], i) => build(v, i, includeUnchanged)
|
case (v: LiveDyn.When[?], i) => build(v, i, includeUnchanged)
|
||||||
case (v: LiveDyn.Split[?, ?], i) =>
|
case (v: LiveDyn.Split[?, ?], i) =>
|
||||||
Diff.Dynamic(i, build(v, includeUnchanged))
|
Diff.Dynamic(i, build(v, includeUnchanged))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
private def build(
|
private def build(
|
||||||
mod: LiveDyn.When[?],
|
mod: LiveDyn.When[?],
|
||||||
index: Int,
|
index: Int,
|
||||||
includeUnchanged: Boolean
|
includeUnchanged: Boolean
|
||||||
): Diff.Dynamic =
|
): Diff.Dynamic =
|
||||||
if mod.displayed then
|
if mod.displayed then
|
||||||
if includeUnchanged || mod.cond.wasUpdated then
|
if includeUnchanged || mod.cond.wasUpdated then
|
||||||
|
|
@ -46,8 +46,8 @@ object DiffBuilder:
|
||||||
else Diff.Dynamic(index, Diff.Deleted)
|
else Diff.Dynamic(index, Diff.Deleted)
|
||||||
|
|
||||||
private def build(
|
private def build(
|
||||||
mod: LiveDyn.Split[?, ?],
|
mod: LiveDyn.Split[?, ?],
|
||||||
includeUnchanged: Boolean
|
includeUnchanged: Boolean
|
||||||
): Diff.Split =
|
): Diff.Split =
|
||||||
Diff.Split(
|
Diff.Split(
|
||||||
static = if includeUnchanged then mod.static else Seq.empty,
|
static = if includeUnchanged then mod.static else Seq.empty,
|
||||||
|
|
@ -68,3 +68,4 @@ object DiffBuilder:
|
||||||
.map(i => Diff.Dynamic(i, Diff.Deleted))
|
.map(i => Diff.Dynamic(i, Diff.Deleted))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
end DiffBuilder
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ object HtmlBuilder:
|
||||||
strw.toString()
|
strw.toString()
|
||||||
|
|
||||||
private def build(
|
private def build(
|
||||||
static: Seq[String],
|
static: Seq[String],
|
||||||
dynamic: Seq[LiveDyn[?]],
|
dynamic: Seq[LiveDyn[?]],
|
||||||
strw: StringWriter
|
strw: StringWriter
|
||||||
): Unit =
|
): Unit =
|
||||||
for i <- dynamic.indices do
|
for i <- dynamic.indices do
|
||||||
strw.append(static(i))
|
strw.append(static(i))
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,12 @@ sealed trait LiveDyn[Model]:
|
||||||
|
|
||||||
object LiveDyn:
|
object LiveDyn:
|
||||||
|
|
||||||
class Value[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false)
|
class Value[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false) extends LiveDyn[I]:
|
||||||
extends LiveDyn[I]:
|
private var value: O = d.run(init)
|
||||||
private var value: O = d.run(init)
|
|
||||||
private var updated: Boolean = startsUpdated
|
private var updated: Boolean = startsUpdated
|
||||||
def wasUpdated: Boolean = updated
|
def wasUpdated: Boolean = updated
|
||||||
def currentValue: O = value
|
def currentValue: O = value
|
||||||
def update(v: I): Unit =
|
def update(v: I): Unit =
|
||||||
val newValue = d.run(v)
|
val newValue = d.run(v)
|
||||||
if value == newValue then updated = false
|
if value == newValue then updated = false
|
||||||
else
|
else
|
||||||
|
|
@ -23,25 +22,25 @@ object LiveDyn:
|
||||||
updated = true
|
updated = true
|
||||||
|
|
||||||
class When[Model](
|
class When[Model](
|
||||||
dynCond: Dyn[Model, Boolean],
|
dynCond: Dyn[Model, Boolean],
|
||||||
el: HtmlElement[Model],
|
el: HtmlElement[Model],
|
||||||
init: Model
|
init: Model)
|
||||||
) extends LiveDyn[Model]:
|
extends LiveDyn[Model]:
|
||||||
val cond = LiveDyn.Value(dynCond, init)
|
val cond = LiveDyn.Value(dynCond, init)
|
||||||
val nested = LiveView.render(el, init)
|
val nested = LiveView.render(el, init)
|
||||||
def displayed: Boolean = cond.currentValue
|
def displayed: Boolean = cond.currentValue
|
||||||
def wasUpdated: Boolean = cond.wasUpdated || nested.wasUpdated
|
def wasUpdated: Boolean = cond.wasUpdated || nested.wasUpdated
|
||||||
def update(model: Model): Unit =
|
def update(model: Model): Unit =
|
||||||
cond.update(model)
|
cond.update(model)
|
||||||
nested.update(model)
|
nested.update(model)
|
||||||
|
|
||||||
class Split[Model, Item](
|
class Split[Model, Item](
|
||||||
dynList: Dyn[Model, List[Item]],
|
dynList: Dyn[Model, List[Item]],
|
||||||
project: Dyn[Item, Item] => HtmlElement[Item],
|
project: Dyn[Item, Item] => HtmlElement[Item],
|
||||||
init: Model
|
init: Model)
|
||||||
) extends LiveDyn[Model]:
|
extends LiveDyn[Model]:
|
||||||
private val el = project(Dyn.id)
|
private val el = project(Dyn.id)
|
||||||
val static: ArraySeq[String] = LiveView.buildStatic(el)
|
val static: ArraySeq[String] = LiveView.buildStatic(el)
|
||||||
val dynamic: ArrayBuffer[ArraySeq[LiveDyn[Item]]] =
|
val dynamic: ArrayBuffer[ArraySeq[LiveDyn[Item]]] =
|
||||||
dynList
|
dynList
|
||||||
.run(init)
|
.run(init)
|
||||||
|
|
@ -65,3 +64,5 @@ object LiveDyn:
|
||||||
)
|
)
|
||||||
else dynamic(i).foreach(_.update(item))
|
else dynamic(i).foreach(_.update(item))
|
||||||
)
|
)
|
||||||
|
end Split
|
||||||
|
end LiveDyn
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ package scalive
|
||||||
import scala.annotation.nowarn
|
import scala.annotation.nowarn
|
||||||
import scala.collection.immutable.ArraySeq
|
import scala.collection.immutable.ArraySeq
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
import scalive.codecs.BooleanAsAttrPresenceCodec
|
||||||
|
|
||||||
class LiveView[Model] private (
|
class LiveView[Model] private (
|
||||||
val static: ArraySeq[String],
|
val static: ArraySeq[String],
|
||||||
val dynamic: ArraySeq[LiveDyn[Model]]
|
val dynamic: ArraySeq[LiveDyn[Model]]):
|
||||||
):
|
|
||||||
assert(
|
assert(
|
||||||
static.size == dynamic.size + 1,
|
static.size == dynamic.size + 1,
|
||||||
s"Static size : ${static.size}, Dynamic size : ${dynamic.size}"
|
s"Static size : ${static.size}, Dynamic size : ${dynamic.size}"
|
||||||
|
|
@ -26,14 +26,14 @@ class LiveView[Model] private (
|
||||||
object LiveView:
|
object LiveView:
|
||||||
|
|
||||||
inline def apply[Model](
|
inline def apply[Model](
|
||||||
lv: View[Model],
|
lv: View[Model],
|
||||||
model: Model
|
model: Model
|
||||||
): LiveView[Model] =
|
): LiveView[Model] =
|
||||||
render(lv.root, model)
|
render(lv.root, model)
|
||||||
|
|
||||||
def render[Model](
|
def render[Model](
|
||||||
tag: HtmlElement[Model],
|
tag: HtmlElement[Model],
|
||||||
model: Model
|
model: Model
|
||||||
): LiveView[Model] =
|
): LiveView[Model] =
|
||||||
new LiveView(buildStatic(tag), buildDynamic(tag, model).to(ArraySeq))
|
new LiveView(buildStatic(tag), buildDynamic(tag, model).to(ArraySeq))
|
||||||
|
|
||||||
|
|
@ -41,11 +41,11 @@ object LiveView:
|
||||||
buildStaticFragments(el).flatten.to(ArraySeq)
|
buildStaticFragments(el).flatten.to(ArraySeq)
|
||||||
|
|
||||||
private def buildStaticFragments[Model](
|
private def buildStaticFragments[Model](
|
||||||
el: HtmlElement[Model]
|
el: HtmlElement[Model]
|
||||||
): Seq[Option[String]] =
|
): Seq[Option[String]] =
|
||||||
val (attrs, children) = buildStaticFragmentsByType(el)
|
val (attrs, children) = buildStaticFragmentsByType(el)
|
||||||
val static = ListBuffer.empty[Option[String]]
|
val static = ListBuffer.empty[Option[String]]
|
||||||
var staticFragment = s"<${el.tag.name}"
|
var staticFragment = s"<${el.tag.name}"
|
||||||
for attr <- attrs do
|
for attr <- attrs do
|
||||||
attr match
|
attr match
|
||||||
case Some(s) =>
|
case Some(s) =>
|
||||||
|
|
@ -54,7 +54,7 @@ object LiveView:
|
||||||
static.append(Some(staticFragment))
|
static.append(Some(staticFragment))
|
||||||
static.append(None)
|
static.append(None)
|
||||||
staticFragment = ""
|
staticFragment = ""
|
||||||
staticFragment += s">"
|
staticFragment += (if el.tag.void then "/>" else ">")
|
||||||
for child <- children do
|
for child <- children do
|
||||||
child match
|
child match
|
||||||
case Some(s) =>
|
case Some(s) =>
|
||||||
|
|
@ -63,19 +63,22 @@ object LiveView:
|
||||||
static.append(Some(staticFragment))
|
static.append(Some(staticFragment))
|
||||||
static.append(None)
|
static.append(None)
|
||||||
staticFragment = ""
|
staticFragment = ""
|
||||||
staticFragment += s"</${el.tag.name}>"
|
staticFragment += (if el.tag.void then "" else s"</${el.tag.name}>")
|
||||||
static.append(Some(staticFragment))
|
static.append(Some(staticFragment))
|
||||||
static.toSeq
|
static.toSeq
|
||||||
|
|
||||||
@nowarn("cat=unchecked")
|
@nowarn("cat=unchecked")
|
||||||
private def buildStaticFragmentsByType[Model](
|
private def buildStaticFragmentsByType[Model](
|
||||||
el: HtmlElement[Model]
|
el: HtmlElement[Model]
|
||||||
): (attrs: Seq[Option[String]], children: Seq[Option[String]]) =
|
): (attrs: Seq[Option[String]], children: Seq[Option[String]]) =
|
||||||
val (attrs, children) = el.mods.partitionMap {
|
val (attrs, children) = el.mods.partitionMap {
|
||||||
case Mod.StaticAttr(attr, value) =>
|
case Mod.StaticAttr(attr, value) => Left(List(Some(s""" ${attr.name}="$value"""")))
|
||||||
Left(List(Some(s""" ${attr.name}="$value"""")))
|
case Mod.StaticAttrValueAsPresence(attr, true) => Left(List(Some(s" ${attr.name}")))
|
||||||
case Mod.DynAttr(attr, _) =>
|
case Mod.StaticAttrValueAsPresence(attr, false) => Left(List.empty)
|
||||||
|
case Mod.DynAttr(attr, _) =>
|
||||||
Left(List(Some(s""" ${attr.name}=""""), None, Some('"'.toString)))
|
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.Tag(el) => Right(buildStaticFragments(el))
|
||||||
case Mod.Text(text) => Right(List(Some(text)))
|
case Mod.Text(text) => Right(List(Some(text)))
|
||||||
case Mod.DynText[Model](_) => Right(List(None))
|
case Mod.DynText[Model](_) => Right(List(None))
|
||||||
|
|
@ -86,15 +89,18 @@ object LiveView:
|
||||||
|
|
||||||
@nowarn("cat=unchecked")
|
@nowarn("cat=unchecked")
|
||||||
def buildDynamic[Model](
|
def buildDynamic[Model](
|
||||||
el: HtmlElement[Model],
|
el: HtmlElement[Model],
|
||||||
model: Model,
|
model: Model,
|
||||||
startsUpdated: Boolean = false
|
startsUpdated: Boolean = false
|
||||||
): Seq[LiveDyn[Model]] =
|
): Seq[LiveDyn[Model]] =
|
||||||
val (attrs, children) = el.mods.partitionMap {
|
val (attrs, children) = el.mods.partitionMap {
|
||||||
case Mod.Text(_) => Right(List.empty)
|
case Mod.Text(_) => Right(List.empty)
|
||||||
case Mod.StaticAttr(_, _) => Left(List.empty)
|
case Mod.StaticAttr(_, _) => Left(List.empty)
|
||||||
case Mod.DynAttr(_, value) =>
|
case Mod.StaticAttrValueAsPresence(_, _) => Left(List.empty)
|
||||||
Right(List(LiveDyn.Value(value, model, startsUpdated)))
|
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) =>
|
case Mod.Tag(el) =>
|
||||||
Right(buildDynamic(el, model, startsUpdated))
|
Right(buildDynamic(el, model, startsUpdated))
|
||||||
case Mod.DynText[Model](dynText) =>
|
case Mod.DynText[Model](dynText) =>
|
||||||
|
|
@ -105,3 +111,4 @@ object LiveView:
|
||||||
Right(List(LiveDyn.Split(dynList, project, model)))
|
Right(List(LiveDyn.Split(dynList, project, model)))
|
||||||
}
|
}
|
||||||
attrs.flatten ++ children.flatten
|
attrs.flatten ++ children.flatten
|
||||||
|
end LiveView
|
||||||
|
|
|
||||||
9
core/src/scalive/Scalive.scala
Normal file
9
core/src/scalive/Scalive.scala
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package scalive
|
||||||
|
|
||||||
|
import scalive.defs.tags.HtmlTags
|
||||||
|
import scalive.defs.attrs.HtmlAttrs
|
||||||
|
import scalive.defs.complex.ComplexHtmlKeys
|
||||||
|
|
||||||
|
object Scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys
|
||||||
|
|
||||||
|
export Scalive.*
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
|
import scalive.codecs.Codec
|
||||||
|
import scalive.codecs.BooleanAsAttrPresenceCodec
|
||||||
|
|
||||||
trait View[Model]:
|
trait View[Model]:
|
||||||
val model: Dyn[Model, Model] = Dyn.id
|
val model: Dyn[Model, Model] = Dyn.id
|
||||||
def root: HtmlElement[Model]
|
def root: HtmlElement[Model]
|
||||||
|
|
@ -14,8 +17,10 @@ extension [I, O](d: Dyn[I, O])
|
||||||
inline def whenNot(f: O => Boolean)(el: HtmlElement[I]): Mod.When[I] =
|
inline def whenNot(f: O => Boolean)(el: HtmlElement[I]): Mod.When[I] =
|
||||||
when(f.andThen(!_))(el)
|
when(f.andThen(!_))(el)
|
||||||
|
|
||||||
def splitByIndex[O2](f: O => List[O2])(
|
def splitByIndex[O2](
|
||||||
project: Dyn[O2, O2] => HtmlElement[O2]
|
f: O => List[O2]
|
||||||
|
)(
|
||||||
|
project: Dyn[O2, O2] => HtmlElement[O2]
|
||||||
): Mod.Split[I, O2] =
|
): Mod.Split[I, O2] =
|
||||||
Mod.Split(d.andThen(f), project)
|
Mod.Split(d.andThen(f), project)
|
||||||
|
|
||||||
|
|
@ -25,34 +30,37 @@ object Dyn:
|
||||||
def id[T]: Dyn[T, T] = identity
|
def id[T]: Dyn[T, T] = identity
|
||||||
|
|
||||||
enum Mod[T]:
|
enum Mod[T]:
|
||||||
case StaticAttr(attr: HtmlAttr, value: String)
|
case StaticAttr(attr: HtmlAttr[?], value: String)
|
||||||
case DynAttr(attr: HtmlAttr, value: Dyn[T, String])
|
case 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 Tag(el: HtmlElement[T])
|
||||||
case Text(text: String)
|
case Text(text: String)
|
||||||
case DynText(dynText: Dyn[T, String])
|
case DynText(dynText: Dyn[T, String])
|
||||||
case When(dynCond: Dyn[T, Boolean], el: HtmlElement[T])
|
case When(dynCond: Dyn[T, Boolean], el: HtmlElement[T])
|
||||||
case Split[T, O](
|
case Split[T, O](
|
||||||
dynList: Dyn[T, List[O]],
|
dynList: Dyn[T, List[O]],
|
||||||
project: Dyn[O, O] => HtmlElement[O]
|
project: Dyn[O, O] => HtmlElement[O]) extends Mod[T]
|
||||||
) extends Mod[T]
|
|
||||||
|
|
||||||
given [T]: Conversion[HtmlElement[T], Mod[T]] = Mod.Tag(_)
|
given [T]: Conversion[HtmlElement[T], Mod[T]] = Mod.Tag(_)
|
||||||
given [T]: Conversion[String, Mod[T]] = Mod.Text(_)
|
given [T]: Conversion[String, Mod[T]] = Mod.Text(_)
|
||||||
given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_)
|
given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_)
|
||||||
|
|
||||||
class HtmlTag(val name: String):
|
class HtmlTag(val name: String, val void: Boolean = false):
|
||||||
def apply[Model](mods: Mod[Model]*): HtmlElement[Model] =
|
def apply[Model](mods: Mod[Model]*): HtmlElement[Model] =
|
||||||
HtmlElement(this, mods.toVector)
|
HtmlElement(this, mods.toVector)
|
||||||
|
|
||||||
class HtmlElement[Model](val tag: HtmlTag, val mods: Vector[Mod[Model]])
|
class HtmlElement[Model](val tag: HtmlTag, val mods: Vector[Mod[Model]])
|
||||||
|
|
||||||
val div = HtmlTag("div")
|
class HtmlAttr[V](val name: String, val codec: Codec[V, String]):
|
||||||
val ul = HtmlTag("ul")
|
val isBooleanAsAttrPresence = codec == BooleanAsAttrPresenceCodec
|
||||||
val li = HtmlTag("li")
|
def :=[T](value: V): Mod[T] =
|
||||||
|
val stringValue = codec.encode(value)
|
||||||
class HtmlAttr(val name: String):
|
if isBooleanAsAttrPresence then
|
||||||
def :=[T](value: String): Mod.StaticAttr[T] = Mod.StaticAttr(this, value)
|
Mod.StaticAttrValueAsPresence(this, BooleanAsAttrPresenceCodec.decode(stringValue))
|
||||||
def :=[T](value: Dyn[T, String]): Mod.DynAttr[T] = Mod.DynAttr(this, value)
|
else Mod.StaticAttr(this, stringValue)
|
||||||
|
def :=[T](value: Dyn[T, V]): Mod[T] =
|
||||||
val idAttr = HtmlAttr("id")
|
val stringDyn = value(codec.encode)
|
||||||
val cls = HtmlAttr("class")
|
if isBooleanAsAttrPresence then
|
||||||
|
Mod.DynAttrValueAsPresence(this, stringDyn(BooleanAsAttrPresenceCodec.decode))
|
||||||
|
else Mod.DynAttr(this, value)
|
||||||
|
|
|
||||||
29
core/src/scalive/codecs/Codec.scala
Normal file
29
core/src/scalive/codecs/Codec.scala
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package scalive.codecs
|
||||||
|
|
||||||
|
class Codec[ScalaType, DomType](val encode: ScalaType => DomType, val decode: DomType => ScalaType)
|
||||||
|
|
||||||
|
def AsIsCodec[V](): Codec[V, V] = Codec(identity, identity)
|
||||||
|
|
||||||
|
val StringAsIsCodec: Codec[String, String] = AsIsCodec()
|
||||||
|
|
||||||
|
val IntAsIsCodec: Codec[Int, Int] = AsIsCodec()
|
||||||
|
|
||||||
|
lazy val IntAsStringCodec: Codec[Int, String] = Codec[Int, String](_.toString, _.toInt)
|
||||||
|
|
||||||
|
lazy val DoubleAsIsCodec: Codec[Double, Double] = AsIsCodec()
|
||||||
|
|
||||||
|
lazy val DoubleAsStringCodec: Codec[Double, String] = Codec[Double, String](_.toString, _.toDouble)
|
||||||
|
|
||||||
|
val BooleanAsIsCodec: Codec[Boolean, Boolean] = AsIsCodec()
|
||||||
|
|
||||||
|
lazy val BooleanAsAttrPresenceCodec: Codec[Boolean, String] =
|
||||||
|
Codec[Boolean, String](if _ then "" else null, _ != null)
|
||||||
|
|
||||||
|
lazy val BooleanAsTrueFalseStringCodec: Codec[Boolean, String] =
|
||||||
|
Codec[Boolean, String](if _ then "true" else "false", _ == "true")
|
||||||
|
|
||||||
|
lazy val BooleanAsYesNoStringCodec: Codec[Boolean, String] =
|
||||||
|
Codec[Boolean, String](if _ then "yes" else "no", _ == "yes")
|
||||||
|
|
||||||
|
lazy val BooleanAsOnOffStringCodec: Codec[Boolean, String] =
|
||||||
|
Codec[Boolean, String](if _ then "on" else "off", _ == "on")
|
||||||
63
core/src/scalive/defs/complex/ComplexHtmlKeys.scala
Normal file
63
core/src/scalive/defs/complex/ComplexHtmlKeys.scala
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package scalive.defs.complex
|
||||||
|
|
||||||
|
import scalive.HtmlAttr
|
||||||
|
import scalive.codecs.*
|
||||||
|
|
||||||
|
// TODO implement composite keys
|
||||||
|
trait ComplexHtmlKeys:
|
||||||
|
|
||||||
|
// #Note: we use attrs instead of props here because of https://github.com/raquo/Laminar/issues/136
|
||||||
|
|
||||||
|
/** This attribute is a list of the classes of the element. Classes allow CSS and Javascript to
|
||||||
|
* select and access specific elements via the class selectors or functions like the DOM method
|
||||||
|
* document.getElementsByClassName
|
||||||
|
*/
|
||||||
|
val className: HtmlAttr[String] = new HtmlAttr("class", StringAsIsCodec)
|
||||||
|
|
||||||
|
val cls: HtmlAttr[String] = className
|
||||||
|
|
||||||
|
/** This attribute names a relationship of the linked document to the current document. The
|
||||||
|
* attribute must be a space-separated list of the link types values. The most common use of this
|
||||||
|
* attribute is to specify a link to an external style sheet: the rel attribute is set to
|
||||||
|
* stylesheet, and the href attribute is set to the URL of an external style sheet to format the
|
||||||
|
* page.
|
||||||
|
*/
|
||||||
|
lazy val rel: HtmlAttr[String] = new HtmlAttr("rel", StringAsIsCodec)
|
||||||
|
|
||||||
|
/** The attribute describes the role(s) the current element plays in the context of the document.
|
||||||
|
* This can be used, for example, by applications and assistive technologies to determine the
|
||||||
|
* purpose of an element. This could allow a user to make informed decisions on which actions may
|
||||||
|
* be taken on an element and activate the selected action in a device independent way. It could
|
||||||
|
* also be used as a mechanism for annotating portions of a document in a domain specific way
|
||||||
|
* (e.g., a legal term taxonomy). Although the role attribute may be used to add semantics to an
|
||||||
|
* element, authors should use elements with inherent semantics, such as p, rather than layering
|
||||||
|
* semantics on semantically neutral elements, such as div role="paragraph".
|
||||||
|
*
|
||||||
|
* See: [[http://www.w3.org/TR/role-attribute/#s_role_module_attributes]]
|
||||||
|
*/
|
||||||
|
lazy val role: HtmlAttr[String] = new HtmlAttr("role", StringAsIsCodec)
|
||||||
|
|
||||||
|
/** This class of attributes, called custom data attributes, allows proprietary information to be
|
||||||
|
* exchanged between the HTML and its DOM representation that may be used by scripts. All such
|
||||||
|
* custom data are available via the HTMLElement interface of the element the attribute is set
|
||||||
|
* on. The HTMLElement.dataset property gives access to them.
|
||||||
|
*
|
||||||
|
* The `suffix` is subject to the following restrictions:
|
||||||
|
*
|
||||||
|
* must not start with xml, whatever case is used for these letters; must not contain any
|
||||||
|
* semicolon (U+003A); must not contain capital A to Z letters.
|
||||||
|
*
|
||||||
|
* Note that the HTMLElement.dataset attribute is a StringMap and the name of the custom data
|
||||||
|
* attribute data-test-value will be accessible via HTMLElement.dataset.testValue as any dash
|
||||||
|
* (U+002D) is replaced by the capitalization of the next letter (camelcase).
|
||||||
|
*/
|
||||||
|
def dataAttr(suffix: String): HtmlAttr[String] = new HtmlAttr(s"data-$suffix", StringAsIsCodec)
|
||||||
|
|
||||||
|
/** This attribute contains CSS styling declarations to be applied to the element. Note that it is
|
||||||
|
* recommended for styles to be defined in a separate file or files. This attribute and the style
|
||||||
|
* element have mainly the purpose of allowing for quick styling, for example for testing
|
||||||
|
* purposes.
|
||||||
|
*/
|
||||||
|
lazy val styleAttr: HtmlAttr[String] = new HtmlAttr("style", StringAsIsCodec)
|
||||||
|
|
||||||
|
end ComplexHtmlKeys
|
||||||
|
|
@ -60,19 +60,22 @@ def main =
|
||||||
|
|
||||||
println("Remove all")
|
println("Remove all")
|
||||||
lv.update(
|
lv.update(
|
||||||
MyModel(List.empty, "text-lg")
|
MyModel(List.empty, "text-lg", bool = false)
|
||||||
)
|
)
|
||||||
println(lv.diff.toJsonPretty)
|
println(lv.diff.toJsonPretty)
|
||||||
println(HtmlBuilder.build(lv))
|
println(HtmlBuilder.build(lv))
|
||||||
|
end main
|
||||||
|
|
||||||
final case class MyModel(elems: List[NestedModel], cls: String = "text-xs")
|
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 View[MyModel]:
|
||||||
val root: HtmlElement[MyModel] =
|
val root: HtmlElement[MyModel] =
|
||||||
div(
|
div(
|
||||||
idAttr := "42",
|
idAttr := "42",
|
||||||
cls := model(_.cls),
|
cls := model(_.cls),
|
||||||
|
draggable := model(_.bool),
|
||||||
|
disabled := model(_.bool),
|
||||||
ul(
|
ul(
|
||||||
model.splitByIndex(_.elems)(elem =>
|
model.splitByIndex(_.elems)(elem =>
|
||||||
li(
|
li(
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,11 @@ import zio.json.ast.Json
|
||||||
object LiveViewSpec extends TestSuite:
|
object LiveViewSpec extends TestSuite:
|
||||||
|
|
||||||
final case class TestModel(
|
final case class TestModel(
|
||||||
title: String = "title value",
|
title: String = "title value",
|
||||||
bool: Boolean = false,
|
bool: Boolean = false,
|
||||||
nestedTitle: String = "nested title value",
|
nestedTitle: String = "nested title value",
|
||||||
cls: String = "text-sm",
|
cls: String = "text-sm",
|
||||||
items: List[NestedModel] = List.empty
|
items: List[NestedModel] = List.empty)
|
||||||
)
|
|
||||||
final case class NestedModel(name: String, age: Int)
|
final case class NestedModel(name: String, age: Int)
|
||||||
|
|
||||||
def assertEqualsJson(actual: Diff, expected: Json) =
|
def assertEqualsJson(actual: Diff, expected: Json) =
|
||||||
|
|
@ -245,9 +244,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
}
|
}
|
||||||
test("diff with item changed") {
|
test("diff with item changed") {
|
||||||
lv.update(
|
lv.update(
|
||||||
initModel.copy(items =
|
initModel.copy(items = initModel.items.updated(2, NestedModel("c", 99)))
|
||||||
initModel.items.updated(2, NestedModel("c", 99))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
lv.diff,
|
lv.diff,
|
||||||
|
|
@ -329,3 +326,4 @@ object LiveViewSpec extends TestSuite:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
end LiveViewSpec
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,13 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
import zio._
|
import zio.*
|
||||||
|
|
||||||
import zio.http.ChannelEvent.{
|
import zio.http.ChannelEvent.{ExceptionCaught, Read, UserEvent, UserEventTriggered}
|
||||||
ExceptionCaught,
|
import zio.http.*
|
||||||
Read,
|
|
||||||
UserEvent,
|
|
||||||
UserEventTriggered
|
|
||||||
}
|
|
||||||
import zio.http._
|
|
||||||
import zio.http.codec.PathCodec.string
|
import zio.http.codec.PathCodec.string
|
||||||
import zio.http.template.Html
|
import zio.http.template.Html
|
||||||
|
|
||||||
object Example extends ZIOAppDefault {
|
object Example extends ZIOAppDefault:
|
||||||
|
|
||||||
val lv =
|
val lv =
|
||||||
LiveView(
|
LiveView(
|
||||||
|
|
@ -77,7 +72,7 @@ object Example extends ZIOAppDefault {
|
||||||
)
|
)
|
||||||
|
|
||||||
override val run = Server.serve(routes).provide(Server.default)
|
override val run = Server.serve(routes).provide(Server.default)
|
||||||
}
|
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)
|
||||||
|
|
@ -86,7 +81,7 @@ object TestView extends View[MyModel]:
|
||||||
val root: HtmlElement[MyModel] =
|
val root: HtmlElement[MyModel] =
|
||||||
div(
|
div(
|
||||||
idAttr := "42",
|
idAttr := "42",
|
||||||
cls := model(_.cls),
|
cls := model(_.cls),
|
||||||
ul(
|
ul(
|
||||||
model.splitByIndex(_.elems)(elem =>
|
model.splitByIndex(_.elems)(elem =>
|
||||||
li(
|
li(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue