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
The goal is to find a clear way to separate DOM mutations from external script and components.
Usually we can separate user DOM updates from component updates using light and shadow trees. However, this is not always possible, such as the case of amp-selector component.
Let's consider the amp-selector component. For the amp-selector, all mutations are in the main DOM tree and all observable by the main script/css/etc. E.g. if the CE user wants to decorated a selected option within the amp-selector, the following CSS can be used:
.option[selected] {
border: 1px solid red;
}
Currently this is virtually the only option for us to signify that a selection option (as a DOM element) is currently selected. This is definitely not great. It'd be much better from API point of view if we could set a custom state, e.g. .option:selected {...}. But custom states spec is still ways and ways away.
As a result, a subtree-based MutationObserver would see these mutations too. In other words the following are both valid code paths that mutate DOM:
In-component: user clicks on an option and the React component sets it as selected. Our Slot delegation updates DOM:
// A. React's onClick handler -> statefunctionAmpSelection.Option(){
...
(<xonClick={()=>setSelectedOptionState(props.option)}...>)
...
}// B. Slot's props.selected is set in React:functionAmpSelection.Option(){
...
(<Slotselected={isSelectedOptionState(props.option)}>)
...
}// C. Slot's side effect sets DOM attribute:functionSlot(){constdomRef=useRef();useEffect(()=>{constslot=domRef.current;
...
// Update in the main DOM:constassignedOption=slot.assignedElements()[0];if(props.selected){assignedOption.setAttribute('selected','');}else{assignedOption.removeAttribute('selected');}});
...
}
Out-of-component mutation: a user script in the main document manually writes DOM:
Update an attribute this way by the user script will trigger mutation observer and trigger React component re-rendering with the new value prop.
We need the mutation observer to synchronize DOM -> React component in the case /2/. But we don't really need mutation observer for /1/ since we ourselves ensure that DOM/React are in full sync. In general case, incorrectly working /1/ can cause cycles. So far the cycles in such mutations have been easy to work around or ignore. But in general case this is still a dangerous situation. It'd be nice to have a more "automatic" solution for this.
The text was updated successfully, but these errors were encountered:
A possibly much simpler approach - expected mutation matching:
Some internal mutations are ignored and thus can be done safely by components. These include i-amphtml attributes, --i-amphtml-x CSS vars in style, children with i-amphtml-x class or special attribute, i-amphtml-x attribute values (slot=i-amphtml-slide-1), etc. The expectation is that majority of mutations that need to be done in the light subtree can be done this way.
For other light tree mutations we will require a MutationRecord to be created and pushed for the target element into the "pending set". When the MutationObserver is notified with a mutation, it can match the MutationRecord against the "pending set", and ignore the record if a matching one exists.
For instance, the component's implementation modifies the light tree:
useEffect(() => {
const el = ref.current;
// This record will be ignore because the attribute's value
// starts with "i-amphtml-" prefix.
el.setAttribute('slot', 'i-amphtml-slot1');
// The argument is in the `MutationRecord` format.
// The `applyMutation` will actually apply the mutation, e.g. it will
// call `el.setAttribute('selected', '')`, but it will also record this
// mutation record in the "pending set".
applyMutation({target: el, type: 'attributes', attributeName: 'selected', value: ''});
});
The main idea here is that the light tree mutation are very rare and thus can take a bit more verbosity and indirection to execute to avoid confusion w.r.t. where a mutation comes from.
Context: #29 (comment)
The goal is to find a clear way to separate DOM mutations from external script and components.
Usually we can separate user DOM updates from component updates using light and shadow trees. However, this is not always possible, such as the case of
amp-selector
component.Let's consider the
amp-selector
component. For the amp-selector, all mutations are in the main DOM tree and all observable by the main script/css/etc. E.g. if the CE user wants to decorated a selected option within theamp-selector
, the following CSS can be used:Currently this is virtually the only option for us to signify that a selection option (as a DOM element) is currently selected. This is definitely not great. It'd be much better from API point of view if we could set a custom state, e.g.
.option:selected {...}
. But custom states spec is still ways and ways away.As a result, a subtree-based MutationObserver would see these mutations too. In other words the following are both valid code paths that mutate DOM:
Slot
delegation updates DOM:Update an attribute this way by the user script will trigger mutation observer and trigger React component re-rendering with the new
value
prop.We need the mutation observer to synchronize DOM -> React component in the case /2/. But we don't really need mutation observer for /1/ since we ourselves ensure that DOM/React are in full sync. In general case, incorrectly working /1/ can cause cycles. So far the cycles in such mutations have been easy to work around or ignore. But in general case this is still a dangerous situation. It'd be nice to have a more "automatic" solution for this.
The text was updated successfully, but these errors were encountered: