Skip to content

Commit

Permalink
add support for raw html via custom elements
Browse files Browse the repository at this point in the history
  • Loading branch information
eliknebel committed Mar 13, 2024
1 parent 96c996f commit fc70dcb
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 21 deletions.
16 changes: 16 additions & 0 deletions client/src/modules/rawHtml.ts
Original file line number Diff line number Diff line change
@@ -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,
}
14 changes: 14 additions & 0 deletions client/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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}`);
}
}
3 changes: 2 additions & 1 deletion client/src/sprocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,7 +41,7 @@ export function connect(path: String, opts: Opts) {
let dom: Record<string, any>;
let oldVNode: VNode;

const patcher = init([attributesModule, eventListenersModule], undefined, {
const patcher = init([attributesModule, eventListenersModule, rawHtmlModule], undefined, {
experimental: {
fragments: true,
},
Expand Down
1 change: 1 addition & 0 deletions src/sprocket/context.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
14 changes: 12 additions & 2 deletions src/sprocket/html/elements.gleam
Original file line number Diff line number Diff line change
@@ -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)
}
Expand Down
13 changes: 12 additions & 1 deletion src/sprocket/internal/patch.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/sprocket/internal/reconcile.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 10 additions & 5 deletions src/sprocket/internal/reconcilers/recursive.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,6 +76,7 @@ pub fn reconcile(
reconcile(ctx, el, key, prev)
}
Text(t) -> text(ctx, t)
Custom(kind, data) -> custom(ctx, kind, data)
}
}

Expand Down Expand Up @@ -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,
Expand Down
63 changes: 52 additions & 11 deletions src/sprocket/renderers/html.gleam
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -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("</"),
string_builder.from_string(tag),
string_builder.from_string(">"),
])
}

fn element(
tag: String,
key: Option(String),
Expand Down Expand Up @@ -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("</"),
string_builder.from_string(tag),
string_builder.from_string(">"),
])
el(tag, rendered_attrs, inner_html)
}

fn component(el: ReconciledElement) {
Expand Down Expand Up @@ -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),
)
}
12 changes: 11 additions & 1 deletion src/sprocket/renderers/json.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)),
Expand Down

0 comments on commit fc70dcb

Please sign in to comment.