feat: CSR WIP

This commit is contained in:
DavidOnTop 2024-09-21 16:56:25 +02:00
parent 0c0ed26408
commit 959241c16b
No known key found for this signature in database
GPG key ID: 5D05538A45D5149F
19 changed files with 172 additions and 65 deletions

27
README.md Normal file
View file

@ -0,0 +1,27 @@
## $ whoami
SFS full name ScalaFullStack is a collection of libraries to make full stack development in scala easy and composable
## Features
This project is far from being ready to use so for now these also include planned features for 1.0
- [X] SSR support
- [ ] CSR support
- [ ] Hydration
- [ ] router
- [ ] RPC integration or server functions
- [ ] ReScala reactive backend
- [ ] zio server integration
### TODO
- [ ] Full type safety, eliminate ? as generic param and asInstanceOf where possible
- [ ] AirStream reactive backend
- [ ] other server integrations
## Packages
- sfs - different renderers (String, dom, hydration) and reactivity bindings
- dom - shared jvm/js html builder which includes: tags, attributes, props, svg utils
- possible to use from other libraries which need a js+jvm html/dom builder

View file

@ -1,4 +1,6 @@
ThisBuild / scalaVersion := "3.5.0" ThisBuild / scalaVersion := "3.5.0"
ThisBuild / versionScheme := Some("semver-spec")
ThisBuild / publishMavenStyle := true ThisBuild / publishMavenStyle := true
ThisBuild / publishTo := Some( ThisBuild / publishTo := Some(
"GitHub Package Registry" at "https://maven.pkg.github.com/davidon-top/sfs" "GitHub Package Registry" at "https://maven.pkg.github.com/davidon-top/sfs"
@ -9,14 +11,16 @@ ThisBuild / credentials += Credentials(
sys.env("THEHUB_USERNAME"), sys.env("THEHUB_USERNAME"),
sys.env("THEHUB_TOKEN") sys.env("THEHUB_TOKEN")
) )
ThisBuild / versionScheme := Some("early-semver")
ThisBuild / licenses += ("MIT", url("https://opensource.org/license/MIT")) ThisBuild / licenses += ("MIT", url("https://opensource.org/license/MIT"))
ThisBuild / scmInfo := Some( ThisBuild / scmInfo := Some(
ScmInfo( ScmInfo(
url("https://github.com/davidon-top/sfs"), url("https://github.com/davidon-top/sfs"),
"scm:https://github.com/davidon-top/sfs.git" "scm:https://github.com/davidon-top/sfs.git"
) )
) )
ThisBuild / organization := "top.davidon.sfs" ThisBuild / organization := "top.davidon.sfs"
ThisBuild / organizationName := "DavidOnTop" ThisBuild / organizationName := "DavidOnTop"
ThisBuild / organizationHomepage := Some(url("https://davidon.top")) ThisBuild / organizationHomepage := Some(url("https://davidon.top"))
@ -32,7 +36,7 @@ lazy val dom = crossProject(JSPlatform, JVMPlatform)
.in(file("./dom")) .in(file("./dom"))
.settings( .settings(
name := "sfs-dom", name := "sfs-dom",
version := "0.1.0-SNAPSHOT" version := "0.1.0-alpha"
) )
.jvmSettings( .jvmSettings(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
@ -50,6 +54,14 @@ lazy val sfs = crossProject(JSPlatform, JVMPlatform)
.in(file("./sfs")) .in(file("./sfs"))
.settings( .settings(
name := "sfs", name := "sfs",
version := "0.1.0-SNAPSHOT" version := "0.1.0-alpha"
) )
.dependsOn(dom) .dependsOn(dom)
lazy val sfsReScala = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("./reactive/rescala"))
.settings(
name := "sfs-rescala",
version := "0.1.0-alpha"
)

View file

@ -1,5 +1,6 @@
package top.davidon.sfs.dom package top.davidon.sfs.dom
import top.davidon.sfs.dom.codecs.StringCodec
import top.davidon.sfs.dom.tags.Tag import top.davidon.sfs.dom.tags.Tag
/** tag + modifiers + value */ /** tag + modifiers + value */

View file

@ -1,6 +1,9 @@
package top.davidon.sfs.dom package top.davidon.sfs.dom
import top.davidon.sfs.dom.Value import top.davidon.sfs.dom.codecs.Codec
import top.davidon.sfs.dom.keys.Key import top.davidon.sfs.dom.keys.Key
class Modifier[F, T](val key: Key, val value: Value[F, T]) {} class Modifier[F, T](
val key: Key,
val value: Value[F, T]
) {}

View file

@ -1,3 +0,0 @@
package top.davidon.sfs.dom
object SFS extends ScalaFullStack {}

View file

@ -7,7 +7,7 @@ import top.davidon.sfs.dom.defs.eventProps.GlobalEventProps
import top.davidon.sfs.dom.defs.props.HtmlProps import top.davidon.sfs.dom.defs.props.HtmlProps
import top.davidon.sfs.dom.defs.tags.{HtmlTags, SvgTags} import top.davidon.sfs.dom.defs.tags.{HtmlTags, SvgTags}
trait ScalaFullStack trait ScalaFullStackDOM
extends HtmlTags extends HtmlTags
with HtmlAttrs with HtmlAttrs
with HtmlProps with HtmlProps

View file

@ -4,16 +4,22 @@ import top.davidon.sfs.dom.codecs.{Codec, StringAsIsCodec}
class Value[F, T]( class Value[F, T](
val value: F, val value: F,
val codec: Codec[F, T], val codec: Codec[F, T]
var isReactive: Boolean = false
) { ) {
def apply(): T = { def apply(): T = {
codec.encode(value) codec.encode(value)
} }
def reactive(value: Boolean = true): Value[F, T] = { override def toString: String = {
isReactive = value value match
this case v: Int => codecs.IntAsStringCodec.encode(v)
case v: Long => codecs.LongAsStringCodec.encode(v)
case v: Double => codecs.DoubleAsStringCodec.encode(v)
case v: Boolean => codecs.BooleanAsTrueFalseStringCodec.encode(v)
case v: Iterable[String] =>
codecs.IterableAsSpaceSeparatedStringCodec.encode(v)
case _ =>
throw Exception("Couldn't find codec to convert a value to a string")
} }
} }
@ -22,9 +28,8 @@ object Value {
iterator: Iterable[Value[?, String]] iterator: Iterable[Value[?, String]]
): Value[String, String] = { ): Value[String, String] = {
Value( Value(
iterator.map(v => v.codec.encode(v.value)).mkString(""), iterator.map(v => v.toString).mkString(""),
StringAsIsCodec, StringAsIsCodec
iterator.exists(_.isReactive)
) )
} }

View file

@ -0,0 +1,7 @@
package top.davidon.sfs.dom.codecs
class AsIsCodec[T](val strCodec: StringCodec[T]) extends Codec[T, T] {
override def encode(scalaValue: T): T = scalaValue
override def decode(domValue: T): T = domValue
}

View file

@ -0,0 +1,4 @@
package top.davidon.sfs.dom.codecs
//trait StringCodec[T] extends Codec[T, String] {}
type StringCodec[T] = Codec[T, String]

View file

@ -2,7 +2,8 @@ package top.davidon.sfs.dom
package object codecs { package object codecs {
lazy val IntAsStringCodec: Codec[Int, String] = new Codec[Int, String] { lazy val IntAsStringCodec: StringCodec[Int] =
new StringCodec[Int] {
override def decode(domValue: String): Int = override def decode(domValue: String): Int =
domValue.toInt // @TODO this can throw exception. How do we handle this? domValue.toInt // @TODO this can throw exception. How do we handle this?
@ -10,20 +11,17 @@ package object codecs {
override def encode(scalaValue: Int): String = scalaValue.toString override def encode(scalaValue: Int): String = scalaValue.toString
} }
lazy val DoubleAsIsCodec: Codec[Double, Double] = AsIsCodec() lazy val DoubleAsStringCodec: StringCodec[Double] =
new StringCodec[Double] {
lazy val DoubleAsStringCodec: Codec[Double, String] =
new Codec[Double, String] {
override def decode(domValue: String): Double = override def decode(domValue: String): Double =
domValue.toDouble // @TODO this can throw exception. How do we handle this? domValue.toDouble // @TODO this can throw exception. How do we handle this?
override def encode(scalaValue: Double): String = scalaValue.toString override def encode(scalaValue: Double): String = scalaValue.toString
} }
lazy val LongAsIsCodec: Codec[Long, Long] = AsIsCodec()
lazy val LongAsStringCodec: Codec[Long, String] = lazy val LongAsStringCodec: StringCodec[Long] =
new Codec[Long, String] { new StringCodec[Long] {
override def decode(domValue: String): Long = override def decode(domValue: String): Long =
domValue.toLong // @TODO this can throw exception. How do we handle this? domValue.toLong // @TODO this can throw exception. How do we handle this?
@ -31,8 +29,8 @@ package object codecs {
override def encode(scalaValue: Long): String = scalaValue.toString override def encode(scalaValue: Long): String = scalaValue.toString
} }
lazy val BooleanAsTrueFalseStringCodec: Codec[Boolean, String] = lazy val BooleanAsTrueFalseStringCodec: StringCodec[Boolean] =
new Codec[Boolean, String] { new StringCodec[Boolean] {
override def decode(domValue: String): Boolean = domValue == "true" override def decode(domValue: String): Boolean = domValue == "true"
@ -40,16 +38,16 @@ package object codecs {
if scalaValue then "true" else "false" if scalaValue then "true" else "false"
} }
lazy val BooleanAsYesNoStringCodec: Codec[Boolean, String] = lazy val BooleanAsYesNoStringCodec: StringCodec[Boolean] =
new Codec[Boolean, String] { new StringCodec[Boolean] {
override def decode(domValue: String): Boolean = domValue == "yes" override def decode(domValue: String): Boolean = domValue == "yes"
override def encode(scalaValue: Boolean): String = override def encode(scalaValue: Boolean): String =
if scalaValue then "yes" else "no" if scalaValue then "yes" else "no"
} }
lazy val BooleanAsOnOffStringCodec: Codec[Boolean, String] = lazy val BooleanAsOnOffStringCodec: StringCodec[Boolean] =
new Codec[Boolean, String] { new StringCodec[Boolean] {
override def decode(domValue: String): Boolean = domValue == "on" override def decode(domValue: String): Boolean = domValue == "on"
@ -57,9 +55,8 @@ package object codecs {
if scalaValue then "on" else "off" if scalaValue then "on" else "off"
} }
lazy val IterableAsSpaceSeparatedStringCodec lazy val IterableAsSpaceSeparatedStringCodec: StringCodec[Iterable[String]] =
: Codec[Iterable[String], String] = new StringCodec[Iterable[String]] { // could use for e.g. className
new Codec[Iterable[String], String] { // could use for e.g. className
override def decode(domValue: String): Iterable[String] = override def decode(domValue: String): Iterable[String] =
if domValue == "" then Nil else domValue.split(' ') if domValue == "" then Nil else domValue.split(' ')
@ -67,9 +64,8 @@ package object codecs {
override def encode(scalaValue: Iterable[String]): String = override def encode(scalaValue: Iterable[String]): String =
scalaValue.mkString(" ") scalaValue.mkString(" ")
} }
lazy val IterableAsCommaSeparatedStringCodec lazy val IterableAsCommaSeparatedStringCodec: StringCodec[Iterable[String]] =
: Codec[Iterable[String], String] = new StringCodec[Iterable[String]] { // could use for lists of IDs
new Codec[Iterable[String], String] { // could use for lists of IDs
override def decode(domValue: String): Iterable[String] = override def decode(domValue: String): Iterable[String] =
if domValue == "" then Nil else domValue.split(',') if domValue == "" then Nil else domValue.split(',')
@ -77,12 +73,8 @@ package object codecs {
override def encode(scalaValue: Iterable[String]): String = override def encode(scalaValue: Iterable[String]): String =
scalaValue.mkString(",") scalaValue.mkString(",")
} }
val StringAsIsCodec: Codec[String, String] = AsIsCodec() lazy val BooleanAsAttrPresenceCodec: StringCodec[Boolean] =
val IntAsIsCodec: Codec[Int, Int] = AsIsCodec() new StringCodec[Boolean] {
val BooleanAsIsCodec: Codec[Boolean, Boolean] = AsIsCodec()
val BooleanAsAttrPresenceCodec: Codec[Boolean, String] =
new Codec[Boolean, String] {
override def decode(domValue: String): Boolean = domValue != null override def decode(domValue: String): Boolean = domValue != null
@ -90,9 +82,12 @@ package object codecs {
if scalaValue then "" else null if scalaValue then "" else null
} }
def AsIsCodec[V](): Codec[V, V] = new Codec[V, V] { lazy val LongAsIsCodec: AsIsCodec[Long] = AsIsCodec(LongAsStringCodec)
override def encode(scalaValue: V): V = scalaValue lazy val DoubleAsIsCodec: AsIsCodec[Double] = AsIsCodec(DoubleAsStringCodec)
lazy val StringAsIsCodec: AsIsCodec[String] & StringCodec[String] =
override def decode(domValue: V): V = domValue new AsIsCodec[String](StringAsIsCodec) with StringCodec[String] {}
} lazy val IntAsIsCodec: AsIsCodec[Int] = AsIsCodec(IntAsStringCodec)
lazy val BooleanAsIsCodec: AsIsCodec[Boolean] = AsIsCodec(
BooleanAsTrueFalseStringCodec
)
} }

View file

@ -1,10 +1,10 @@
package top.davidon.sfs.dom.keys package top.davidon.sfs.dom.keys
import top.davidon.sfs.dom.codecs.Codec import top.davidon.sfs.dom.codecs.{Codec, StringCodec}
import top.davidon.sfs.dom.{Modifier, Value} import top.davidon.sfs.dom.{Modifier, Value}
class AriaAttr[V]( class AriaAttr[V](
suffix: String, suffix: String,
val codec: Codec[V, String] val codec: StringCodec[V]
) extends Key { ) extends Key {
override val name: String = "aria-" + suffix override val name: String = "aria-" + suffix

View file

@ -1,11 +1,11 @@
package top.davidon.sfs.dom.keys package top.davidon.sfs.dom.keys
import top.davidon.sfs.dom.codecs.Codec import top.davidon.sfs.dom.codecs.{Codec, StringCodec}
import top.davidon.sfs.dom.{Modifier, Value} import top.davidon.sfs.dom.{Modifier, Value}
class HtmlAttr[V]( class HtmlAttr[V](
override val name: String, override val name: String,
val codec: Codec[V, String] val codec: StringCodec[V]
) extends Key { ) extends Key {
@inline def apply(value: V): Modifier[V, String] = { @inline def apply(value: V): Modifier[V, String] = {
this := value this := value

View file

@ -1,10 +1,10 @@
package top.davidon.sfs.dom.keys package top.davidon.sfs.dom.keys
import top.davidon.sfs.dom.codecs.Codec import top.davidon.sfs.dom.codecs.{Codec, StringCodec}
import top.davidon.sfs.dom.{Modifier, Value} import top.davidon.sfs.dom.{Modifier, Value}
class SvgAttr[V]( class SvgAttr[V](
val localName: String, val localName: String,
val codec: Codec[V, String], val codec: StringCodec[V],
val namespacePrefix: Option[String] val namespacePrefix: Option[String]
) extends Key { ) extends Key {
override val name: String = override val name: String =

View file

@ -1,6 +1,7 @@
package top.davidon.sfs.dom.tags package top.davidon.sfs.dom.tags
import org.scalajs.dom import org.scalajs.dom
import top.davidon.sfs.dom.codecs.StringCodec
import top.davidon.sfs.dom.{Element, Modifier, Value} import top.davidon.sfs.dom.{Element, Modifier, Value}
trait Tag[+Ref <: dom.Element] { trait Tag[+Ref <: dom.Element] {

View file

@ -0,0 +1,47 @@
package top.davidon.sfs.renderers
import org.scalajs.dom
import top.davidon.sfs.dom.keys.{EventProp, HtmlProp}
import top.davidon.sfs.dom.{Element, Renderer, Value}
import scala.scalajs.js
class ClientSideRenderer(val root: dom.Element)
extends Renderer[Unit]
with ReactiveRenderer {
override def render(
elements: Element[dom.Element]*
): Unit = {
elements.foreach(renderElement(root, _))
}
override def valueFunc[F](element: dom.Element, value: F): Unit = {}
override def modifierFunc[F, T](
modifier: top.davidon.sfs.dom.Modifier[F, T],
value: F
): Unit = {}
private def renderElement(
parent: dom.Element,
element: Element[dom.Element]
): Unit = {
val el = dom.document.createElement(element.tag.name)
element.mods.foreach(m =>
m.key match {
case _: HtmlProp[?, ?] =>
el.asInstanceOf[js.Dynamic]
.updateDynamic(m.key.name)(m.value().asInstanceOf[js.Any])
case _: EventProp[?] => ???
case _ =>
el.setAttribute(m.key.name, m.value().asInstanceOf[String])
}
)
element.children.foreach {
case e: Element[dom.Element] => renderElement(el, e)
case s: Value[?, String] => el.append(s())
}
parent.append(el)
}
}

View file

@ -0,0 +1,4 @@
package top.davidon.sfs
import top.davidon.sfs.dom.ScalaFullStackDOM
object SFS extends ScalaFullStackDOM {}

View file

@ -0,0 +1,9 @@
package top.davidon.sfs.renderers
import org.scalajs.dom
import top.davidon.sfs.dom.Modifier
trait ReactiveRenderer {
def valueFunc[F](element: dom.Element, value: F): Unit
def modifierFunc[F, T](modifier: Modifier[F, T], value: F): Unit
}

View file

@ -1,10 +1,9 @@
package top.davidon.sfs.renderers package top.davidon.sfs.renderers
import org.scalajs.dom import org.scalajs.dom
import top.davidon.sfs.dom.SFS.given
import top.davidon.sfs.dom.{Element, Renderer, Value} import top.davidon.sfs.dom.{Element, Renderer, Value}
class StringRenderer extends Renderer[String] { class StringRenderer(val ssr: Boolean) extends Renderer[String] {
override def render( override def render(
elements: Element[dom.Element]* elements: Element[dom.Element]*
): String = { ): String = {
@ -13,17 +12,13 @@ class StringRenderer extends Renderer[String] {
private def renderElement(e: Element[dom.Element]): String = { private def renderElement(e: Element[dom.Element]): String = {
val modsStr = val modsStr =
e.mods.map(m => s" ${m.key.name}=\"${m.value()}\"").mkString("") e.mods.map(m => s" ${m.key.name}=\"${m.value.toString}\"").mkString("")
val bodyStr = e.children val bodyStr = e.children
.map { .map {
case e: Element[?] => case e: Element[?] =>
renderElement(e) renderElement(e)
case c: Value[?, String] => case c: Value[?, String] =>
c() c()
case _ =>
throw Exception(
"attempted to parse child that was neither an Element or Value, this should never happen if it did its a bug"
)
} }
.mkString(" ") .mkString(" ")
s"<${e.tag.name}$modsStr>$bodyStr${ s"<${e.tag.name}$modsStr>$bodyStr${