mirror of
https://github.com/roc-lang/roc.git
synced 2024-09-22 08:17:40 +03:00
virtual-dom: create App type and rocScript function
This commit is contained in:
parent
fbc8e684fd
commit
9beaedc536
@ -1,5 +1,6 @@
|
||||
interface Html.Internal
|
||||
exposes [
|
||||
App,
|
||||
Node,
|
||||
Attribute,
|
||||
CyclicStructureAccessor,
|
||||
@ -9,6 +10,7 @@ interface Html.Internal
|
||||
insertHandler,
|
||||
replaceHandler,
|
||||
dispatchEvent,
|
||||
rocScript,
|
||||
appendRenderedStatic,
|
||||
nodeSize,
|
||||
]
|
||||
@ -219,3 +221,42 @@ dispatchEvent = \lookup, handlerId, eventData, state ->
|
||||
|
||||
Ok (Custom handler) ->
|
||||
handler state eventData
|
||||
|
||||
HtmlId : Str
|
||||
|
||||
App state : {
|
||||
static : Node [],
|
||||
initDynamic : Str -> state,
|
||||
renderDynamic : state -> Dict HtmlId (Node state),
|
||||
}
|
||||
|
||||
rocScript : Str, List HtmlId, Str -> Result (Node []) [InvalidUtf8]*
|
||||
rocScript = \initData, dynamicRootIds, wasmUrl ->
|
||||
toJs = \data ->
|
||||
data
|
||||
|> Encode.toBytes Json.toUtf8
|
||||
|> Str.fromUtf8
|
||||
encInitData = toJs initData
|
||||
encDynamicRootIds = toJs dynamicRootIds
|
||||
encWasmUrl = toJs wasmUrl
|
||||
|
||||
when { encInitData, encDynamicRootIds, encWasmUrl } is
|
||||
{ encInitData: Ok jsInitData, encDynamicRootIds: Ok jsDynamicRootIds, encWasmUrl: Ok jsWasmUrl } ->
|
||||
elem : Node []
|
||||
elem = (element "script") [] [
|
||||
Text
|
||||
"""
|
||||
\(virtualDomJavaScript)
|
||||
(function() {
|
||||
const initData = \(jsInitData);
|
||||
const dynamicRootIds = \(jsDynamicRootIds);
|
||||
const wasmUrl = \(jsWasmUrl);
|
||||
window.roc.init(initData, dynamicRootIds, wasmUrl);
|
||||
})();
|
||||
""",
|
||||
]
|
||||
|
||||
Ok elem
|
||||
|
||||
_ ->
|
||||
Err InvalidUtf8
|
||||
|
279
examples/virtual-dom/platform/Html/VdomJs.roc
Normal file
279
examples/virtual-dom/platform/Html/VdomJs.roc
Normal file
@ -0,0 +1,279 @@
|
||||
interface Html.VdomJs
|
||||
exposes [virtualDomJavaScript]
|
||||
imports []
|
||||
|
||||
virtualDomJavaScript : Str
|
||||
virtualDomJavaScript =
|
||||
"""
|
||||
/** @typedef {(e: Event) => void} JsEventDispatcher */
|
||||
|
||||
/** @typedef {[string, JsEventDispatcher]} Listener */
|
||||
|
||||
/**
|
||||
* @typedef {Object} RocWasmExports
|
||||
* @property {(size: number, alignment: number) => number} roc_alloc
|
||||
* @property {(jsonListAddr: number, jsonListLength: number, handlerId: number) => void} roc_dispatch_event
|
||||
* @property {() => number} main
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ObjectField: [string, CyclicStructureAccessor]} | {ArrayIndex: [number, CyclicStructureAccessor]} | {SerializableValue: []}} CyclicStructureAccessor
|
||||
*/
|
||||
|
||||
/** @type {Array<Node | null>} */
|
||||
const nodes = [];
|
||||
|
||||
/** @type {Array<Listener | null>} */
|
||||
const listeners = [];
|
||||
|
||||
const utf8Decoder = new TextDecoder();
|
||||
const utf8Encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* @param {string} wasmFilename
|
||||
*/
|
||||
export const init = async (wasmFilename) => {
|
||||
const effects = {
|
||||
/**
|
||||
* @param {number} tagAddr
|
||||
*/
|
||||
createElement: (tagAddr) => {
|
||||
const tagName = decodeRocStr(tagAddr);
|
||||
const node = document.createElement(tagName);
|
||||
return insertNode(node);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} contentAddr
|
||||
*/
|
||||
createTextNode: (contentAddr) => {
|
||||
const content = decodeRocStr(contentAddr);
|
||||
const node = document.createTextNode(content);
|
||||
return insertNode(node);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} parentId
|
||||
* @param {number} childId
|
||||
*/
|
||||
appendChild: (parentId, childId) => {
|
||||
const parent = nodes[parentId];
|
||||
const child = nodes[childId];
|
||||
parent.appendChild(child);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
removeNode: (id) => {
|
||||
var _a;
|
||||
const node = nodes[id];
|
||||
nodes[id] = null;
|
||||
node.parentElement.removeChild(node);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} nodeId
|
||||
* @param {number} typeAddr
|
||||
* @param {number} valueAddr
|
||||
*/
|
||||
setAttribute: (nodeId, typeAddr, valueAddr) => {
|
||||
const node = nodes[nodeId];
|
||||
const name = decodeRocStr(typeAddr);
|
||||
const value = decodeRocStr(valueAddr);
|
||||
node.setAttribute(name, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} nodeId
|
||||
* @param {number} typeAddr
|
||||
*/
|
||||
removeAttribute: (nodeId, typeAddr) => {
|
||||
const node = nodes[nodeId];
|
||||
const name = decodeRocStr(typeAddr);
|
||||
node.removeAttribute(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} nodeId
|
||||
* @param {number} propNameAddr
|
||||
* @param {number} jsonAddr
|
||||
*/
|
||||
setProperty: (nodeId, propNameAddr, jsonAddr) => {
|
||||
const node = nodes[nodeId];
|
||||
const propName = decodeRocStr(propNameAddr);
|
||||
const json = decodeRocListUtf8(jsonAddr);
|
||||
const value = JSON.parse(json);
|
||||
node[propName] = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} nodeId
|
||||
* @param {number} propNameAddr
|
||||
*/
|
||||
removeProperty: (nodeId, propNameAddr) => {
|
||||
const node = nodes[nodeId];
|
||||
const propName = decodeRocStr(propNameAddr);
|
||||
node[propName] = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} nodeId
|
||||
* @param {number} eventTypeAddr
|
||||
* @param {number} accessorsJsonAddr
|
||||
* @param {number} handlerId
|
||||
*/
|
||||
setListener: (nodeId, eventTypeAddr, accessorsJsonAddr, handlerId) => {
|
||||
const element = nodes[nodeId];
|
||||
const eventType = decodeRocStr(eventTypeAddr);
|
||||
const accessorsJson = decodeRocStr(accessorsJsonAddr);
|
||||
const accessors = JSON.parse(accessorsJson);
|
||||
|
||||
// Dispatch a DOM event to the specified handler function in Roc
|
||||
const dispatchEvent = (ev) => {
|
||||
const { roc_alloc, roc_dispatch_event } = app.exports;
|
||||
const outerListRcAddr = roc_alloc(4 + accessors.length * 12, 4);
|
||||
memory32[outerListRcAddr >> 2] = 1;
|
||||
const outerListBaseAddr = outerListRcAddr + 4;
|
||||
let outerListIndex32 = outerListBaseAddr >> 2;
|
||||
accessors.forEach((accessor) => {
|
||||
const json = accessCyclicStructure(accessor, ev);
|
||||
const length16 = json.length;
|
||||
|
||||
// Due to UTF-8 encoding overhead, a few code points go from 2 bytes in UTF-16 to 3 bytes in UTF-8!
|
||||
const capacity8 = length16 * 3; // Extremely "worst-case", but simple, and the allocation is short-lived.
|
||||
const rcAddr = roc_alloc(4 + capacity8, 4);
|
||||
memory32[rcAddr >> 2] = 1;
|
||||
const baseAddr = rcAddr + 4;
|
||||
|
||||
// Write JSON to the heap allocation of the inner `List U8`
|
||||
const allocation = memory8.subarray(baseAddr, baseAddr + capacity8);
|
||||
const { written } = utf8Encoder.encodeInto(json, allocation);
|
||||
const length = written || 0; // TypeScript claims that `written` can be undefined, though I don't see this in the spec.
|
||||
|
||||
// Write the fields of the inner `List U8` into the heap allocation of the outer List
|
||||
memory32[outerListIndex32++] = baseAddr;
|
||||
memory32[outerListIndex32++] = length;
|
||||
memory32[outerListIndex32++] = capacity8;
|
||||
});
|
||||
|
||||
roc_dispatch_event(outerListBaseAddr, accessors.length, handlerId);
|
||||
};
|
||||
|
||||
// Make things easier to debug
|
||||
dispatchEvent.name = `dispatchEvent${handlerId}`;
|
||||
element.setAttribute("data-roc-event-handler-id", `${handlerId}`);
|
||||
|
||||
listeners[handlerId] = [eventType, dispatchEvent];
|
||||
element.addEventListener(eventType, dispatchEvent);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} nodeId
|
||||
* @param {number} handlerId
|
||||
*/
|
||||
removeListener: (nodeId, handlerId) => {
|
||||
const element = nodes[nodeId];
|
||||
const [eventType, dispatchEvent] = findListener(element, handlerId);
|
||||
listeners[handlerId] = null;
|
||||
element.removeAttribute("data-roc-event-handler-id");
|
||||
element.removeEventListener(eventType, dispatchEvent);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* decode a Roc `Str` to a JavaScript string
|
||||
* @param {number} strAddr8
|
||||
*/
|
||||
const decodeRocStr = (strAddr8) => {
|
||||
const lastByte = memory8[strAddr8 + 12];
|
||||
const isSmall = lastByte >= 0x80;
|
||||
if (isSmall) {
|
||||
const len = lastByte & 0x7f;
|
||||
const bytes = memory8.slice(strAddr8, strAddr8 + len);
|
||||
return utf8Decoder.decode(bytes);
|
||||
} else {
|
||||
return decodeRocListUtf8(strAddr8);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* decode a Roc List of UTF-8 bytes to a JavaScript string
|
||||
* @param {number} listAddr8
|
||||
*/
|
||||
const decodeRocListUtf8 = (listAddr8) => {
|
||||
const listIndex32 = listAddr8 >> 2;
|
||||
const bytesAddr8 = memory32[listIndex32];
|
||||
const len = memory32[listIndex32 + 1];
|
||||
const bytes = memory8.slice(bytesAddr8, bytesAddr8 + len);
|
||||
return utf8Decoder.decode(bytes);
|
||||
};
|
||||
|
||||
const wasmImports = { effects };
|
||||
const promise = fetch(wasmFilename);
|
||||
const instanceAndModule = await WebAssembly.instantiateStreaming(
|
||||
promise,
|
||||
wasmImports
|
||||
);
|
||||
const app = instanceAndModule.instance;
|
||||
const memory = app.exports.memory;
|
||||
const memory8 = new Uint8Array(memory.buffer);
|
||||
const memory32 = new Uint32Array(memory.buffer);
|
||||
|
||||
const { main } = app.exports;
|
||||
const exitCode = main();
|
||||
if (exitCode) {
|
||||
throw new Error(`Roc exited with error code ${exitCode}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
*/
|
||||
const insertNode = (node) => {
|
||||
let i = 0;
|
||||
for (; i < nodes.length; i++) {
|
||||
if (!nodes[i]) break;
|
||||
}
|
||||
nodes[i] = node;
|
||||
return i;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {CyclicStructureAccessor} accessor
|
||||
* @param {any} structure
|
||||
*/
|
||||
const accessCyclicStructure = (accessor, structure) => {
|
||||
while (true) {
|
||||
if ("SerializableValue" in accessor) {
|
||||
return JSON.stringify(structure);
|
||||
} else if ("ObjectField" in accessor) {
|
||||
const [field, childAccessor] = accessor.ObjectField;
|
||||
structure = structure[field];
|
||||
accessor = childAccessor;
|
||||
} else if ("ArrayIndex" in accessor) {
|
||||
const [index, childAccessor] = accessor.ArrayIndex;
|
||||
structure = structure[index];
|
||||
accessor = childAccessor;
|
||||
}
|
||||
throw new Error("Invalid CyclicStructureAccessor");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {number} handlerId
|
||||
*/
|
||||
const findListener = (element, handlerId) => {
|
||||
const listener = listeners[handlerId];
|
||||
if (listener) {
|
||||
return listener;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Event listener #${handlerId} not found. This is a bug in virtual-dom, not your app!` +
|
||||
"It should have been on this node:\n" +
|
||||
element.outerHTML
|
||||
);
|
||||
}
|
||||
};
|
||||
"""
|
Loading…
Reference in New Issue
Block a user