Skip to content

Instantly share code, notes, and snippets.

@majo44
Last active February 19, 2022 05:00
Show Gist options
  • Save majo44/564e1cfb969d7218d00b6ea2e8a82ca5 to your computer and use it in GitHub Desktop.
Save majo44/564e1cfb969d7218d00b6ea2e8a82ca5 to your computer and use it in GitHub Desktop.
SAM (like) implementation by Proxy.
<!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>
// 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;
}
}
}
// 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);
});
<!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