Skip to content

Instantly share code, notes, and snippets.

@pablen
Last active March 26, 2022 22:00
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pablen/c07afa6a69291d771699b0e8c91fe547 to your computer and use it in GitHub Desktop.
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).
/**
* 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;
/**
* 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();
}
-- 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"
]
[]
]
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