The example is based on Alpine.js's memory card game example
The API is not final, expect a lot of things to change. It is 3:30AM I'll add more to this later, gotta get some sleep first
The example is based on Alpine.js's memory card game example
The API is not final, expect a lot of things to change. It is 3:30AM I'll add more to this later, gotta get some sleep first
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Document</title> | |
| <link | |
| href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" | |
| rel="stylesheet" | |
| /> | |
| </head> | |
| <body> | |
| <!-- Memory Game --> | |
| <div class="px-10 flex items-center justify-center min-h-screen"> | |
| <h1 class="fixed top-0 right-0 p-10 font-bold text-3xl"> | |
| <span data-bind="points"></span> | |
| <span class="text-xs">pts</span> | |
| </h1> | |
| <div data-bind="cards" class="flex-1 grid grid-cols-4 gap-10"> | |
| <template data-template="card"> | |
| <div> | |
| <button class="w-full h-32 bg-red-500"></button> | |
| </div> | |
| </template> | |
| </div> | |
| <pre></pre> | |
| </div> | |
| </body> | |
| <script type="fluor"> | |
| function pause(milliseconds = 1000) { | |
| return new Promise((resolve) => setTimeout(resolve, milliseconds)) | |
| } | |
| let cardTemplate = template("card") | |
| let cards = variable([ | |
| { color: "green", flipped: false, cleared: false }, | |
| { color: "red", flipped: false, cleared: false }, | |
| { color: "blue", flipped: false, cleared: false }, | |
| { color: "yellow", flipped: false, cleared: false }, | |
| { color: "green", flipped: false, cleared: false }, | |
| { color: "red", flipped: false, cleared: false }, | |
| { color: "blue", flipped: false, cleared: false }, | |
| { color: "yellow", flipped: false, cleared: false }, | |
| ].sort(() => Math.random() - 0.5)) | |
| let flippedCards = dynamic([cards], cards => cards.filter(c => c.flipped)) | |
| let remaining = dynamic([cards], cards => cards.filter(c => !c.cleared).length) | |
| let points = dynamic([cards], cards => cards.filter(c => c.cleared).length) | |
| bind.text("@points", points) | |
| bind.collection("@cards", cards, cardTemplate, (card, item) => { | |
| let color = item.variable(() => card.flipped ? card.color : "#999") | |
| let display = item.variable(() => card.cleared ? "none" : null) | |
| let disabled = item.variable(() => get(flippedCards).length >= 2 ? "disabled" : false) | |
| item.bind.style("button", color, "background") | |
| item.bind.style("button", display, "display") | |
| item.bind.attr("button", disabled, "disabled") | |
| item.on("click", "button", () => flipCard(card)) | |
| }) | |
| async function flipCard(card) { | |
| card.flipped = !card.flipped | |
| render(cards) | |
| if (get(flippedCards).length !== 2) return | |
| await pause() | |
| if (hasMatch()) { | |
| update(cards, cards => cards.map( | |
| c => (c.cleared = c.cleared || (get(flippedCards).includes(c)), c) | |
| )) | |
| if (get(remaining) === 0) { | |
| alert("You won!") | |
| } | |
| } | |
| update(cards, cards => cards.map(c => (c.flipped = false, c))) | |
| } | |
| function hasMatch() { | |
| const flipped = get(flippedCards) | |
| return flipped[0].color === flipped[1].color | |
| } | |
| </script> | |
| <script src="./fluor.js"></script> | |
| </html> |
| function $(selector, root = document) { | |
| if (selector instanceof Node) { | |
| return [selector] | |
| } | |
| return root.querySelectorAll(selector) | |
| } | |
| function $$(selector, root = document) { | |
| if (selector instanceof Node) { | |
| return selector | |
| } | |
| return root.querySelector(selector) | |
| } | |
| function isFunction(object) { | |
| return Boolean(object && object.constructor && object.call && object.apply) | |
| } | |
| function createRuntime($root = document.body) { | |
| const $variables = new Map() | |
| const $bindings = new Map() | |
| function variable(value) { | |
| const variable = { value, deps: new Set() } | |
| $variables.set(variable, value) | |
| return variable | |
| } | |
| function dynamic(dependencies, reducer = (...args) => args) { | |
| const dynamicVar = variable(() => | |
| reducer( | |
| ...dependencies.map((d) => (isFunction(d.value) ? d.value() : d.value)) | |
| ) | |
| ) | |
| dependencies.forEach((dep) => dep.deps.add(dynamicVar)) | |
| return dynamicVar | |
| } | |
| function template(name) { | |
| const t = $$(`[data-template="${name}"]`) | |
| const cached = t.cloneNode(true) | |
| t.parentNode.removeChild(t) | |
| return cached | |
| } | |
| function bindLowLevel(type, selector, variable, ...args) { | |
| if (selector.startsWith("@")) { | |
| selector = `[data-bind="${selector.slice(1)}"]` | |
| } | |
| const binding = { type, variable, args } | |
| if ($bindings.has(selector)) { | |
| $bindings.get(selector).push(binding) | |
| } else { | |
| $bindings.set(selector, [binding]) | |
| } | |
| } | |
| const bind = new Proxy( | |
| {}, | |
| { | |
| get(obj, key) { | |
| return (...args) => bindLowLevel(key, ...args) | |
| }, | |
| } | |
| ) | |
| function get(variable) { | |
| const value = $variables.get(variable) | |
| return isFunction(value) ? value() : value | |
| } | |
| function set(variable, value) { | |
| variable.value = value | |
| $variables.set(variable, value) | |
| render(variable) | |
| } | |
| function update(variable, updater) { | |
| set(variable, updater(get(variable))) | |
| } | |
| function refresh(variable) { | |
| set(variable, get(variable)) | |
| } | |
| function add(numericVariable, value = 1) { | |
| set(numericVariable, get(numericVariable) + value) | |
| } | |
| function sub(numericVariable, value = 1) { | |
| add(numericVariable, -value) | |
| } | |
| function on(event, selector, baseHandler) { | |
| const handler = (ev) => { | |
| const matchedTarget = | |
| selector instanceof Node ? selector : ev.target.closest(selector) | |
| if (matchedTarget && $root.contains(matchedTarget)) { | |
| baseHandler(ev, matchedTarget) | |
| } | |
| } | |
| $root.addEventListener(event, handler) | |
| } | |
| function show(selector) { | |
| $(selector).forEach((el) => (el.style.display = null)) | |
| } | |
| function hide(selector) { | |
| $(selector).forEach((el) => (el.style.display = "none")) | |
| } | |
| function render(variable = null) { | |
| for (const [selector, bindings] of $bindings) { | |
| for (const binding of bindings) { | |
| if (variable && binding.variable !== variable) { | |
| continue | |
| } | |
| const value = get(binding.variable) | |
| for (const el of $(selector, $root)) { | |
| switch (binding.type) { | |
| case "text": | |
| el.textContent = value | |
| break | |
| case "attr": | |
| { | |
| const [attribute] = binding.args | |
| switch (value) { | |
| case true: | |
| el.setAttribute(attribute, "") | |
| break | |
| case false: | |
| el.removeAttribute(attribute) | |
| break | |
| default: | |
| el.setAttribute(attribute, value) | |
| } | |
| } | |
| break | |
| case "html": | |
| el.innerHTML = value | |
| break | |
| case "json": | |
| el.textContent = JSON.stringify(value, null, 2) | |
| break | |
| case "style": { | |
| const [style] = binding.args | |
| el.style[style] = value | |
| break | |
| } | |
| case "collection": | |
| { | |
| const [template, runtimeHandler] = binding.args | |
| const fragment = document.createDocumentFragment() | |
| const instances = value.map((item) => { | |
| const clone = template.content.cloneNode(true) | |
| const root = clone.firstElementChild | |
| const runtime = createRuntime(root) | |
| fragment.appendChild(clone) | |
| return { item, runtime, root } | |
| }) | |
| while (el.firstChild) { | |
| el.removeChild(el.firstChild) | |
| } | |
| el.appendChild(fragment) | |
| if (runtimeHandler) { | |
| for (const instance of instances) { | |
| runtimeHandler(instance.item, instance.runtime) | |
| instance.runtime.render() | |
| } | |
| } | |
| } | |
| break | |
| } | |
| } | |
| } | |
| } | |
| if (variable) { | |
| variable.deps.forEach((d) => render(d)) | |
| } | |
| } | |
| return { | |
| $variables, | |
| $bindings, | |
| $root, | |
| variable, | |
| dynamic, | |
| template, | |
| bind, | |
| get, | |
| set, | |
| update, | |
| refresh, | |
| add, | |
| sub, | |
| on, | |
| render, | |
| show, | |
| hide, | |
| } | |
| } | |
| window.RunFluor = function RunFluor(atomCode) { | |
| atomCode(createRuntime()) | |
| } | |
| async function main() { | |
| // Wait for the DOM to be ready! | |
| await new Promise((resolve) => { | |
| if (document.readyState == "loading") { | |
| document.addEventListener("DOMContentLoaded", resolve) | |
| } else { | |
| resolve() | |
| } | |
| }) | |
| const scripts = $("script[type='fluor']") | |
| const fragment = document.createDocumentFragment() | |
| for (const script of scripts) { | |
| const newScript = document.createElement("script") | |
| newScript.textContent = `RunFluor(async ({${Object.keys( | |
| createRuntime() | |
| ).join(", ")}}) => {${script.textContent};render()})` | |
| script.parentNode.removeChild(script) | |
| fragment.appendChild(newScript) | |
| } | |
| document.body.appendChild(fragment) | |
| } | |
| main() |