Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rethinking Html Rendering, a la SSR #235

Merged
merged 3 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion indigo-sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"build": "parcel build index.html --dist-dir dist --log-level info"
},
"devDependencies": {
"parcel": "^2.4.0",
"parcel": "^2.10.3",
"parcel-reporter-static-files-copy": "^1.3.4",
"process": "^0.11.10"
},
Expand Down
1,394 changes: 867 additions & 527 deletions indigo-sandbox/yarn.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"build": "parcel build index.html --dist-dir dist --log-level info"
},
"devDependencies": {
"parcel": "^2.4.0",
"parcel": "^2.10.3",
"process": "^0.11.10"
},
"dependencies": {
Expand Down
6 changes: 6 additions & 0 deletions sandbox/src/main/scala/example/Sandbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ object Sandbox extends TyrianApp[Msg, Model]:
if true then p("Showing") else Empty,
if false then p("Showing") else Empty
),
div(
p("From rendered HTML:"),
raw("div")(
"<p>Hello, Bob!</p>" + p("Hello, world!")
)
),
div(
input(id := "fruitName", onInput(s => Msg.UpdateFruitInput(s))),
button(onClick(Msg.AddFruit))(
Expand Down
1,490 changes: 910 additions & 580 deletions sandbox/yarn.lock

Large diffs are not rendered by default.

41 changes: 0 additions & 41 deletions tyrian/js/src/main/scala/tyrian/Tyrian.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package tyrian
import cats.effect.kernel.Async
import org.scalajs.dom.Element
import tyrian.runtime.TyrianRuntime
import tyrian.runtime.TyrianSSR

object Tyrian:

Expand Down Expand Up @@ -43,43 +42,3 @@ object Tyrian:
subscriptions: Model => Sub[F, Msg]
): F[Nothing] =
TyrianRuntime[F, Model, Msg](router, node, init._1, init._2, update, view, subscriptions)

/** Takes a normal Tyrian Model and view function and renders the html to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, model: Model, view: Model => Html[Msg]): String =
TyrianSSR.render(includeDocType, model, view)

/** Takes a normal Tyrian Model and view function and renders the html to a string.
*/
def render[Model, Msg](model: Model, view: Model => Html[Msg]): String =
render(false, model, view)

/** Takes a Tyrian HTML view, and renders it into to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, html: Html[Msg]): String =
TyrianSSR.render(includeDocType, html)

/** Takes a Tyrian HTML view, and renders it into to a string.
*/
def render[Model, Msg](html: Html[Msg]): String =
render(false, html)

/** Takes a list of Tyrian elements, and renders the fragment into to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, elems: List[Elem[Msg]]): String =
TyrianSSR.render(includeDocType, elems)

/** Takes a list of Tyrian elements, and renders the fragment into to a string.
*/
def render[Model, Msg](elems: List[Elem[Msg]]): String =
render(false, elems)

/** Takes repeatingTyrian elements, and renders the fragment into to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, elems: Elem[Msg]*): String =
render(includeDocType, elems.toList)

/** Takes repeating Tyrian elements, and renders the fragment into to a string.
*/
def render[Model, Msg](elems: Elem[Msg]*): String =
render(elems.toList)
42 changes: 0 additions & 42 deletions tyrian/jvm/src/main/scala/tyrian/Tyrian.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package tyrian

import tyrian.runtime.TyrianSSR

object Tyrian:

final case class FakeEvent(name: String, value: Any, target: Any)
Expand All @@ -10,43 +8,3 @@ object Tyrian:
type KeyboardEvent = FakeEvent
type MouseEvent = FakeEvent
type HTMLInputElement = FakeHTMLInputElement

/** Takes a normal Tyrian Model and view function and renders the html to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, model: Model, view: Model => Html[Msg]): String =
TyrianSSR.render(includeDocType, model, view)

/** Takes a normal Tyrian Model and view function and renders the html to a string.
*/
def render[Model, Msg](model: Model, view: Model => Html[Msg]): String =
render(false, model, view)

/** Takes a Tyrian HTML view, and renders it into to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, html: Html[Msg]): String =
TyrianSSR.render(includeDocType, html)

/** Takes a Tyrian HTML view, and renders it into to a string.
*/
def render[Model, Msg](html: Html[Msg]): String =
render(false, html)

/** Takes a list of Tyrian elements, and renders the fragment into to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, elems: List[Elem[Msg]]): String =
TyrianSSR.render(includeDocType, elems)

/** Takes a list of Tyrian elements, and renders the fragment into to a string.
*/
def render[Model, Msg](elems: List[Elem[Msg]]): String =
render(false, elems)

/** Takes repeatingTyrian elements, and renders the fragment into to a string prefixed with the doctype.
*/
def render[Model, Msg](includeDocType: Boolean, elems: Elem[Msg]*): String =
render(includeDocType, elems.toList)

/** Takes repeating Tyrian elements, and renders the fragment into to a string.
*/
def render[Model, Msg](elems: Elem[Msg]*): String =
render(elems.toList)
9 changes: 0 additions & 9 deletions tyrian/jvm/src/test/scala/tyrian/PlaceholderTests.scala

This file was deleted.

1 change: 1 addition & 0 deletions tyrian/shared/src/main/scala/tyrian/Attr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import scala.util.Try
/** HTML attribute */
sealed trait Attr[+M]:
def map[N](f: M => N): Attr[N]
override def toString(): String = this.render

/** An attribute of an HTML tag that does not exist, used as a "do not render" placeholder
*/
Expand Down
59 changes: 59 additions & 0 deletions tyrian/shared/src/main/scala/tyrian/HTMLRendering.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package tyrian

import tyrian.*

val DOCTYPE: String = "<!DOCTYPE HTML>"

private val spacer = (str: String) => if str.isEmpty then str else " " + str

extension [Msg](elem: Elem[Msg])
def render: String =
elem match
case _: Empty.type => ""
case t: Text => t.value
case h: Html[_] => h.render

extension [Msg](html: Html[Msg])
def render: String =
html match
case tag: RawTag[_] =>
val attributes =
spacer(tag.attributes.map(_.render).filterNot(_.isEmpty).mkString(" "))
s"""<${tag.name}$attributes>${tag.innerHTML}</${tag.name}>"""
case tag: Tag[_] =>
val attributes =
spacer(tag.attributes.map(_.render).filterNot(_.isEmpty).mkString(" "))

val children = tag.children.map {
case _: Empty.type => ""
case t: Text => t.value
case h: Html[_] => h.render
}.mkString

s"""<${tag.name}$attributes>$children</${tag.name}>"""

extension (a: Attr[_])
def render: String =
a match
case _: Event[_, _] => ""
case a: Attribute => a.render
case p: Property => p.render
case a: NamedAttribute => a.name
case _: EmptyAttribute.type => ""

extension (a: Attribute)
def render: String =
s"""${a.name}="${a.value}""""

extension (p: Property)
def render: String =
val asStr: String =
p.valueOf match
case x: Boolean => x.toString
case x: String => x

s"""${p.name}="${asStr}""""

extension [Msg](elems: List[Elem[Msg]])
def render: String =
elems.map(_.render).mkString
10 changes: 8 additions & 2 deletions tyrian/shared/src/main/scala/tyrian/Html.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ import scala.annotation.targetName
/** An HTML element can be a tag or a text node */
sealed trait Elem[+M]:
def map[N](f: M => N): Elem[N]
override def toString(): String = this.render

/** An Empty Node - renders nothing */
case object Empty extends Elem[Nothing]:
def map[N](f: Nothing => N): Empty.type = this
override def toString(): String = this.render

/** A text node */
final case class Text(value: String) extends Elem[Nothing]:
def map[N](f: Nothing => N): Text = this
override def toString(): String = this.render

/** Base class for HTML tags */
sealed trait Html[+M] extends Elem[M]:
def map[N](f: M => N): Html[N]
def innerHtml(html: String): Html[M]
override def toString(): String = this.render

/** Object used to provide Html syntax `import tyrian.Html.*`
*/
Expand All @@ -35,10 +39,12 @@ object Html extends HtmlTags with HtmlAttributes:
def tag[M](name: String)(attributes: List[Attr[M]])(children: List[Elem[M]]): Html[M] =
Tag(name, attributes, children)

def raw[M](name: String)(attributes: Attr[M]*)(html: String): Html[M] =
def raw[M](name: String)(html: String): Html[M] =
RawTag(name, Nil, html)
def raw[M](name: String, attributes: Attr[M]*)(html: String): Html[M] =
RawTag(name, attributes.toList, html)
@targetName("raw-list")
def raw[M](name: String)(attributes: List[Attr[M]])(html: String): Html[M] =
def raw[M](name: String, attributes: List[Attr[M]])(html: String): Html[M] =
RawTag(name, attributes, html)

// Custom tag syntax
Expand Down
80 changes: 0 additions & 80 deletions tyrian/shared/src/main/scala/tyrian/runtime/TyrianSSR.scala

This file was deleted.

Loading