You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Current behavior: @children and @child decorators do not support containerless custom elements and template controller custom attributes (which in fact are shortcuts to containerless custom elements)
Expected/desired behavior:
It is easy to track children of containerless elements. When @containerless element or @templateController custom attribute is created - it is attached to <!--anchor--> Comment DOM node . You can get this node before any children of containerless element are created by injecting Element to constructor. Later all children will be inserted before this <!--anchor--> node by Aurelia engine. So it is possible to insert another Comment node <!--@children--> before <!--anchor--> node, and later all children will be added to DOM tree between those two Comment nodes. And here we can add MutationObserver to <!--anchor--> parent node and use compareDocumentPosition() DOM API to filter children of containerless element.
Here is a modified version of ChildObserverBinder that is used by @children decorator from aurelia-templating/src/child-observation.ts that implements the above logic:
child-observation.js
import{DOM}from'aurelia-pal';import{metadata}from'aurelia-metadata';import{HtmlBehaviorResource}from'aurelia-templating';/*import { Controller } from './controller';import { SlotMarkedNode } from './type-extension';import { ShadowSlot } from './shadow-dom';*/functioncreateChildObserverDecorator(selectorOrConfig,all){returnfunction(target,key,descriptor){letactualTarget=typeofkey==='string' ? target.constructor : target;letr=metadata.getOrCreateOwn(metadata.resource,HtmlBehaviorResource,actualTarget);if(typeofselectorOrConfig==='string'){selectorOrConfig={selector: selectorOrConfig,name: key};}if(descriptor){descriptor.writable=true;descriptor.configurable=true;}selectorOrConfig.all=all;r.addChildBinding(newChildObserver(selectorOrConfig));};}exportfunctionchildren(selectorOrConfig){returncreateChildObserverDecorator(selectorOrConfig,true);}exportfunctionchild(selectorOrConfig){returncreateChildObserverDecorator(selectorOrConfig,false);}exportfunctionbindChildObserver(selector,viewModel,propName,element,all=true){constobserver=newChildObserver({
selector,name: propName,
all
});constbinder=observer.create(element,viewModel);binder.bind(viewModel);if(!Array.isArray(viewModel[propName]))viewModel[propName]=[];viewModel[propName].unbind=()=>{binder.unbind()}}classChildObserver{constructor(config){this.name=config.name;this.changeHandler=config.changeHandler||this.name+'Changed';this.selector=config.selector;this.all=config.all;}create(viewHost,viewModel,controller){returnnewChildObserverBinder(this.selector,viewHost,this.name,viewModel,controller,this.changeHandler,this.all);}}constnoMutations=[];functiontrackMutation(groupedMutations,binder,record){letmutations=groupedMutations.get(binder);if(!mutations){mutations=[];groupedMutations.set(binder,mutations);}mutations.push(record);}functiononChildChange(mutations,observer){letbinders=observer.binders;letbindersLength=binders.length;letgroupedMutations=newMap();for(leti=0,ii=mutations.length;i<ii;++i){letrecord=mutations[i];letadded=record.addedNodes;letremoved=record.removedNodes;for(letj=0,jj=removed.length;j<jj;++j){letnode=removed[j];if(node.nodeType===1){for(letk=0;k<bindersLength;++k){letbinder=binders[k];if(binder.onRemove(node)){trackMutation(groupedMutations,binder,record);}}}}for(letj=0,jj=added.length;j<jj;++j){letnode=added[j];if(node.nodeType===1){for(letk=0;k<bindersLength;++k){letbinder=binders[k];if(binder.onAdd(node)){trackMutation(groupedMutations,binder,record);}}}}}groupedMutations.forEach((mutationRecords,binder)=>{if(binder.isBound&&binder.changeHandler!==null){binder.viewModel[binder.changeHandler](mutationRecords);}});}classChildObserverBinder{constructor(selector,viewHost,property,viewModel,controller,changeHandler,all){this.selector=selector;this.viewHost=viewHost;this.property=property;this.viewModel=viewModel;this.controller=controller;this.changeHandler=changeHandlerinviewModel ? changeHandler : null;this.all=all;this.contentView=null;if(controller){this.usesShadowDOM=controller.behavior.usesShadowDOM;if(!this.usesShadowDOM&&controller.view&&controller.view.contentView){this.contentView=controller.view.contentView;}else{this.contentView=null;}}else{this.contentView=null;}this.source=null;this.isBound=false;}matches(element){constviewHost=this.viewHost;if(viewHost.__childrenStartNode__){// Extra check for containerless children supportletstart=viewHost.__childrenStartNode__;if(!(element.isSameNode(start)||element.isSameNode(viewHost)||(element.compareDocumentPosition(start)&Node.DOCUMENT_POSITION_PRECEDING&&element.compareDocumentPosition(viewHost)&Node.DOCUMENT_POSITION_FOLLOWING)))returnfalse;}if(element.matches(this.selector)){if(this.contentView===null){returntrue;}letcontentView=this.contentView;letassignedSlot=element.auAssignedSlot;if(assignedSlot&&assignedSlot.projectFromAnchors){letanchors=assignedSlot.projectFromAnchors;for(leti=0,ii=anchors.length;i<ii;++i){if(anchors[i].auOwnerView===contentView){returntrue;}}returnfalse;}returnelement.auOwnerView===contentView;}returnfalse;}bind(source){if(this.isBound){if(this.source===source){return;}this.source=source;}this.isBound=true;letviewHost=this.viewHost;letviewModel=this.viewModel;letobserver=viewHost.__childObserver__;if(!observer){observer=viewHost.__childObserver__=DOM.createMutationObserver(onChildChange);letoptions={childList: true,subtree: !this.usesShadowDOM};letobserverHost;// support containerlessif(viewHostinstanceofElement)// viewHost is containerobserverHost=viewHostelse{// viewHost is Comment node - containerless view hostobserverHost=viewHost.parentNode;constchildrenStartNode=DOM.createComment('@children');viewHost.parentNode.insertBefore(childrenStartNode,viewHost);viewHost.__childrenStartNode__=childrenStartNode;}observer.observe(observerHost,options);observer.binders=[];}observer.binders.push(this);if(this.usesShadowDOM){letcurrent=viewHost.firstElementChild;if(this.all){letitems=viewModel[this.property];if(!items){items=viewModel[this.property]=[];}else{items.splice(0);}while(current){if(this.matches(current)){items.push(current.au&¤t.au.controller ? current.au.controller.viewModel : current);}current=current.nextElementSibling;}if(this.changeHandler!==null){this.viewModel[this.changeHandler](noMutations);}}else{while(current){if(this.matches(current)){letvalue=current.au&¤t.au.controller ? current.au.controller.viewModel : current;this.viewModel[this.property]=value;if(this.changeHandler!==null){this.viewModel[this.changeHandler](value);}break;}current=current.nextElementSibling;}}}}onRemove(element){if(this.matches(element)){letvalue=element.au&&element.au.controller ? element.au.controller.viewModel : element;if(this.all){letitems=(this.viewModel[this.property]||(this.viewModel[this.property]=[]));letindex=items.indexOf(value);if(index!==-1){items.splice(index,1);}returntrue;}constcurrentValue=this.viewModel[this.property];if(currentValue===value){this.viewModel[this.property]=null;if(this.isBound&&this.changeHandler!==null){this.viewModel[this.changeHandler](value);}}}returnfalse;}onAdd(element){if(this.matches(element)){letvalue=element.au&&element.au.controller ? element.au.controller.viewModel : element;if(this.all){letitems=(this.viewModel[this.property]||(this.viewModel[this.property]=[]));if(this.selector==='*'){items.push(value);returntrue;}letindex=0;letprev=element.previousElementSibling;while(prev){if(this.matches(prev)){index++;}prev=prev.previousElementSibling;}items.splice(index,0,value);returntrue;}this.viewModel[this.property]=value;if(this.isBound&&this.changeHandler!==null){this.viewModel[this.changeHandler](value);}}returnfalse;}unbind(){if(!this.isBound){return;}this.isBound=false;this.source=null;letchildObserver=this.viewHost.__childObserver__;if(childObserver){letbinders=childObserver.binders;if(binders&&binders.length){letidx=binders.indexOf(this);if(idx!==-1){binders.splice(idx,1);}if(binders.length===0){childObserver.disconnect();this.viewHost.__childObserver__=null;}}if(this.usesShadowDOM){this.viewModel[this.property]=null;}}}}
Modified matcher:
matches(element){constviewHost=this.viewHost;if(viewHost.__childrenStartNode__){// Extra check for containerless children supportletstart=viewHost.__childrenStartNode__;if(!(element.isSameNode(start)||element.isSameNode(viewHost)||(element.compareDocumentPosition(start)&Node.DOCUMENT_POSITION_PRECEDING&&element.compareDocumentPosition(viewHost)&Node.DOCUMENT_POSITION_FOLLOWING)))returnfalse;}
And modified MutationObserver creation:
if(!observer){observer=viewHost.__childObserver__=DOM.createMutationObserver(onChildChange);letoptions={childList: true,subtree: !this.usesShadowDOM};letobserverHost;// support containerlessif(viewHostinstanceofElement)// viewHost is containerobserverHost=viewHostelse{// viewHost is Comment node - containerless view hostobserverHost=viewHost.parentNode;constchildrenStartNode=DOM.createComment('@children');viewHost.parentNode.insertBefore(childrenStartNode,viewHost);viewHost.__childrenStartNode__=childrenStartNode;}observer.observe(observerHost,options);
But the above modification does not work inside @children decorator, because ChildObserver.create(viewHost, viewModel, controller) method is never called when decorator is applied to @containerless element.
I added extra function to use this technic in constructor without decorator:
exportfunctionbindChildObserver(selector,viewModel,propName,element,all=true){constobserver=newChildObserver({
selector,name: propName,
all
});constbinder=observer.create(element,viewModel);binder.bind(viewModel);if(!Array.isArray(viewModel[propName]))viewModel[propName]=[];viewModel[propName].unbind=()=>{binder.unbind()}}
I'm submitting a feature request
1.4.1
Current behavior:
@children
and@child
decorators do not support containerless custom elements and template controller custom attributes (which in fact are shortcuts to containerless custom elements)Expected/desired behavior:
It is easy to track children of containerless elements. When
@containerless
element or@templateController
custom attribute is created - it is attached to<!--anchor-->
Comment DOM node . You can get this node before any children of containerless element are created by injectingElement
to constructor. Later all children will be inserted before this<!--anchor-->
node by Aurelia engine. So it is possible to insert another Comment node<!--@children-->
before<!--anchor-->
node, and later all children will be added to DOM tree between those two Comment nodes. And here we can addMutationObserver
to<!--anchor-->
parent node and usecompareDocumentPosition()
DOM API to filter children of containerless element.Here is a modified version of
ChildObserverBinder
that is used by@children
decorator fromaurelia-templating/src/child-observation.ts
that implements the above logic:child-observation.js
Modified matcher:
And modified
MutationObserver
creation:But the above modification does not work inside
@children
decorator, becauseChildObserver.create(viewHost, viewModel, controller)
method is never called when decorator is applied to@containerless
element.I added extra function to use this technic in constructor without decorator:
Usage example:
The text was updated successfully, but these errors were encountered: