diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..cf66596 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1 @@ +dist/main.js diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..ce1a014 --- /dev/null +++ b/web/README.md @@ -0,0 +1,12 @@ + + +# Web framework experiment + +Work in progress of a DOM-patching web framework, like Elm. + +In the root, you can do: + +```sh +newt src/Todo.newt -o dist/main.js +esbuild dist/main.js --bundle --servedir=web --outdir=web --alias:fs=./dist/empty.js --watch +``` diff --git a/web/dist/empty.js b/web/dist/empty.js new file mode 100644 index 0000000..b1c6ea4 --- /dev/null +++ b/web/dist/empty.js @@ -0,0 +1 @@ +export default {} diff --git a/web/dist/index.html b/web/dist/index.html new file mode 100644 index 0000000..bafd1bc --- /dev/null +++ b/web/dist/index.html @@ -0,0 +1,11 @@ + + + + Counter + + + +
+ + + diff --git a/web/dist/spruce.ts b/web/dist/spruce.ts new file mode 100644 index 0000000..211ccce --- /dev/null +++ b/web/dist/spruce.ts @@ -0,0 +1,126 @@ +// We're going to do the VDom thing, directly off of newt data + +type Attrib = + | { tag: 0; h1: string; h2: string } + | { tag: 1; h1: string; h2: Msg } + | { tag: 2; h1: string; h2: (_: string) => Msg }; + +type List = { tag: 0 } | { tag: 1; h1: A; h2: List }; + +type VNode = + | { tag: 0; h1: string } // TNode + | { tag: 1; h1: string; h2: List>; h3: List> }; + +type FancyElement = Element & { events?: Record > }; + +// the pfunc will have two dummy arguments in front, and I dunno no node? +export function runapp( + node: Node, + init: Model, + update: (_: Msg) => (_: Model) => Model, + view: (_: Model) => VNode, +) { + function replace(parent: Node, node: Node | undefined, child: Node) { + if (node) { + parent.insertBefore(node, child); + parent.removeChild(node); + } else { + parent.appendChild(child); + } + return child; + } + // patch node, possibly returning a new node + function patch( + parentNode: Node, + node: Node | undefined, + vnode: VNode, + ): Node { + if (vnode.tag == 0) { + if (node && node.nodeType == 3) { + node.nodeValue = vnode.h1; + return node; + } + return replace(parentNode, node, document.createTextNode(vnode.h1)); + } + let el: FancyElement; + if (node instanceof Element && node.tagName.toLowerCase() == vnode.h1) { + el = node; + } else { + el = document.createElement(vnode.h1); + } + // update node here + let has: Record = {}; + for (let attrs = vnode.h2; attrs.tag == 1; attrs = attrs.h2) { + let attr = attrs.h1; + if (attr.tag == 0) { + let key = attr.h1; + has[key] = true; + if (key in el) + (el as any)[key] = attr.h2 + else + el.setAttribute(key, attr.h2); + } else { + // onBlah + let key = attr.h1.slice(2).toLowerCase(); + has[key] = true; + let events = el.events || (el.events = {}); + if (!events[key]) el.addEventListener(key, listener); + events[key] = attr; + } + } + // remove attrs, not efficient.. + for (let i = 0; i < el.attributes.length; ) { + let attr = el.attributes[i]; + if (!has[attr.name]) { + el.removeAttribute(attr.name); + } else { + i++; + } + } + if (el.events) { + for (let key of Object.keys(el.events)) { + if (!has[key]) delete el.events[key]; + } + } + let i = 0; + for (let kids = vnode.h3; kids.tag == 1; kids = kids.h2) { + patch(el, el.childNodes[i++], kids.h1); + } + while (el.childNodes[i]) el.removeChild(el.childNodes[i]); + + return node == el ? node : replace(parentNode, node, el); + } + let model = init; + let vdom = view(model); + let listener = (ev: Event) => { + let target = ev.target as FancyElement; + if (!target.events) return; + const attr = target.events[ev.type] + let action + if (attr.tag === 2) { + // probably need to pass back the event + // we'll want to deal with keypress, change, ... + let action = attr.h2(ev.target!.value) + model = update(action)(model); + } else if (attr.tag === 1) { + model = update(attr.h2)(model); + } + vdom = view(model); + node = patch(node.parentNode!, node, vdom); + }; + node = patch(node.parentNode!, node, vdom); +} +[{ + "resource": "/Users/dunham/prj/newt/src/Web/spruce.ts", + "owner": "typescript", + "code": "2349", + "severity": 8, + "message": "This expression is not callable.\n Not all constituents of type '((_: string) => Msg) | (Msg & Function)' are callable.\n Type 'Msg & Function' has no call signatures.", + "source": "ts", + "startLineNumber": 101, + "startColumn": 16, + "endLineNumber": 101, + "endColumn": 19, + "modelVersionId": 289, + "origin": "extHost1" +}] diff --git a/web/dist/style.css b/web/dist/style.css new file mode 100644 index 0000000..4b53a64 --- /dev/null +++ b/web/dist/style.css @@ -0,0 +1,13 @@ +.item { + display: flex; +} +.item > * { + vertical-align: middle; + padding-right: 10px; +} +.item > div { + display: inline; + width: 100px; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/web/src/Prelude.newt b/web/src/Prelude.newt new file mode 120000 index 0000000..2d44821 --- /dev/null +++ b/web/src/Prelude.newt @@ -0,0 +1 @@ +../../src/Prelude.newt \ No newline at end of file diff --git a/web/src/Spruce.newt b/web/src/Spruce.newt new file mode 100644 index 0000000..a3a8134 --- /dev/null +++ b/web/src/Spruce.newt @@ -0,0 +1,31 @@ +module Spruce + +-- Spruce is not Elm + +import Prelude + +ptype Element +pfunc getElementById : String → Element := `(id) => document.getElementById(id)` + +-- Make this align with spruce.ts +data Attr msg = SAttr String String | MAttr String msg | VAttr String (String → msg) +data VNode msg = TNode String | ENode String (List $ Attr msg) (List $ VNode msg) + +text : ∀ msg. String → VNode msg +text = TNode + +tag : ∀ msg. String → List (Attr msg)→ List (VNode msg) → VNode msg +tag = ENode + +tag_ : ∀ msg. String → List (VNode msg)→ VNode msg +tag_ tag es = ENode tag Nil es + +-- TODO better story for import.. +-- also, we'll probably want to expose something with subscriptions / dispatch / ... +pfunc runApp : ∀ msg model. Element → model → (update : msg → model → model) → (view : model → VNode msg) → Unit := + `(_msg, _model, el, init, update, view) => { + require('./spruce').runapp(el,init,update,view); + return 0; + }` + +-- App diff --git a/web/src/Todo.newt b/web/src/Todo.newt new file mode 100644 index 0000000..d7dc7ce --- /dev/null +++ b/web/src/Todo.newt @@ -0,0 +1,84 @@ +module Todo + +import Prelude +import Spruce + +data Msg + = Toggle Nat + | Remove Nat + | Change String + +record Item where + checked : Bool + text : String + +record Model where + items : List Item + edit : String + +ElCon : U +ElCon = List (Attr Msg) → List (VNode Msg) → VNode Msg + +-- Attributes + +onChange : Msg → Attr Msg +onChange = MAttr "onChange" + +onClick : Msg → Attr Msg +onClick action = MAttr "OnClick" action + +className : String → Attr Msg +className v = SAttr "class" v + +checkbox : Bool → Nat → VNode Msg +checkbox checked ix = + tag "input" [ SAttr "checked" (ite checked "true" ""), onChange (Toggle ix), SAttr "type" "checkbox" ] [] + +-- Elements + +div : ElCon +div = tag "div" + +button : String → Msg → VNode Msg +button label action = tag "button" [ onClick action ] [ text label ] + + +itemView : Nat × Item → VNode Msg +itemView (ix , item) = + div [ className "item" ] + [ checkbox item.checked ix + , div [] [text item.text] + , button "x" (Remove ix) + ] + +view : Model → VNode Msg +view model = + div [] + [ div [] (map itemView $ enumerate model.items) + , tag "input" [ SAttr "value" model.edit, VAttr "onChange" Change ] [] + ] + +update : Msg → Model → Model +update (Toggle ix) model = { items $= toggle ix } model + where + toggleItem : Item → Item + toggleItem item = { checked := not item.checked } item + + toggle : Nat → List Item → List Item + toggle _ Nil = Nil + toggle Z (item :: rest) = toggleItem item :: rest + toggle (S k) (item :: rest) = item :: toggle k rest + +update (Remove ix) model = {items $= delete ix } model + where + delete : Nat → List Item → List Item + delete _ Nil = Nil + delete Z (item :: rest) = rest + delete (S k) (item :: rest) = item :: delete k rest + +update (Change text) model = { items := (snoc model.items $ MkItem False text) } model + +main : IO Unit +main = pure $ runApp (getElementById "main") (MkModel [] "") update view + +