diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0bc4dc --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/build.sbt b/build.sbt index 972e0b1..d2d5aca 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,6 @@ ThisBuild / scalaVersion := "3.5.0" +ThisBuild / versionScheme := Some("semver-spec") + ThisBuild / publishMavenStyle := true ThisBuild / publishTo := Some( "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_TOKEN") ) -ThisBuild / versionScheme := Some("early-semver") + ThisBuild / licenses += ("MIT", url("https://opensource.org/license/MIT")) + ThisBuild / scmInfo := Some( ScmInfo( url("https://github.com/davidon-top/sfs"), "scm:https://github.com/davidon-top/sfs.git" ) ) + ThisBuild / organization := "top.davidon.sfs" ThisBuild / organizationName := "DavidOnTop" ThisBuild / organizationHomepage := Some(url("https://davidon.top")) @@ -32,7 +36,7 @@ lazy val dom = crossProject(JSPlatform, JVMPlatform) .in(file("./dom")) .settings( name := "sfs-dom", - version := "0.1.0-SNAPSHOT" + version := "0.1.0-alpha" ) .jvmSettings( libraryDependencies ++= Seq( @@ -50,6 +54,14 @@ lazy val sfs = crossProject(JSPlatform, JVMPlatform) .in(file("./sfs")) .settings( name := "sfs", - version := "0.1.0-SNAPSHOT" + version := "0.1.0-alpha" ) .dependsOn(dom) + +lazy val sfsReScala = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Pure) + .in(file("./reactive/rescala")) + .settings( + name := "sfs-rescala", + version := "0.1.0-alpha" + ) diff --git a/dom/src/main/scala/top/davidon/sfs/dom/Element.scala b/dom/src/main/scala/top/davidon/sfs/dom/Element.scala index 152866d..ec7c55c 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/Element.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/Element.scala @@ -1,5 +1,6 @@ package top.davidon.sfs.dom +import top.davidon.sfs.dom.codecs.StringCodec import top.davidon.sfs.dom.tags.Tag /** tag + modifiers + value */ diff --git a/dom/src/main/scala/top/davidon/sfs/dom/Modifier.scala b/dom/src/main/scala/top/davidon/sfs/dom/Modifier.scala index e794a3b..3673a63 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/Modifier.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/Modifier.scala @@ -1,6 +1,9 @@ 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 -class Modifier[F, T](val key: Key, val value: Value[F, T]) {} +class Modifier[F, T]( + val key: Key, + val value: Value[F, T] +) {} diff --git a/dom/src/main/scala/top/davidon/sfs/dom/SFS.scala b/dom/src/main/scala/top/davidon/sfs/dom/SFS.scala deleted file mode 100644 index 9e7d752..0000000 --- a/dom/src/main/scala/top/davidon/sfs/dom/SFS.scala +++ /dev/null @@ -1,3 +0,0 @@ -package top.davidon.sfs.dom - -object SFS extends ScalaFullStack {} diff --git a/dom/src/main/scala/top/davidon/sfs/dom/ScalaFullStack.scala b/dom/src/main/scala/top/davidon/sfs/dom/ScalaFullStackDOM.scala similarity index 98% rename from dom/src/main/scala/top/davidon/sfs/dom/ScalaFullStack.scala rename to dom/src/main/scala/top/davidon/sfs/dom/ScalaFullStackDOM.scala index db71c2c..11fa3ca 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/ScalaFullStack.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/ScalaFullStackDOM.scala @@ -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.tags.{HtmlTags, SvgTags} -trait ScalaFullStack +trait ScalaFullStackDOM extends HtmlTags with HtmlAttrs with HtmlProps diff --git a/dom/src/main/scala/top/davidon/sfs/dom/Value.scala b/dom/src/main/scala/top/davidon/sfs/dom/Value.scala index 78c1efd..9c7f192 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/Value.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/Value.scala @@ -4,16 +4,22 @@ import top.davidon.sfs.dom.codecs.{Codec, StringAsIsCodec} class Value[F, T]( val value: F, - val codec: Codec[F, T], - var isReactive: Boolean = false + val codec: Codec[F, T] ) { def apply(): T = { codec.encode(value) } - def reactive(value: Boolean = true): Value[F, T] = { - isReactive = value - this + override def toString: String = { + value match + 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]] ): Value[String, String] = { Value( - iterator.map(v => v.codec.encode(v.value)).mkString(""), - StringAsIsCodec, - iterator.exists(_.isReactive) + iterator.map(v => v.toString).mkString(""), + StringAsIsCodec ) } diff --git a/dom/src/main/scala/top/davidon/sfs/dom/codecs/AsIsCodec.scala b/dom/src/main/scala/top/davidon/sfs/dom/codecs/AsIsCodec.scala new file mode 100644 index 0000000..0403e80 --- /dev/null +++ b/dom/src/main/scala/top/davidon/sfs/dom/codecs/AsIsCodec.scala @@ -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 +} diff --git a/dom/src/main/scala/top/davidon/sfs/dom/codecs/Codecs.scala b/dom/src/main/scala/top/davidon/sfs/dom/codecs/Codec.scala similarity index 100% rename from dom/src/main/scala/top/davidon/sfs/dom/codecs/Codecs.scala rename to dom/src/main/scala/top/davidon/sfs/dom/codecs/Codec.scala diff --git a/dom/src/main/scala/top/davidon/sfs/dom/codecs/StringCodec.scala b/dom/src/main/scala/top/davidon/sfs/dom/codecs/StringCodec.scala new file mode 100644 index 0000000..9ce2454 --- /dev/null +++ b/dom/src/main/scala/top/davidon/sfs/dom/codecs/StringCodec.scala @@ -0,0 +1,4 @@ +package top.davidon.sfs.dom.codecs + +//trait StringCodec[T] extends Codec[T, String] {} +type StringCodec[T] = Codec[T, String] diff --git a/dom/src/main/scala/top/davidon/sfs/dom/codecs/package.scala b/dom/src/main/scala/top/davidon/sfs/dom/codecs/package.scala index 3b04e92..fc14f03 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/codecs/package.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/codecs/package.scala @@ -2,28 +2,26 @@ package top.davidon.sfs.dom 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 = - domValue.toInt // @TODO this can throw exception. How do we handle this? + override def decode(domValue: String): Int = + domValue.toInt // @TODO this can throw exception. How do we handle this? - 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: Codec[Double, String] = - new Codec[Double, String] { + lazy val DoubleAsStringCodec: StringCodec[Double] = + new StringCodec[Double] { override def decode(domValue: String): Double = domValue.toDouble // @TODO this can throw exception. How do we handle this? override def encode(scalaValue: Double): String = scalaValue.toString } - lazy val LongAsIsCodec: Codec[Long, Long] = AsIsCodec() - lazy val LongAsStringCodec: Codec[Long, String] = - new Codec[Long, String] { + lazy val LongAsStringCodec: StringCodec[Long] = + new StringCodec[Long] { override def decode(domValue: String): Long = 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 } - lazy val BooleanAsTrueFalseStringCodec: Codec[Boolean, String] = - new Codec[Boolean, String] { + lazy val BooleanAsTrueFalseStringCodec: StringCodec[Boolean] = + new StringCodec[Boolean] { override def decode(domValue: String): Boolean = domValue == "true" @@ -40,16 +38,16 @@ package object codecs { if scalaValue then "true" else "false" } - lazy val BooleanAsYesNoStringCodec: Codec[Boolean, String] = - new Codec[Boolean, String] { + lazy val BooleanAsYesNoStringCodec: StringCodec[Boolean] = + new StringCodec[Boolean] { override def decode(domValue: String): Boolean = domValue == "yes" override def encode(scalaValue: Boolean): String = if scalaValue then "yes" else "no" } - lazy val BooleanAsOnOffStringCodec: Codec[Boolean, String] = - new Codec[Boolean, String] { + lazy val BooleanAsOnOffStringCodec: StringCodec[Boolean] = + new StringCodec[Boolean] { override def decode(domValue: String): Boolean = domValue == "on" @@ -57,9 +55,8 @@ package object codecs { if scalaValue then "on" else "off" } - lazy val IterableAsSpaceSeparatedStringCodec - : Codec[Iterable[String], String] = - new Codec[Iterable[String], String] { // could use for e.g. className + lazy val IterableAsSpaceSeparatedStringCodec: StringCodec[Iterable[String]] = + new StringCodec[Iterable[String]] { // could use for e.g. className override def decode(domValue: String): Iterable[String] = if domValue == "" then Nil else domValue.split(' ') @@ -67,9 +64,8 @@ package object codecs { override def encode(scalaValue: Iterable[String]): String = scalaValue.mkString(" ") } - lazy val IterableAsCommaSeparatedStringCodec - : Codec[Iterable[String], String] = - new Codec[Iterable[String], String] { // could use for lists of IDs + lazy val IterableAsCommaSeparatedStringCodec: StringCodec[Iterable[String]] = + new StringCodec[Iterable[String]] { // could use for lists of IDs override def decode(domValue: String): Iterable[String] = if domValue == "" then Nil else domValue.split(',') @@ -77,12 +73,8 @@ package object codecs { override def encode(scalaValue: Iterable[String]): String = scalaValue.mkString(",") } - val StringAsIsCodec: Codec[String, String] = AsIsCodec() - val IntAsIsCodec: Codec[Int, Int] = AsIsCodec() - val BooleanAsIsCodec: Codec[Boolean, Boolean] = AsIsCodec() - - val BooleanAsAttrPresenceCodec: Codec[Boolean, String] = - new Codec[Boolean, String] { + lazy val BooleanAsAttrPresenceCodec: StringCodec[Boolean] = + new StringCodec[Boolean] { override def decode(domValue: String): Boolean = domValue != null @@ -90,9 +82,12 @@ package object codecs { if scalaValue then "" else null } - def AsIsCodec[V](): Codec[V, V] = new Codec[V, V] { - override def encode(scalaValue: V): V = scalaValue - - override def decode(domValue: V): V = domValue - } + lazy val LongAsIsCodec: AsIsCodec[Long] = AsIsCodec(LongAsStringCodec) + lazy val DoubleAsIsCodec: AsIsCodec[Double] = AsIsCodec(DoubleAsStringCodec) + lazy val StringAsIsCodec: AsIsCodec[String] & StringCodec[String] = + new AsIsCodec[String](StringAsIsCodec) with StringCodec[String] {} + lazy val IntAsIsCodec: AsIsCodec[Int] = AsIsCodec(IntAsStringCodec) + lazy val BooleanAsIsCodec: AsIsCodec[Boolean] = AsIsCodec( + BooleanAsTrueFalseStringCodec + ) } diff --git a/dom/src/main/scala/top/davidon/sfs/dom/keys/AriaAttr.scala b/dom/src/main/scala/top/davidon/sfs/dom/keys/AriaAttr.scala index 256c033..ff0505f 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/keys/AriaAttr.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/keys/AriaAttr.scala @@ -1,10 +1,10 @@ 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} class AriaAttr[V]( suffix: String, - val codec: Codec[V, String] + val codec: StringCodec[V] ) extends Key { override val name: String = "aria-" + suffix diff --git a/dom/src/main/scala/top/davidon/sfs/dom/keys/HtmlAttr.scala b/dom/src/main/scala/top/davidon/sfs/dom/keys/HtmlAttr.scala index c89956f..e754f78 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/keys/HtmlAttr.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/keys/HtmlAttr.scala @@ -1,11 +1,11 @@ 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} class HtmlAttr[V]( override val name: String, - val codec: Codec[V, String] + val codec: StringCodec[V] ) extends Key { @inline def apply(value: V): Modifier[V, String] = { this := value diff --git a/dom/src/main/scala/top/davidon/sfs/dom/keys/SvgAttr.scala b/dom/src/main/scala/top/davidon/sfs/dom/keys/SvgAttr.scala index d0d1e58..9f6719b 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/keys/SvgAttr.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/keys/SvgAttr.scala @@ -1,10 +1,10 @@ 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} class SvgAttr[V]( val localName: String, - val codec: Codec[V, String], + val codec: StringCodec[V], val namespacePrefix: Option[String] ) extends Key { override val name: String = diff --git a/dom/src/main/scala/top/davidon/sfs/dom/tags/Tag.scala b/dom/src/main/scala/top/davidon/sfs/dom/tags/Tag.scala index 1b15248..c53333a 100644 --- a/dom/src/main/scala/top/davidon/sfs/dom/tags/Tag.scala +++ b/dom/src/main/scala/top/davidon/sfs/dom/tags/Tag.scala @@ -1,6 +1,7 @@ package top.davidon.sfs.dom.tags import org.scalajs.dom +import top.davidon.sfs.dom.codecs.StringCodec import top.davidon.sfs.dom.{Element, Modifier, Value} trait Tag[+Ref <: dom.Element] { diff --git a/sfs/.js/src/main/scala/top/davidon/sfs/renderers/ClientSideRenderer.scala b/sfs/.js/src/main/scala/top/davidon/sfs/renderers/ClientSideRenderer.scala new file mode 100644 index 0000000..f462c47 --- /dev/null +++ b/sfs/.js/src/main/scala/top/davidon/sfs/renderers/ClientSideRenderer.scala @@ -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) + } +} diff --git a/sfs/src/main/scala/top/davidon/sfs/SFS.scala b/sfs/src/main/scala/top/davidon/sfs/SFS.scala new file mode 100644 index 0000000..f5d62fe --- /dev/null +++ b/sfs/src/main/scala/top/davidon/sfs/SFS.scala @@ -0,0 +1,4 @@ +package top.davidon.sfs +import top.davidon.sfs.dom.ScalaFullStackDOM + +object SFS extends ScalaFullStackDOM {} diff --git a/sfs/src/main/scala/top/davidon/sfs/renderers/ReactiveRenderer.scala b/sfs/src/main/scala/top/davidon/sfs/renderers/ReactiveRenderer.scala new file mode 100644 index 0000000..51c8df9 --- /dev/null +++ b/sfs/src/main/scala/top/davidon/sfs/renderers/ReactiveRenderer.scala @@ -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 +} diff --git a/sfs/src/main/scala/top/davidon/sfs/renderers/StringRenderer.scala b/sfs/src/main/scala/top/davidon/sfs/renderers/StringRenderer.scala index feb75ca..658ae9e 100644 --- a/sfs/src/main/scala/top/davidon/sfs/renderers/StringRenderer.scala +++ b/sfs/src/main/scala/top/davidon/sfs/renderers/StringRenderer.scala @@ -1,10 +1,9 @@ package top.davidon.sfs.renderers import org.scalajs.dom -import top.davidon.sfs.dom.SFS.given import top.davidon.sfs.dom.{Element, Renderer, Value} -class StringRenderer extends Renderer[String] { +class StringRenderer(val ssr: Boolean) extends Renderer[String] { override def render( elements: Element[dom.Element]* ): String = { @@ -13,17 +12,13 @@ class StringRenderer extends Renderer[String] { private def renderElement(e: Element[dom.Element]): String = { 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 .map { case e: Element[?] => renderElement(e) case c: Value[?, String] => 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(" ") s"<${e.tag.name}$modsStr>$bodyStr${