(() => { var observer = new MutationObserver( handleMutations ); var root = document.body; // Start watching for changes on the DOM tree. observer.observe( root, { // Watch for nodes added and removed. childList: true, // Watch for descendant changes deep in the observed root. subtree: true } ); // Bind controllers within the initial DOM structure. handleNodesAdded([ root ]); // --- // PRIVATE METHODS. // --- /** * When the DOM is mutated, the Observer only sees the local "roots" that were changed. * This method expands those local roots to include any nested nodes of interest (ie, * nodes those that represent x-controllers and x-refs). */ function expandNodesOfInterest( nodes ) { var nodesOfInterest = []; for ( var node of nodes ) { // MutationObserver reports TEXT node changes and COMMENT node changes. But, // we only care about ELEMENT changes. if ( node.nodeType !== Node.ELEMENT_NODE ) { continue; } // Collect "self" nodes of interest. if ( node.hasAttribute( "x-controller" ) || node.hasAttribute( "x-ref" ) ) { nodesOfInterest.push( node ); } // Collect nested nodes of interest. nodesOfInterest.push( ...node.querySelectorAll( "[x-controller], [x-ref]" ) ); } return nodesOfInterest; } /** * I handle DOM mutations and bind and unbind controllers as necessary. Note that only * element-level changes are being observed in this exploration. Dynamically mutated * attributes will not be noticed (ie, if you dynamically add "x-controller" to an * existing element, nothing will happen). */ function handleMutations( mutationList ) { for ( var mutation of mutationList ) { switch ( mutation.type ) { case "childList": handleNodesRemoved( mutation.removedNodes ); handleNodesAdded( mutation.addedNodes ); break; // Other [type] values are "attributes", "characterData". } } } /** * I handle the new nodes, instantiating controllers and injecting refs. */ function handleNodesAdded( nodes ) { var controllers = []; // MutationObserver only sees the "local root" of a newly added tree branch. But, // we need to know about all of the relevant nodes within the new tree branch. As // such, we must expand our view of the new nodes. for ( var node of expandNodesOfInterest( nodes ) ) { if ( node.hasAttribute( "x-controller" ) ) { // All controllers are defined as a dot-delimited object path. var controllerPath = node.getAttribute( "x-controller" ); var constructor = reduceControllerPath( controllerPath ); var controller = node._x_controller = new constructor( node ); controller.refs = ( controller.refs || Object.create( null ) ); controllers.push( controller ); } if ( node.hasAttribute( "x-ref" ) ) { // All references are defined as a "scope.name" two-segment path. var refPath = node.getAttribute( "x-ref" ); var parts = refPath.split( "." ); var scopeName = parts[ 0 ]; var refName = parts[ 1 ]; // Find the closest controller with the given scope name. This may be a // controller that was just added; or, it may be one that was previously // created in a different DOM mutation. var controller = node.closest( `[x-scope=${ scopeName }]` )._x_controller; controller.refs[ refName ] = node; } } // Once we have all of our new controllers and new refs in place, call the init // life-cycle method on any new controllers. for ( var controller of controllers ) { controller?.$onInit( node ); } } /** * I unbind all controllers from the given removed DOM nodes. */ function handleNodesRemoved( nodes ) { // MutationObserver only sees the "local root" of a recently removed tree branch. // But, we need to know about all of the relevant nodes within the old tree // branch. As such, we must expand our view of the old nodes. for ( var node of expandNodesOfInterest( nodes ) ) { var controller = node._x_controller; // Teardown any controller bound to the given node. if ( controller ) { delete node._x_controller; controller?.$onDestroy( node ); } } } /** * I reduce the given dot-delimited controller path into a constructor reference (which * is assumed to be the last segment in the given path). */ function reduceControllerPath( path ) { return path.split( "." ).reduce( ( context, segment ) => { return context[ segment ]; }, window // Start reducing at the global context. ); } })();