Skip to content

Instantly share code, notes, and snippets.

@madx
Last active August 19, 2020 01:35
Show Gist options
  • Save madx/b3dbcfef36e9c13a6aa59e10106acc1d to your computer and use it in GitHub Desktop.
Save madx/b3dbcfef36e9c13a6aa59e10106acc1d to your computer and use it in GitHub Desktop.
Fluor 2

This is a draft of the next API for Fluor

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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment