RE: Style mutation observer

> Hey all,
> 
> I’ve been holding off on bringing this up because I don’t have a specific 
> suggestion, but I figure we should start talking about how to design 
> something like a style mutation observer.
> 

Hi Alan,

I did flag this mail when you initially sent it, but I decided to take some time to think about it before writing a reply ^_^ Please find my current thoughts below.



> CSS polyfills need to be able to respond to things like:
> 
> 1. Property value changes for a selector
> 
> The immediate need is for computed values. 

I would argue this probably isn't accurate, depending on what you mean by "computed value". A css implementation returns the used values (getComputedStyle) as a by-product of the layout process, not as an input. My belief is what we need for polyfills is the cascaded value or, as I call it, the "resolved value" (the cascaded value with references being replaced; i.e. stuff like 'variables' or 'attr(...')).



> Parsing code would also need 
> declared values, but that might be better exposed for rules rather than 
> selectors. 

CORS-free use of external stylesheets make this proposal hard to cope with in practice. 



> There should probably be a way to filter for particular 
> properties. 

Sure.



> Polyfills need to run code based on the values of their new 
> properties, and possibly on existing property values (like display:none) 
> that interact with the new properties.

Moreover, polyfills may need to add support for new values to properties that the browser already supports. That's the reason why I don't like the 'computed value' so much, because it's way too late to get the info we need.


Now, enough arguments; let's roll out some proposals. I decided that instead of simply providing what I believe is a good starting point for a solution to the problem, I would also leave here examples showing why I came up with the design I'm in favor of now [See bullet 5 of http://lists.w3.org/Archives/Public/public-wtf/2014Dec/0038.html for an initial introduction].

Firstly, a polyfill needs to obtain the resolved value of a property applied to an element. More difficult, we want to obtain the resolved value that actually matches our "acceptable" definition for the property, which may not map the one the browser implemented. I had this issue in my CSS Grid polyfill when IE added support for 'break-before' without support for the 'region' value. For this reason, I wouldn't want browsers to be smart here, and I introduced previously the idea we want the browser to consider all declarations and to let us filter the ones we want him to ignore. I favor this approach because it removes much of the burden from the browser, and by making sure the browser can do "more", we ensure it can also  do less.

Then, we have to handle shorthand properties. In my css-grid polyfill, I have no way to decide whether "grid-area" overrides or is overridden by "grid-row", because the cascaded value doesn't yield me any information about the importance of the winning cascaded declaration, which I now understand as a big limitation of my current implementation. I believe this problem is better solved by abstracting the problem as much as possible, and returning along with the winning declaration some details about the context itself, like the specificity, at a very high level:

   var element : StylableElement; // Element|PseudoElement
   var property: string;
   var filter: (e: AbstractCSSDeclaration) => boolean;

   var property : string;
   var rawValue : Token[];
   var filtredValue: Token[]; // no whitespace, comment, etc...
   var stringValue : string;
   var toString: () => string;
   //var rawSelector: Token[];
   //var filtredSelector: Token[]; // idem
   //var stringSelector: string;
   var wouldOverride: (d: AbstractCSSDeclaration) => boolean;
   var wouldBeOverridenBy: (d: AbtractCSSDeclaration) => boolean;
   
   { 
       property, rawValue, filtredValue, stringValue, toString, 
       //rawSelector, filtredSelector, stringSelector, 
       wouldOverride, wouldBeOverridenBy 
   } : AbstractCSSDeclaration
   = CSSCascade.getResolvedStyle(element, property, { filter: filter });

The reason I prefer "d1.wouldOverride(d2)" instead of exposing the selector, the origin and the importance is that this design provides all necessary pieces of information, does leak very little on the actual stylesheet (CORS-free design) and allows for extensions to the priority model (scoped stylesheets...) which a polyfill would have a hard time dealing with.


Once we are here, the polyfill will need to perform some actions here, to do its job.

After that, a polyfill is likely to modify the computed values 
of existing properties in order to emulate the ones the browser couldn't
 understand. A clear example is my grid layout polyfill, which relies on
 relative/absolute positionning to get things at the right location on 
the screen once the layout is computed. I discovered the best way to 
achieve this is to create stylesheets your polyfill owns and can easily 
enable/disable/modify to your liking. Using an equivalent of IE's 
runtimeStyle would technically work, but debugging the interaction 
between polyfills becomes challenging when you dump/merge your final 
styles in one single location. [1] This is something we can already do today.


Finally, and initially, we need to know when the conditions for our polyfill to apply on some element are met, and changes. I believe the mutation observer comes here in the overall design. The ideal style mutation observer should not know anything about the computed style of an element: it should take all stylesheets, and when an element starts or stop to match the selector of a rule which contains a declaration having one of the watched property names, it should bookmark the element and fire an event asynchronously specifying the elements which may have been affected by a change (and maybe list the properties that may have been affected for each element). It's then up to the polyfill to get the new 'resolved style' using its filters and detect if a relayout/repaint/... is needed.

Another problem is to know when the used value of a property changed. I believe it's not something we actually want, except for layout purposes, where we depend on the values of the parents to compute the values of the children, without affecting them directly. Therefore, I'm in favor of reviving a non-bubbling 'resize' event which IE does already support, and possibly contnue this trend with other (a)synchronous layout-related events. I'm not sure a true mutation observer is required in this case; it would be very hard to implement and would only be useful in a small amount of cases. I think adhoc solutions would be a better fit for those use cases. For "property: inherit" to work, a simple "possible mutation observer" like I described before would to the job as "inherit" doesn't actually need the used value.


> 2. Changes to qSA results for a selector
> 
> This might not be strictly necessary, but it would reduce qSA calls. 
> Returning a list of added and removed items in the results could reduce 
> polyfill record-keeping. Polyfills need to run code for new matches, and 
> clean up code on removals.
> 
> 3. Added/Removed selectors
> 
> If style rules change, polyfills need to scan new selectors to see if they 
> include relevant declarations, and run clean up code on removed rules.

My current thoughts is none of this is actually necessary. A well-designed Style Mutation Observer provides us qSLive trivially, and makes it unnecessary to have anything related to 3 for polyfills related to css *properties* [2]. 

Now, if we want to go further and support syntax (for instance, at-rules) polyfills, that may become useful; but this also means we will need to limit ourself to CORS-enabled stylesheets in this case, if we don't design an open API. I've yet to find a good way to enable custom syntax for stylesheets, I'm tentatively proponent of pre-processing in this specific case as an easier way forward; but that's arguably because I never saw anything conclusive in this space.


> Please comment on where I’m wrong and what I’ve missed.
> 
> Thanks,
> Alan

You're welcome ;-)
François



[1] An issue I've here is that we have to rely on IDs & "!important" to 
achieve this, and if the DOM doesn't have an ID defined for an element 
we have to create one. I would like to find a better option here, it 
this is possible. Reviving IE's "uniqueID" (some sort of technical ID 
each element would have that's independant of its ID attribute) may be 
useful here, but it's only an idea. I'm open to better alternatives.

[2] That being said, since you can trivially implement qSLive using the 
Style Mutation Observer codebase, I'm in favor of adding the function 
anyway, for convenience. 		 	   		  

Received on Sunday, 25 January 2015 14:20:12 UTC