diff --git a/tyrian/js/src/main/scala/tyrian/runtime/Rendering.scala b/tyrian/js/src/main/scala/tyrian/runtime/Rendering.scala index bd64c56e..1eb99c2c 100644 --- a/tyrian/js/src/main/scala/tyrian/runtime/Rendering.scala +++ b/tyrian/js/src/main/scala/tyrian/runtime/Rendering.scala @@ -22,7 +22,7 @@ import scala.scalajs.js object Rendering: - private def buildNodeData[Msg](attrs: List[Attr[Msg]], onMsg: Msg => Unit): VNodeData = + private def buildNodeData[Msg](attrs: List[Attr[Msg]], onMsg: Msg => Unit, key: Option[String]): VNodeData = val as: List[(String, String)] = attrs.collect { case Attribute(n, v) => (n, v) @@ -51,7 +51,8 @@ object Rendering: VNodeData.empty.copy( props = props.toMap, attrs = as.toMap, - on = events.toMap + on = events.toMap, + key = key ) private def interceptHref[Msg](attrs: List[Attr[Msg]]): Boolean = @@ -103,8 +104,8 @@ object Rendering: def toVNode[Msg](html: Html[Msg], onMsg: Msg => Unit, router: Location => Msg): VNode = html match - case RawTag(name, attrs, html) => - val data = buildNodeData(attrs, onMsg) + case RawTag(name, attrs, html, key) => + val data = buildNodeData(attrs, onMsg, key) val elm = dom.document.createElement(name) elm.innerHTML = html val vNode = snabbdom.toVNode(elm) @@ -113,8 +114,8 @@ object Rendering: // Intercept a tags with an href and no onClick attribute to stop the // browser following links by default. - case Tag("a", attrs, children) if interceptHref(attrs) => - val data = buildNodeData(attrs, onMsg) + case Tag("a", attrs, children, key) if interceptHref(attrs) => + val data = buildNodeData(attrs, onMsg, key) val childrenElem: Array[VNode] = children.toArray.map { case _: Empty.type => VNode.empty() @@ -128,8 +129,8 @@ object Rendering: childrenElem ) - case Tag(name, attrs, children) => - val data = buildNodeData(attrs, onMsg) + case Tag(name, attrs, children, key) => + val data = buildNodeData(attrs, onMsg, key) val childrenElem: Array[VNode] = children.toArray.map { case _: Empty.type => VNode.empty() diff --git a/tyrian/shared/src/main/scala/tyrian/Html.scala b/tyrian/shared/src/main/scala/tyrian/Html.scala index b593f700..be47b2af 100644 --- a/tyrian/shared/src/main/scala/tyrian/Html.scala +++ b/tyrian/shared/src/main/scala/tyrian/Html.scala @@ -19,8 +19,21 @@ final case class Text(value: String) extends Elem[Nothing]: /** Base class for HTML tags */ sealed trait Html[+M] extends Elem[M]: + /** Map over the node in order to modify the Msg type */ def map[N](f: M => N): Html[N] + + /** Set this node's innerHtml with stringified HTML. */ def innerHtml(html: String): Html[M] + + /** Optionally set a key value to help the virtual-dom understand what has changed. */ + def withKey(value: Option[String]): Html[M] + + /** Set a key value to help the virtual-dom understand what has changed. */ + def setKey(value: String): Html[M] + + /** Clear a key value that was being used to help the virtual-dom understand what has changed. */ + def clearKey: Html[M] + override def toString(): String = this.render /** Object used to provide Html syntax `import tyrian.Html.*` @@ -92,7 +105,8 @@ object SVG extends SVGTags with SVGAttributes object Aria extends AriaAttributes /** An HTML tag */ -final case class Tag[+M](name: String, attributes: List[Attr[M]], children: List[Elem[M]]) extends Html[M]: +final case class Tag[+M](name: String, attributes: List[Attr[M]], children: List[Elem[M]], key: Option[String]) + extends Html[M]: def map[N](f: M => N): Tag[N] = this.copy( attributes = attributes.map(_.map(f)), @@ -102,14 +116,37 @@ final case class Tag[+M](name: String, attributes: List[Attr[M]], children: List def innerHtml(html: String): RawTag[M] = RawTag(name, attributes, html) + def withKey(value: Option[String]): Tag[M] = + this.copy(key = value) + def setKey(value: String): Tag[M] = + this.copy(key = Option(value)) + def clearKey: Tag[M] = + this.copy(key = None) + +object Tag: + def apply[M](name: String, attributes: List[Attr[M]], children: List[Elem[M]]): Tag[M] = + Tag(name, attributes, children, None) + /** An HTML tag with raw HTML rendered inside. Beware that the inner HTML is not validated to be correct, nor does it * get modified as a response to messages in any way. */ -final case class RawTag[+M](name: String, attributes: List[Attr[M]], innerHTML: String) extends Html[M]: +final case class RawTag[+M](name: String, attributes: List[Attr[M]], innerHTML: String, key: Option[String]) + extends Html[M]: def map[N](f: M => N): RawTag[N] = this.copy( attributes = attributes.map(_.map(f)) ) def innerHtml(html: String): RawTag[M] = - RawTag(name, attributes, html) + RawTag(name, attributes, html, key) + + def withKey(value: Option[String]): RawTag[M] = + this.copy(key = value) + def setKey(value: String): RawTag[M] = + this.copy(key = Option(value)) + def clearKey: RawTag[M] = + this.copy(key = None) + +object RawTag: + def apply[M](name: String, attributes: List[Attr[M]], innerHTML: String): RawTag[M] = + RawTag(name, attributes, innerHTML, None)