Last active
March 26, 2022 22:00
-
-
Save pablen/c07afa6a69291d771699b0e8c91fe547 to your computer and use it in GitHub Desktop.
DOM mutation observer helper that will run a hook when a DOM node matching a selector is mounted or unmounted. This pattern is particularly useful for working with external JS libraries in your Elm apps, using minimal amount of code. The helper leverages the MutationObserver API (https://developer.mozilla.org/es/docs/Web/API/MutationObserver).
This file contains hidden or 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
/** | |
* Initializes a DOM mutation observer that will run a hook when | |
* a DOM node matching a selector is mounted or unmounted. | |
* | |
* const myObs = createObserver({ | |
* selector: "[data-ace-editor]", | |
* onMount: node => {}, | |
* onUnmount: node => {} | |
* }); | |
*/ | |
const DEFAULT_ROOT_ELEMENT = document; | |
const DEFAULT_OBSERVER_CONFIG = { childList: true, subtree: true }; | |
const createObserver = config => { | |
const { | |
observerConfig = DEFAULT_OBSERVER_CONFIG, | |
rootElement = DEFAULT_ROOT_ELEMENT, | |
selector, | |
onMount, | |
onUnmount | |
} = config; | |
const observer = new MutationObserver(mutations => { | |
mutations.forEach(mutation => { | |
// Handle added nodes | |
if (onMount) { | |
mutation.addedNodes.forEach(addedNode => { | |
const matchingElements = getMatchingElementsFromTree(addedNode, selector); | |
if (matchingElements.length < 1) return; | |
matchingElements.forEach(node => onMount(node)); | |
}); | |
} | |
// Handle removed nodes | |
if (onUnmount) { | |
mutation.removedNodes.forEach(removedNode => { | |
const matchingElements = getMatchingElementsFromTree(removedNode, selector); | |
if (matchingElements.length < 1) return; | |
matchingElements.forEach(node => onUnmount(node)); | |
}); | |
} | |
}); | |
}); | |
observer.observe(rootElement, observerConfig); | |
return observer; | |
}; | |
// Returns an iterator containing elements that were part of a DOM mutation & matches the selector | |
const getMatchingElementsFromTree = (rootElement, selector) => { | |
return rootElement.querySelectorAll && rootElement.matches | |
? rootElement.matches(selector) ? [rootElement] : rootElement.querySelectorAll(selector) | |
: []; | |
}; | |
export default createObserver; |
This file contains hidden or 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
/** | |
* Initialize an ACE editor each time a node with an attribute data-ace="some-id" is mounted. | |
* Also destroys the editor instance when the node in unmounted. | |
* | |
* Pro Tip: Pass editor options as extra data-some-option attributes (see below). | |
*/ | |
import createObserver from './createObserver.js'; | |
var rootElement = document.getElementById('root'); | |
createObserver({ | |
rootElement, | |
selector: '[data-ace]', | |
onMount: initAce, | |
onUnmount: killAce | |
}); | |
// onMount hook. Use it for initializing your things. The mounted node is passed as an argument. | |
function initAce(node) { | |
const langTools = ace.require('ace/ext/language_tools'); | |
const editor = ace.edit(node); | |
editor.$blockScrolling = Infinity; | |
// set theme by adding a data-ace-theme attribute to the node | |
const theme = node.getAttribute('data-ace-theme'); | |
if (theme) editor.setTheme(`ace/theme/${theme}`); | |
// set mode by adding a data-ace-mode attribute to the node | |
const mode = node.getAttribute('data-ace-mode'); | |
if (mode) editor.getSession().setMode(`ace/mode/${mode}`); | |
// set an initial value by adding a data-ace-theme attribute to the node | |
const initialValue = node.getAttribute('data-ace-initial-value'); | |
if (initialValue) editor.setValue(initialValue); | |
editor.setOptions({ | |
enableBasicAutocompletion: true, | |
enableSnippets: true, | |
enableLiveAutocompletion: true | |
}); | |
// set custom autocomplete values by adding a data-ace-completer to the node and separating values with pipes "|" | |
const completer = node.getAttribute('data-ace-completer'); | |
if (completer) { | |
langTools.addCompleter({ | |
getCompletions: function(editor, session, pos, prefix, callback) { | |
callback(null, completer.split('|').map(str => ({ name: str, value: str, score: 1, meta: str }))); | |
} | |
}); | |
} | |
// add you event listeners | |
editor.getSession().on('change', function(e) { | |
app.ports.codeChanged.send([node.id, editor.getSession().getValue()]); | |
}); | |
} | |
// onUnmount hook. You can use it for cleanup. The unmounted node is passed as an argument. | |
function killAce(node) { | |
const editor = ace.edit(node); | |
editor.destroy(); | |
} |
This file contains hidden or 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
-- Somewhere in you Elm app you can add editor by adding an empty node with the correct attributes. | |
-- The JS library will be initialized and destroyed automatically! | |
view : Model -> Html Msg | |
view model = | |
div [] | |
[ div | |
[ attribute "data-ace" "" | |
, attribute "data-ace-theme" "monokai" | |
, attribute "data-ace-mode" "javascript" | |
, attribute "data-ace-completer" "foo|bar|baz" | |
, attribute "data-ace-initial-value" "some initial value for the editor" | |
, id "some-unique-id" | |
] | |
[] | |
] |
This file contains hidden or 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
port module Ports exposing (..) | |
-- a port for receiving editors new values | |
port codeChanged : (( String, String ) -> msg) -> Sub msg |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment