From fc70dcbccbd9766350a638286f6a7c7433286a5d Mon Sep 17 00:00:00 2001 From: Eli Knebel Date: Tue, 12 Mar 2024 21:16:54 -0400 Subject: [PATCH] add support for raw html via custom elements --- client/src/modules/rawHtml.ts | 16 +++++ client/src/render.ts | 14 +++++ client/src/sprocket.ts | 3 +- src/sprocket/context.gleam | 1 + src/sprocket/html/elements.gleam | 14 ++++- src/sprocket/internal/patch.gleam | 13 +++- src/sprocket/internal/reconcile.gleam | 1 + .../internal/reconcilers/recursive.gleam | 15 +++-- src/sprocket/renderers/html.gleam | 63 +++++++++++++++---- src/sprocket/renderers/json.gleam | 12 +++- 10 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 client/src/modules/rawHtml.ts diff --git a/client/src/modules/rawHtml.ts b/client/src/modules/rawHtml.ts new file mode 100644 index 0000000..bb0eeca --- /dev/null +++ b/client/src/modules/rawHtml.ts @@ -0,0 +1,16 @@ +function updateInnerHtml (oldVNode, vNode) { + const {data: oldData = {}} = oldVNode + const {data = {}, elm} = vNode + const html = data.innerHtml || false + + if (!html) return + + if (html && oldData.innerHtml !== html) { + elm.innerHTML = html + } + } + + export const rawHtmlModule = { + create: updateInnerHtml, + update: updateInnerHtml, + } \ No newline at end of file diff --git a/client/src/render.ts b/client/src/render.ts index 7b5a4ac..d9c7eab 100644 --- a/client/src/render.ts +++ b/client/src/render.ts @@ -23,6 +23,8 @@ export function render( return renderComponent(node, providers); case "fragment": return renderFragment(node, providers); + case "custom": + return renderCustom(node, providers); default: throw new Error(`Unknown node type: ${node.type}`); } @@ -80,3 +82,15 @@ function renderFragment(f, providers: Providers): VNode { .reduce((acc, key) => [...acc, render(f[key], providers)], []) ); } + +function renderCustom(custom, providers: Providers): VNode { + switch(custom.kind) { + case "raw": + const { tag, innerHtml } = JSON.parse(custom.data); + + return h(tag, { innerHtml }); + + default: + throw new Error(`Unknown custom kind: ${custom.kind}`); + } +} \ No newline at end of file diff --git a/client/src/sprocket.ts b/client/src/sprocket.ts index 4399a64..aca7ba1 100644 --- a/client/src/sprocket.ts +++ b/client/src/sprocket.ts @@ -10,6 +10,7 @@ import { render, Providers } from "./render"; import { applyPatch } from "./patch"; import { initEventHandlerProvider } from "./events"; import { initClientHookProvider, Hook } from "./hooks"; +import { rawHtmlModule } from "./modules/rawHtml"; export type ClientHook = { create?: (hook: Hook) => void; @@ -40,7 +41,7 @@ export function connect(path: String, opts: Opts) { let dom: Record; let oldVNode: VNode; - const patcher = init([attributesModule, eventListenersModule], undefined, { + const patcher = init([attributesModule, eventListenersModule, rawHtmlModule], undefined, { experimental: { fragments: true, }, diff --git a/src/sprocket/context.gleam b/src/sprocket/context.gleam index d8a450a..5a36110 100644 --- a/src/sprocket/context.gleam +++ b/src/sprocket/context.gleam @@ -46,6 +46,7 @@ pub type Element { IgnoreUpdate(element: Element) Provider(key: String, value: Dynamic, element: Element) Text(text: String) + Custom(kind: String, data: String) } pub type Updater(r) { diff --git a/src/sprocket/html/elements.gleam b/src/sprocket/html/elements.gleam index efe3104..9d3e507 100644 --- a/src/sprocket/html/elements.gleam +++ b/src/sprocket/html/elements.gleam @@ -1,14 +1,24 @@ +import gleam/json import gleam/option.{type Option, None, Some} import gleam/dynamic.{type Dynamic} import sprocket/context.{ - type Attribute, type Element, Debug, Element, Fragment, IgnoreUpdate, Keyed, - Text, + type Attribute, type Element, Custom, Debug, Element, Fragment, IgnoreUpdate, + Keyed, Text, } pub fn el(tag: String, attrs: List(Attribute), children: List(Element)) { Element(tag, attrs, children) } +pub fn raw(tag: String, html: String) { + let data = + [#("tag", json.string(tag)), #("innerHtml", json.string(html))] + |> json.object() + |> json.to_string() + + Custom("raw", data) +} + pub fn fragment(children: List(Element)) { Fragment(children) } diff --git a/src/sprocket/internal/patch.gleam b/src/sprocket/internal/patch.gleam index 4f35120..2f6ccf7 100644 --- a/src/sprocket/internal/patch.gleam +++ b/src/sprocket/internal/patch.gleam @@ -6,7 +6,7 @@ import gleam/option.{type Option, None, Some} import gleam/json.{type Json} import sprocket/internal/reconcile.{ type ReconciledAttribute, type ReconciledElement, ReconciledAttribute, - ReconciledClientHook, ReconciledComponent, ReconciledElement, + ReconciledClientHook, ReconciledComponent, ReconciledCustom, ReconciledElement, ReconciledEventHandler, ReconciledFragment, ReconciledIgnoreUpdate, ReconciledText, } @@ -136,6 +136,17 @@ pub fn create(old: ReconciledElement, new: ReconciledElement) -> Patch { } } + ReconciledCustom(old_kind, old_data), ReconciledCustom(new_kind, new_data) -> { + case old_kind == new_kind, old_data == new_data { + True, True -> { + NoOp + } + _, _ -> { + Replace(el: new) + } + } + } + // ignore updates _, ReconciledIgnoreUpdate(_el) -> NoOp diff --git a/src/sprocket/internal/reconcile.gleam b/src/sprocket/internal/reconcile.gleam index d3a3b6d..9246628 100644 --- a/src/sprocket/internal/reconcile.gleam +++ b/src/sprocket/internal/reconcile.gleam @@ -28,6 +28,7 @@ pub type ReconciledElement { ReconciledFragment(key: Option(String), children: List(ReconciledElement)) ReconciledIgnoreUpdate(el: ReconciledElement) ReconciledText(text: String) + ReconciledCustom(kind: String, data: String) } pub type ReconciledResult { diff --git a/src/sprocket/internal/reconcilers/recursive.gleam b/src/sprocket/internal/reconcilers/recursive.gleam index 984a552..5650550 100644 --- a/src/sprocket/internal/reconcilers/recursive.gleam +++ b/src/sprocket/internal/reconcilers/recursive.gleam @@ -6,15 +6,15 @@ import gleam/option.{type Option, None, Some} import gleam/dynamic.{type Dynamic} import sprocket/context.{ type AbstractFunctionalComponent, type Attribute, type Context, type Element, - Attribute, ClientHook, Component, ComponentWip, Context, Debug, Element, Event, - Fragment, IgnoreUpdate, Keyed, Provider, Text, + Attribute, ClientHook, Component, ComponentWip, Context, Custom, Debug, + Element, Event, Fragment, IgnoreUpdate, Keyed, Provider, Text, } import sprocket/internal/reconcile.{ type ReconciledAttribute, type ReconciledElement, type ReconciledResult, type Reconciler, ReconciledAttribute, ReconciledClientHook, - ReconciledComponent, ReconciledElement, ReconciledEventHandler, - ReconciledFragment, ReconciledIgnoreUpdate, ReconciledResult, ReconciledText, - Reconciler, + ReconciledComponent, ReconciledCustom, ReconciledElement, + ReconciledEventHandler, ReconciledFragment, ReconciledIgnoreUpdate, + ReconciledResult, ReconciledText, Reconciler, } import sprocket/internal/utils/unique import sprocket/internal/utils/ordered_map @@ -76,6 +76,7 @@ pub fn reconcile( reconcile(ctx, el, key, prev) } Text(t) -> text(ctx, t) + Custom(kind, data) -> custom(ctx, kind, data) } } @@ -315,6 +316,10 @@ fn text(ctx: Context, t: String) -> ReconciledResult { ReconciledResult(ctx, ReconciledText(t)) } +fn custom(ctx: Context, kind: String, data: String) -> ReconciledResult { + ReconciledResult(ctx, ReconciledCustom(kind, data)) +} + pub fn traverse( el: ReconciledElement, updater: fn(ReconciledElement) -> ReconciledElement, diff --git a/src/sprocket/renderers/html.gleam b/src/sprocket/renderers/html.gleam index e43cfa6..3a5a0ac 100644 --- a/src/sprocket/renderers/html.gleam +++ b/src/sprocket/renderers/html.gleam @@ -1,10 +1,12 @@ import gleam/list import gleam/string +import gleam/dynamic.{type Dynamic, field} import gleam/option.{type Option, None, Some} import gleam/string_builder.{type StringBuilder} +import gleam/json import sprocket/internal/reconcile.{ type ReconciledAttribute, type ReconciledElement, ReconciledAttribute, - ReconciledComponent, ReconciledElement, ReconciledFragment, + ReconciledComponent, ReconciledCustom, ReconciledElement, ReconciledFragment, ReconciledIgnoreUpdate, ReconciledText, } import sprocket/render.{type Renderer, Renderer} @@ -25,9 +27,27 @@ fn render(el: ReconciledElement) -> StringBuilder { ReconciledFragment(children: children, ..) -> fragment(children) ReconciledIgnoreUpdate(el) -> render(el) ReconciledText(text: t) -> text(t) + ReconciledCustom(kind: kind, data: data) -> custom(kind, data) } } +fn el( + tag: String, + attrs: StringBuilder, + inner_html: StringBuilder, +) -> StringBuilder { + string_builder.concat([ + string_builder.from_string("<"), + string_builder.from_string(tag), + attrs, + string_builder.from_string(">"), + inner_html, + string_builder.from_string(""), + ]) +} + fn element( tag: String, key: Option(String), @@ -63,16 +83,7 @@ fn element( string_builder.append_builder(acc, render(child)) }) - string_builder.concat([ - string_builder.from_string("<"), - string_builder.from_string(tag), - rendered_attrs, - string_builder.from_string(">"), - inner_html, - string_builder.from_string(""), - ]) + el(tag, rendered_attrs, inner_html) } fn component(el: ReconciledElement) { @@ -112,3 +123,33 @@ fn text(t: String) -> StringBuilder { escape_html(t) |> string_builder.from_string() } + +fn custom(kind: String, data: String) -> StringBuilder { + case kind { + "raw" -> { + case json.decode(data, decode_raw) { + Ok(RawHtml(tag, raw_html)) -> + el( + tag, + string_builder.from_string(""), + string_builder.from_string(raw_html), + ) + Error(_) -> string_builder.from_string("") + } + } + _ -> string_builder.from_string("") + } +} + +type RawHtml { + RawHtml(tag: String, raw_html: String) +} + +fn decode_raw(data: Dynamic) { + data + |> dynamic.decode2( + RawHtml, + field("tag", dynamic.string), + field("innerHtml", dynamic.string), + ) +} diff --git a/src/sprocket/renderers/json.gleam b/src/sprocket/renderers/json.gleam index 93e8849..c82e29a 100644 --- a/src/sprocket/renderers/json.gleam +++ b/src/sprocket/renderers/json.gleam @@ -4,7 +4,7 @@ import gleam/option.{type Option, None, Some} import gleam/json.{type Json} import sprocket/internal/reconcile.{ type ReconciledAttribute, type ReconciledElement, ReconciledAttribute, - ReconciledClientHook, ReconciledComponent, ReconciledElement, + ReconciledClientHook, ReconciledComponent, ReconciledCustom, ReconciledElement, ReconciledEventHandler, ReconciledFragment, ReconciledIgnoreUpdate, ReconciledText, } @@ -22,6 +22,7 @@ fn render(el: ReconciledElement) -> Json { ReconciledFragment(key, children: children) -> fragment(key, children) ReconciledIgnoreUpdate(el) -> render(el) ReconciledText(text: t) -> text(t) + ReconciledCustom(kind: kind, data: data) -> custom(kind, data) } } @@ -99,6 +100,15 @@ fn text(t: String) -> Json { json.string(t) } +fn custom(kind: String, data: String) -> Json { + [ + #("type", json.string("custom")), + #("kind", json.string(kind)), + #("data", json.string(data)), + ] + |> json.object() +} + // appends a string property to a json object if the value is present fn maybe_append_string( json_object_builder: List(#(String, Json)),