Skip to content

Commit

Permalink
Rethinking Html rendering (SSR)
Browse files Browse the repository at this point in the history
  • Loading branch information
davesmith00000 committed Dec 18, 2023
1 parent bc45b0c commit cffee68
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 184 deletions.
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
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
4 changes: 4 additions & 0 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 Down
80 changes: 0 additions & 80 deletions tyrian/shared/src/main/scala/tyrian/runtime/TyrianSSR.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package tyrian.runtime
package tyrian

import tyrian.Html.*
import tyrian.*

class TyrianSSRTests extends munit.FunSuite {
class HTMLRenderingTests extends munit.FunSuite {

type Model = Int
type Msg = Unit
Expand All @@ -15,7 +14,7 @@ class TyrianSSRTests extends munit.FunSuite {
_ => p(text("Hello, world!"))

val actual =
TyrianSSR.render(model, view)
view(model).render

val expected =
"<p>Hello, world!</p>"
Expand All @@ -28,7 +27,7 @@ class TyrianSSRTests extends munit.FunSuite {
_ => p(text("Hello, world!"))

val actual =
TyrianSSR.render(true, model, view)
"<!DOCTYPE HTML>" + view(model)

val expected =
"<!DOCTYPE HTML><p>Hello, world!</p>"
Expand All @@ -45,7 +44,7 @@ class TyrianSSRTests extends munit.FunSuite {
)

val actual =
TyrianSSR.render(html)
html.render

val expected =
"""<div id="my-div"><p>Hello, world!</p><span class="my-span-class" style="width:10px;height:12pt;">test</span><a href="http://tyrian">my link</a></div>"""
Expand All @@ -62,7 +61,7 @@ class TyrianSSRTests extends munit.FunSuite {
)

val actual =
TyrianSSR.render(elems)
elems.render

val expected =
"<p>a</p><span>b</span><b>c</b>"
Expand All @@ -77,7 +76,7 @@ class TyrianSSRTests extends munit.FunSuite {
)

val actual =
TyrianSSR.render(elems)
elems.render

val expected =
"""<div id="my-div" hidden>some content</div>"""
Expand All @@ -92,7 +91,7 @@ class TyrianSSRTests extends munit.FunSuite {
)

val actual =
TyrianSSR.render(elems)
elems.render

val expected =
"""<div id="my-div">some content</div>"""
Expand All @@ -101,7 +100,7 @@ class TyrianSSRTests extends munit.FunSuite {
}

test("Can render a simple page") {
val elems: Elem[Msg] =
val page: Html[Msg] =
html(
head(
title("My Page")
Expand All @@ -112,7 +111,7 @@ class TyrianSSRTests extends munit.FunSuite {
)

val actual =
TyrianSSR.render(true, elems)
"<!DOCTYPE HTML>" + page

val expected =
"<!DOCTYPE HTML><html><head><title>My Page</title></head><body><p>Hello, world!</p></body></html>"
Expand All @@ -138,7 +137,7 @@ class TyrianSSRTests extends munit.FunSuite {
)

val actual =
TyrianSSR.render(elems)
elems.render

val expected =
"""<div><button></button><button class="a"></button><button class="a"></button><button class="a"></button><button class="a">X</button><button class="a">X</button><button class="a">X</button><button class="a">X</button><button class="a">X</button><button>X</button><button>X</button><button>X</button></div>"""
Expand Down

0 comments on commit cffee68

Please sign in to comment.