Skip to content
This repository has been archived by the owner on Feb 23, 2022. It is now read-only.

Extensible DSL and Context support #5

Merged
merged 17 commits into from Aug 12, 2019
1 change: 1 addition & 0 deletions .gitignore
@@ -1 +1,2 @@
.scalafmt.conf
target/
6 changes: 0 additions & 6 deletions .scalafmt.conf

This file was deleted.

6 changes: 6 additions & 0 deletions core/.js/src/main/scala/io/taig/schelm/BrowserDom.scala
Expand Up @@ -57,6 +57,12 @@ final class BrowserDom[F[_], Event](
override def createElement(name: String): F[dom.Element] =
F.delay(document.createElement(name))

override def createElementNS(
namespace: String,
name: String
): F[dom.Element] =
F.delay(document.createElementNS(namespace, name))

override def createTextNode(value: String): F[dom.Text] =
F.delay(document.createTextNode(value))

Expand Down
3 changes: 3 additions & 0 deletions core/.jvm/src/main/scala/io/taig/schelm/ServerDom.scala
Expand Up @@ -42,6 +42,9 @@ final class ServerDom[F[_], Event](document: JDocument)(implicit F: Sync[F])
override def createElement(name: String): F[JElement] =
F.delay(new JElement(name))

override def createElementNS(namespace: String, name: String): F[Element] =
createElement(name)

override def createTextNode(value: String): F[JText] =
F.delay(new JText(value))

Expand Down
16 changes: 16 additions & 0 deletions core/src/main/scala/io/taig/schelm/Children.scala
Expand Up @@ -16,6 +16,22 @@ sealed abstract class Children[A] extends Product with Serializable {
case Children.Identified(values) => values.size
}

def ++(children: Children[A]): Children[A] =
(this, children) match {
case (Children.Indexed(x), Children.Indexed(y)) =>
Children.Indexed(x ++ y)
case (Children.Identified(x), Children.Identified(y)) =>
Children.Identified(x ++ y)
case (Children.Indexed(x), Children.Identified(y)) =>
Children.Identified(ListMap(x.zipWithIndex.map {
case (value, index) => (String.valueOf(index), value)
}: _*) ++ y)
case (Children.Identified(x), Children.Indexed(y)) =>
Children.Identified(x ++ ListMap(y.zipWithIndex.map {
case (value, index) => (String.valueOf(index), value)
}: _*))
}

def indexOf(key: Key): Option[Int] =
(key, this) match {
case (Key.Index(index), _) => index.some
Expand Down
3 changes: 0 additions & 3 deletions core/src/main/scala/io/taig/schelm/Cofree.scala

This file was deleted.

4 changes: 3 additions & 1 deletion core/src/main/scala/io/taig/schelm/Component.scala
Expand Up @@ -7,6 +7,7 @@ sealed abstract class Component[+A, +Event] extends Product with Serializable
object Component {
final case class Element[A, Event](
name: String,
namespace: Option[String],
attributes: Attributes,
listeners: Listeners[Event],
children: Children[A]
Expand All @@ -26,9 +27,10 @@ object Component {
fa: Component[A, Event]
)(f: A => B): Component[B, Event] =
fa match {
case Element(name, attributes, listeners, children) =>
case Element(name, namespace, attributes, listeners, children) =>
Element(
name,
namespace,
attributes,
listeners,
children.map((_, value) => f(value))
Expand Down
3 changes: 0 additions & 3 deletions core/src/main/scala/io/taig/schelm/ComponentOps.scala
Expand Up @@ -19,9 +19,6 @@ abstract class ComponentOps[F[_], A](
final def updateListeners(f: Listeners[A] => Listeners[A]): F[A] =
ComponentOps.updateListeners(component, extract, inject)(f)

final def children: Children[F[A]] =
ComponentOps.children(component, extract, inject)

final def setChildren(children: Children[F[A]]): F[A] =
updateChildren(_ => children)

Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/io/taig/schelm/Dom.scala
Expand Up @@ -28,6 +28,8 @@ abstract class Dom[F[_], Event] {

def createElement(name: String): F[Element]

def createElementNS(namespace: String, name: String): F[Element]

def createTextNode(value: String): F[Text]

def childAt(element: Element, index: Int): F[Option[Node]]
Expand Down
3 changes: 0 additions & 3 deletions core/src/main/scala/io/taig/schelm/Fix.scala

This file was deleted.

3 changes: 3 additions & 0 deletions core/src/main/scala/io/taig/schelm/Html.scala
@@ -0,0 +1,3 @@
package io.taig.schelm

final case class Html[+A](component: Component[Html[A], A])
2 changes: 1 addition & 1 deletion core/src/main/scala/io/taig/schelm/HtmlDiffer.scala
Expand Up @@ -6,7 +6,7 @@ import cats.implicits._
class HtmlDiffer[A] extends Differ[Html[A], HtmlDiff[A]] {
override def diff(previous: Html[A], next: Html[A]): Option[HtmlDiff[A]] =
// format: off
(previous.value, next.value) match {
(previous.component, next.component) match {
case (previous: Component.Element[Html[A], A], next: Component.Element[Html[A], A]) =>
element(previous, next)
case (previous: Component.Fragment[Html[A]], next: Component.Fragment[Html[A]]) =>
Expand Down
36 changes: 22 additions & 14 deletions core/src/main/scala/io/taig/schelm/HtmlRenderer.scala
Expand Up @@ -8,30 +8,38 @@ final class HtmlRenderer[F[_], Event](dom: Dom[F, Event])(
) extends Renderer[F, Html[Event], Reference[Event]] {
override def render(
html: Html[Event],
parent: Option[Element],
path: Path
): F[Reference[Event]] =
html.value match {
html.component match {
case component: Component.Element[Html[Event], Event] =>
val name = component.name

for {
element <- dom.createElement(component.name)
element <- component.namespace.fold(dom.createElement(name))(
dom.createElementNS(_, name)
)
_ <- parent.traverse_(dom.appendChild(_, element))
parent = element.some
_ <- component.attributes.traverse_(register(element, _))
_ <- component.listeners.traverse_(register(element, path, _))
children <- component.children.traverse(
(key, child) => render(child, path / key)
)
_ <- dom.appendChildren(element, children.values.flatMap(_.root))
children <- component.children.traverse { (key, child) =>
render(child, parent, path / key)
}
} yield Reference.Element(component.copy(children = children), element)
case component: Component.Fragment[Html[Event]] =>
component.children
.traverse((key, child) => render(child, path / key))
.map(component.copy)
.map(Reference.Fragment.apply)
for {
children <- component.children.traverse { (key, child) =>
render(child, parent, path / key)
}
} yield Reference.Fragment(component.copy(children = children))
case component: Component.Lazy[Html[Event]] =>
render(component.eval.value, path)
render(component.eval.value, parent, path)
case component: Component.Text =>
dom
.createTextNode(component.value)
.map(Reference.Text(component, _))
for {
text <- dom.createTextNode(component.value)
_ <- parent.traverse_(dom.appendChild(_, text))
} yield Reference.Text(component, text)
}

def register(element: Element, attribute: Attribute): F[Unit] =
Expand Down
8 changes: 5 additions & 3 deletions core/src/main/scala/io/taig/schelm/ReferencePatcher.scala
Expand Up @@ -17,8 +17,10 @@ final class ReferencePatcher[F[_], A](
// format: off
(reference, diff) match {
case (_, diff: HtmlDiff.Group[A]) => group(reference, diff, path)
case (reference: Reference.Element[A], diff: HtmlDiff.AddAttribute) => addAttribute(reference, diff)
case (reference: Reference.Element[A], diff: HtmlDiff.RemoveAttribute) => removeAttribute(reference, diff)
case (reference: Reference.Element[A], diff: HtmlDiff.AddAttribute) =>
addAttribute(reference, diff)
case (reference: Reference.Element[A], diff: HtmlDiff.RemoveAttribute) =>
removeAttribute(reference, diff)
case (element@Reference.Element(_, node), HtmlDiff.RemoveListener(event)) =>
dom.removeEventListener(node, event, path) *>
element.updateListeners(_ - event).pure[F]
Expand All @@ -45,7 +47,7 @@ final class ReferencePatcher[F[_], A](
reference <- child(component.children, key)
child <- patch(reference, diff, path / key)
} yield parent.updateChildren(_.updated(key, child))
case (_, HtmlDiff.Replace(html)) => renderer.render(html, path)
case (_, HtmlDiff.Replace(html)) => renderer.render(html, parent = None, path)
case (reference @ Reference.Text(_, node), HtmlDiff.UpdateText(value)) =>
dom.data(node, value) *> reference.pure[F].widen
case _ =>
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/io/taig/schelm/Renderer.scala
@@ -1,5 +1,5 @@
package io.taig.schelm

abstract class Renderer[F[_], A, B] {
def render(value: A, path: Path): F[B]
def render(value: A, parent: Option[Element], path: Path): F[B]
}
2 changes: 1 addition & 1 deletion core/src/main/scala/io/taig/schelm/Schelm.scala
Expand Up @@ -38,7 +38,7 @@ final class Schelm[F[_], Event, Component, Reference, Diff](
val component = render(initial)

for {
reference <- renderer.render(component, Path.Root)
reference <- renderer.render(component, parent = None, Path.Root)
_ <- attacher.attach(container, reference)
htmls = (manager.subscription merge subscriptions)
.evalScan(initial) { (state, event) =>
Expand Down
136 changes: 136 additions & 0 deletions core/src/main/scala/io/taig/schelm/Widget.scala
@@ -0,0 +1,136 @@
package io.taig.schelm

import cats.Monoid
import cats.implicits._

import scala.annotation.tailrec

sealed abstract class Widget[+Event, Context, Payload]
extends Product
with Serializable {
def component(
context: Context
): Component[Widget[Event, Context, Payload], Event] = this match {
case Widget.Pure(component, _) => component
case Widget.Render(apply) => apply(context).component(context)
case Widget.Local(f, widget) => widget.component(f(context))
}

def component(
implicit ev: Unit =:= Context
): Component[Widget[Event, Context, Payload], Event] = component(unit)

def payload(context: Context): Payload = this match {
case Widget.Pure(_, payload) => payload
case Widget.Render(apply) => apply(context).payload(context)
case Widget.Local(f, widget) => widget.payload(f(context))
}

def payload(implicit ev: Unit =:= Context): Payload = payload(unit)

def render(context: Context): Widget[Event, Unit, Payload] =
Widget.render(context, this)

def merge(implicit F: Monoid[Payload], ev: Unit =:= Context): Payload = {
val payloads = component match {
case component: Component.Element[Widget[_, Context, Payload], _] =>
component.children.values.map(_.merge).combineAll
case component: Component.Fragment[Widget[_, Context, Payload]] =>
component.children.values.map(_.merge).combineAll
case component: Component.Lazy[Widget[_, Context, Payload]] =>
component.eval.value.merge
case _: Component.Text => F.empty
}

payload(unit) |+| payloads
}
}

object Widget {
final case class Pure[+Event, Context, Payload](
component: Component[Widget[Event, Context, Payload], Event],
payload: Payload
) extends Widget[Event, Context, Payload]

final case class Render[+Event, Context, Payload](
apply: Context => Widget[Event, Context, Payload]
) extends Widget[Event, Context, Payload]

final case class Local[+Event, Context, Payload](
f: Context => Context,
widget: Widget[Event, Context, Payload]
) extends Widget[Event, Context, Payload]

def apply[Event, Context, Payload](
apply: Context => Widget[Event, Context, Payload]
): Widget[Event, Context, Payload] =
Render(apply)

def pure[Event, Context, Payload](
component: Component[Widget[Event, Context, Payload], Event],
payload: Payload
): Widget[Event, Context, Payload] = Pure(component, payload)

def empty[Event, Context, Payload: Monoid](
component: Component[Widget[Event, Context, Payload], Event]
): Widget[Event, Context, Payload] =
Pure(component, Monoid[Payload].empty)

def local[Event, Context, Payload](
f: Context => Context
)(widget: Widget[Event, Context, Payload]): Widget[Event, Context, Payload] =
Local(f, widget)

@tailrec
def render[Event, Context, Payload](
context: Context,
widget: Widget[Event, Context, Payload]
): Widget[Event, Unit, Payload] =
widget match {
case Pure(component, payload) => Pure(render(context, component), payload)
case Render(apply) => render(context, apply(context))
case Local(f, widget) => render(f(context), widget)
}

def render[Event, Context, Payload](
context: Context,
component: Component[Widget[Event, Context, Payload], Event]
): Component[Widget[Event, Unit, Payload], Event] =
component match {
case component: Component.Element[Widget[Event, Context, Payload], Event] =>
val children = component.children.map { (_, child) =>
render(context, child)
}
component.copy(children = children)
case Component.Fragment(children) =>
Component.Fragment(children.map((_, child) => render(context, child)))
case Component.Lazy(eval, hash) =>
Component.Lazy(eval.map(render(context, _)), hash)
case component: Component.Text => component
}

def payload[Event, Context, Payload](
widget: Widget[Event, Context, Payload]
)(transform: Payload => Payload): Widget[Event, Context, Payload] =
widget match {
case Pure(component, payload) => Pure(component, transform(payload))
case Render(apply) =>
Render(context => payload(apply(context))(transform))
case Local(f, widget) => Local(f, payload(widget)(transform))
}

def component[Event, Context, Payload](
widget: Widget[Event, Context, Payload]
)(
transform: Component[Widget[Event, Context, Payload], Event] => Component[
Widget[Event, Context, Payload],
Event
]
): Widget[Event, Context, Payload] =
widget match {
case Pure(component, payload) => Pure(transform(component), payload)
case Render(apply) =>
Render(context => component(apply(context))(transform))
case Local(f, widget) => Local(f, component(widget)(transform))
}
}