Skip to content

Instantly share code, notes, and snippets.

@ramybenaroya
Last active July 19, 2018 08:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ramybenaroya/e1caa432bbdb3f7fe733005a6904d2c8 to your computer and use it in GitHub Desktop.
Save ramybenaroya/e1caa432bbdb3f7fe733005a6904d2c8 to your computer and use it in GitHub Desktop.
MobX for DOM Elements (https://codesandbox.io/s/wom6r2wr47)
<html>
<head>
<title>MobX DOM Example</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="videoControls">
Duration: <span id="videoControlsDuration"></span>
|
Muted: <span id="videoControlsMuted"></span>
|
Current Time: <span id="videoControlsCurrentTime"></span>
</div>
<script src="usage-example.js"></script>
</body>
</html>
import { observable, action, onBecomeObserved, onBecomeUnobserved } from "mobx";
const getChangeEvent = (attr, element) => {
let name;
if (attr === "value" || attr === "checked") {
name = "change";
} else {
name = `${attr}change`.toLowerCase();
}
return `on${name}` in element ? name : null;
};
const getAttrEntry = (attrs, attr, element) => {
const event = getChangeEvent(attr, element);
const entry = {
on: attrs[attr].on || (event && [event]),
property:
typeof attrs[attr].property === "boolean" ? attrs[attr].property : true,
attribute: typeof attrs[attr].attribute
? typeof attrs[attr].attribute
: attr !== "value"
};
return entry.on ? entry : null;
};
const normalizeAttrs = (options, element) => {
let attrs = options;
let normalized = {};
if (typeof attrs === "string") {
attrs = {
[attrs]: true
};
}
for (const attr in attrs) {
const entry = getAttrEntry(attrs, attr, element);
if (entry) {
normalized[attr] = entry;
}
}
return normalized;
};
const observableElement = (el, options) => {
const element = typeof el === "string" ? document.createElement(el) : el;
if (!(element instanceof Element)) {
throw new Error(
"Failed to observe element: The first argument must be an Element or string of Tag Name."
);
}
const observedAttrs = {};
const attrListeners = {};
const attrs = normalizeAttrs(options, element);
const attrsList = Object.keys(attrs);
let isMutationObserverActive = false;
const $element = observable(
attrsList.reduce(
(acc, attr) => {
acc[attr] = element[attr];
return acc;
},
{
element
}
)
);
const updateAttr = action((attr, value) => {
$element[attr] = value;
});
// Create an observer instance linked to the callback function
const mutationObserver = new MutationObserver(mutationsList => {
for (var mutation of mutationsList) {
if (
mutation.type === "attributes" &&
~attrsList.indexOf(mutation.attributeName) &&
attrs[mutation.attributeName].attribute
) {
updateAttr(mutation.attributeName, element[mutation.attributeName]);
}
}
});
const adjustListeners = () => {
if (Object.keys(observedAttrs).length > 0 && !isMutationObserverActive) {
isMutationObserverActive = true;
mutationObserver.observe(element, { attributes: true });
} else if (
Object.keys(observedAttrs).length === 0 &&
isMutationObserverActive
) {
isMutationObserverActive = false;
mutationObserver.disconnect();
}
for (const attr in attrListeners) {
if (!(attr in observedAttrs)) {
attrs[attr].on.forEach(event => {
element.removeEventListener(event, attrListeners[attr][event]);
});
delete attrListeners[attr];
}
}
for (const attr in observedAttrs) {
if (!(attr in attrListeners)) {
attrListeners[attr] = attrListeners[attr] || {};
attrs[attr].on.forEach(event => {
attrListeners[attr][event] = () => {
updateAttr(attr, element[attr]);
};
element.addEventListener(event, attrListeners[attr][event]);
});
}
}
};
attrsList.forEach(attr => {
onBecomeObserved($element, attr, () => {
observedAttrs[attr] = true;
adjustListeners();
});
});
attrsList.forEach(attr => {
onBecomeUnobserved($element, attr, () => {
delete observedAttrs[attr];
adjustListeners();
});
});
return $element;
};
export default observableElement;
import observableElement from "./observable-element";
import { autorun } from "mobx";
const observableVideo = observableElement("video", {
muted: {
attribute: true,
property: true,
on: ["volumechange"]
},
duration: true,
currentTime: {
on: ["timeupdate"]
}
});
const video = observableVideo.element;
video.height = 200;
video.controls = true;
video.src = "https://media.w3.org/2010/05/sintel/trailer.mp4";
autorun(() => {
window.videoControlsDuration.innerHTML = observableVideo.duration;
});
autorun(() => {
window.videoControlsMuted.innerHTML = observableVideo.muted;
});
autorun(() => {
window.videoControlsCurrentTime.innerHTML = observableVideo.currentTime;
});
document.body.appendChild(video);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment