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() |