Last active
February 19, 2022 05:00
-
-
Save majo44/564e1cfb969d7218d00b6ea2e8a82ca5 to your computer and use it in GitHub Desktop.
SAM (like) implementation by Proxy.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Proxy Sam ?</title> | |
</head> | |
<body> | |
<script> | |
/// UTILS CODE | |
// Build model just by cloning initial state, | |
// and wrapping it in Proxies with middlewares as handlers, | |
// this can be done even without Proxies but this is simplest way | |
function model(initState, middlewares) { | |
return middlewares.reduceRight( | |
(result, m) => new Proxy(result, m), | |
{state: Object.assign({},initState)}); | |
} | |
// APP CODE | |
const MAX_COUNT = 10; | |
// Initial state | |
let launcherInitState = { | |
counter: MAX_COUNT, | |
launched: false, | |
aborted: false, | |
started: false | |
}; | |
// Middleware for accepting/rejecting state proposal, keep the system state predicable, can: | |
// check new proposal, | |
// check that system can go to next state (comparing current state with proposal), | |
// ... | |
let acceptorMiddleware = { | |
set: (target, p, value) => { | |
if ((value.launched && !value.started) || | |
(value.aborted && (value.launched || value.started)) || | |
(value.counter < 0 || value.counter > MAX_COUNT)) { | |
throw 'Unaccepted state !'; | |
} | |
if (!target.state.aborted) { | |
target.state = value; | |
} | |
return true; | |
} | |
}; | |
// Next action predicable middleware, auto executing next action | |
let napMiddleware = { | |
set: (target, p, value) => { | |
target.state = value; | |
if (value.started && !value.launched) { | |
if (value.counter > 0) { | |
decrementAction(); | |
} else { | |
lunchAction(); | |
} | |
} | |
return true; | |
} | |
}; | |
// Render middleware, in general triggers render on any state change | |
let renderMiddleware = { | |
set: (target, p, value) => { | |
target.state = value; | |
view(value); | |
return true; | |
} | |
}; | |
// Model is a composition of initial state, and middlewares | |
let launcherModel = model( | |
launcherInitState, [ | |
acceptorMiddleware, | |
napMiddleware, | |
renderMiddleware | |
]); | |
// Action is just setting new proposal to model | |
function startAction() { | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
started: true | |
}); | |
} | |
function decrementAction() { | |
setTimeout(() =>{ | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
counter: launcherModel.state.counter - 1 | |
}); | |
}, 300); | |
} | |
function lunchAction() { | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
launched: true | |
}); | |
} | |
function abortAction() { | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
aborted: true, | |
started: false | |
}); | |
} | |
function view(state) { | |
let html; | |
if (state.aborted) { | |
html = `Aborted at ${state.counter}`; | |
} else if (state.started) { | |
if (state.launched) { | |
html = `Launched` | |
} else { | |
html = `Counting ${state.counter} <button onclick="abortAction()">Abort</button>` | |
} | |
} else { | |
html = `<button onclick="startAction()">Start</button>` | |
} | |
document.body.innerHTML = html; | |
} | |
view(launcherModel.state); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// BONUS 1 | |
// example how to create middleware which after each mutation makes | |
// the (first level of) state immutable | |
function readonlyMiddleware() { | |
return { | |
set: (target, key, value) => { | |
target.state = new Proxy(value, { | |
set: () => { | |
throw 'State is readonly'; | |
} | |
}); | |
return true; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// BONUS 2 | |
// state forking, this can be used eg. for server side rendering | |
// depends on Zone.js | |
function getFork() { | |
return Zone.current.get('fork'); | |
} | |
function inFork() { | |
return !!getFork(); | |
} | |
function forkMiddleware() { | |
return { | |
get: (target) => { | |
if (inFork() && getFork().state) { | |
return getFork().state; | |
} else { | |
return target.state; | |
} | |
}, | |
set: (target, p, value) => { | |
if (inFork()) { | |
getFork().state = value; | |
getFork().target = target; | |
} else { | |
target.state = value; | |
} | |
return true; | |
} | |
} | |
} | |
function fork(fn) { | |
return Zone.current.fork({ | |
name: 'fork', | |
properties: { | |
fork: {} | |
} | |
}).run(() => { | |
return fn(); | |
}); | |
} | |
// usage | |
fork(() => { | |
view(state); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Proxy Sam ?</title> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/zone.js/0.7.7/zone.min.js"></script> | |
</head> | |
<body> | |
<div id="host1"></div> | |
<div id="host2"></div> | |
<script> | |
/// LIB CODE | |
function getFork() { | |
return Zone.current.get('fork'); | |
} | |
function inFork() { | |
return !!getFork(); | |
} | |
let forkMiddleware = { | |
get: (target) => { | |
if (inFork() && getFork().state) { | |
return getFork().state; | |
} else { | |
return target.state; | |
} | |
}, | |
set: (target, p, value) => { | |
if (inFork()) { | |
getFork().state = value; | |
getFork().target = target; | |
} else { | |
target.state = value; | |
} | |
return true; | |
} | |
}; | |
function fork(fn) { | |
return Zone.current.fork({ | |
name: 'fork', | |
properties: { | |
fork: {} | |
} | |
}).run(() => { | |
return fn(); | |
}); | |
} | |
function model(initState, middlewares) { | |
return middlewares.reduceRight( | |
(result, m) => new Proxy(result, m), | |
{state: initState}); | |
} | |
// APP CODE | |
const MAX_COUNT = 10; | |
// Initial state | |
let launcherInitState = { | |
counter: MAX_COUNT, | |
launched: false, | |
aborted: false, | |
started: false | |
}; | |
// Middleware for accepting/rejecting state proposal, keep the system state predicable, can: | |
// check new proposal, | |
// check that system can go to next state (comparing current state with proposal), | |
// ... | |
let acceptorMiddleware = { | |
set: (target, p, value) => { | |
if ((value.launched && !value.started) || | |
(value.aborted && (value.launched || value.started)) || | |
(value.counter < 0 || value.counter > MAX_COUNT)) { | |
throw 'Unaccepted state !'; | |
} | |
if (!target.state.aborted) { | |
target.state = value; | |
} | |
return true; | |
} | |
}; | |
// Next action predicable middleware, auto executing next action | |
let napMiddleware = { | |
set: (target, p, value) => { | |
target.state = value; | |
if (value.started && !value.launched) { | |
if (value.counter > 0) { | |
decrementAction(); | |
} else { | |
lunchAction(); | |
} | |
} | |
return true; | |
} | |
}; | |
// Render middleware, in general triggers render on any state change | |
let renderMiddleware = { | |
set: (target, p, value) => { | |
target.state = value; | |
view(); | |
return true; | |
} | |
}; | |
// Model is a composition of initial state, and middlewares | |
let launcherModel = model( | |
launcherInitState, [ | |
acceptorMiddleware, | |
napMiddleware, | |
renderMiddleware, | |
forkMiddleware | |
]); | |
// Action is just setting new proposal to model | |
function startAction() { | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
started: true | |
}); | |
} | |
function decrementAction() { | |
setTimeout(() =>{ | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
counter: launcherModel.state.counter - 1 | |
}); | |
}, 1000); | |
} | |
function lunchAction() { | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
launched: true | |
}); | |
} | |
function abortAction() { | |
launcherModel.state = Object.assign({}, launcherModel.state, { | |
aborted: true, | |
started: false | |
}); | |
} | |
function render(host) { | |
let state = launcherModel.state; | |
let html; | |
if (state.aborted) { | |
html = `Aborted at ${state.counter}`; | |
} else if (state.started) { | |
if (state.launched) { | |
html = `Launched` | |
} else { | |
html = `Counting ${state.counter} <button class="abort">Abort</button>` | |
} | |
} else { | |
html = `<button class="start">Start</button>` | |
} | |
host.innerHTML = html; | |
let abort = host.querySelector('.abort'); | |
abort && abort.addEventListener('click', abortAction); | |
let start = host.querySelector('.start'); | |
start && start.addEventListener('click', startAction); | |
} | |
let view1 = () => render(document.querySelector('#host1')); | |
let view2 = () => render(document.querySelector('#host2')); | |
let view1Zone; | |
let view2Zone; | |
let view = () => { | |
view1Zone.run(view1); | |
view2Zone.run(view2); | |
}; | |
fork(() => { | |
view1Zone = Zone.current; | |
view1(); | |
}); | |
fork(() => { | |
view2Zone = Zone.current; | |
view2(); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment