Last active
July 19, 2018 08:54
-
-
Save ramybenaroya/e1caa432bbdb3f7fe733005a6904d2c8 to your computer and use it in GitHub Desktop.
MobX for DOM Elements (https://codesandbox.io/s/wom6r2wr47)
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
<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> |
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
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; |
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
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