WICG / construct-stylesheets

API for constructing CSS stylesheet objects
Other
137 stars 25 forks source link

Instead of assignable FrozenArray, use add / remove #45

Closed rniwa closed 9 months ago

rniwa commented 5 years ago

In cases where this API is used by custom elements, different subclasses may want to add a different stylesheet to adoptedStyleSheets. In that case, it's better to have explicit add and remove methods rather than to have author scripts manipulate FrozenArray.

domenic commented 5 years ago

We can do both! add/remove would be sugar, in addition to allowing access to the list.

developit commented 5 years ago

Just catching up on this - I'm definitely fond of explicit methods. Here's my pitch:

interface DocumentOrShadowRoot {
  void adoptStyleSheet(CSSStyleSheet sheet)
  void removeStyleSheet(CSSStyleSheet sheet)
}
chrishtr commented 5 years ago

The methods as described above might not make it obvious enough that there is an ordering to the style sheets, with later style sheets overriding earlier ones for cases when both apply to an element.

emilio commented 5 years ago

What about prependStyleSheet / appendStyleSheet / removeStyleSheet? That makes the ordering explicit, and if you want something else you just go ahead and manipulate the list yourself.

emilio commented 5 years ago

(Not that I'm particularly fond of it, just dumping thoughts :))

developit commented 5 years ago

Perhaps the FrozenArray used here could be extended to allow indirect modifications like CSSRuleList (document.adoptedStyleSheets.insertSheet(sheet, 0)) or NodeList?

I'll admit it seems odd to add a whole new API for working with an ordered set of like objects.

surma commented 5 years ago

May I ask what the rational was behind making it a frozen array in the first place? It seems a bit odd to make it a frozen array and then re-implement methods to basically allow arbitrary manipulation.

domenic commented 5 years ago

It's not possible to make an array that the browser "watches" for changes. So, if it were a normal array, you'd do document.adoptedStyleSheets.push(foo) and then nothing would happen. The browser can only react to setters/method calls. The frozen array's current API is to use the setter, but this thread is about requesting additionally adding a method call API.

surma commented 5 years ago

Fair. Thank you.

v-python commented 5 years ago

@developit 's comment on Jan 28 is key: because order is important to the list, you need to be able to insert/remove elements from specific positions, not just the front / end.

rniwa commented 5 years ago

Again, the fact people are introducing race conditions, etc... by writing a simple snippet of code in https://github.com/w3c/webcomponents/issues/759#issuecomment-521053064 is yet another evidence that having add/remove is better than having people assign a FrozenArray.

Now I consider this issue as an absolute show stopper. I don't think we want to ever implement this feature in WebKit unless this issue is resolved.

Rich-Harris commented 5 years ago

As an aside, it already feels odd that assigning an array to document.adoptedStyleSheets results in that array being frozen. Adding add and remove methods to it feels very surprising:

const sheets = [...document.adoptedStyleSheets, sheet];
document.adoptedStyleSheets = sheets;

sheets.add(otherSheet); // huh?

Is there precedent for that elsewhere in the DOM? Having a non-reassignable object with methods for adding and removing feels much more natural, notwithstanding the ergonomic issues around prepending (though I'd expect that to be a very small minority of cases, and maybe not the thing to optimise the API for).

fvsch commented 5 years ago

Very anecdotal, but as an author seeing this in examples:

document.adoptedStyleSheets = [ ...document.adoptedStyleSheets, sheet ];

my gut reaction was that it was a useless recreation of the array and I would quickly simplify it to:

document.adoptedStyleSheets.push(sheet);

Maybe the assignment style suggests that any kind of Array manipulation (push, sort, etc.) would work? (Or maybe there are several other web APIs where you set an Array and get a FrozenArray, with similar ergonomics and restrictions, and I'm just not used to them.)

justinfagnani commented 5 years ago

The potential race condition linked to in the CSS modules thread is a problem with any of the built-in data structures like arrays. It's not unique to adoptedStyleSheets at all. I think that any sufficient alternate to adoptedStyleSheets will have possible race conditions if you read from and write to the collection with an intervening await.

calebdwilliams commented 5 years ago

Is there a reason a FrozenArray was chosen as the type as opposed to a StyleSheetList?

Could that interface be modified to support for addition, removal and reordering of stylesheet objects (even as a subclass)?

domenic commented 5 years ago

StyleSheetList is like a frozen Array but with no useful reading methods (map(), filter(), etc.), and one useless method (item()). So that is why we did not choose StyleSheetList.

calebdwilliams commented 5 years ago

Right, but the question was if it could be made to be more useful …

Rich-Harris commented 5 years ago

It's not unique to adoptedStyleSheets at all

No-one is claiming that. But 'this pattern is bug-prone anyway' isn't a reason to embrace it. An API along these lines wouldn't suffer the same problem:

document.whatever.add(await import('./styles.css'));
tabatkins commented 5 years ago

The potential race condition linked to in the CSS modules thread is a problem with any of the built-in data structures like arrays. It's not unique to adoptedStyleSheets at all.

More specifically, it's a problem with using await in an argument to a function (which a literal array construction is, effectively). The JS engine evaluates all preceding arguments before it pauses the function for the await, and that can cause snapshotting bugs as seen in that code.

So that is why we did not choose StyleSheetList.

Yup, StyleSheetList is just one of those shitty legacy "whoops I guess we have to act like Java" interfaces that the old DOM is plagued with. We didn't want to spread it further, since it's kinda actively hostile to web authors.

We can do both! add/remove would be sugar, in addition to allowing access to the list.

With the additional point that we probably want to make prepending as easy as appending, yeah, I'm okay with some sugar methods for append/prepend/remove, and letting array manip handle all the rest, as Emilio suggests.

matthew-dean commented 5 years ago

Yup, StyleSheetList is just one of those shitty legacy "whoops I guess we have to act like Java" interfaces that the old DOM is plagued with.

I know this is a huge undertaking and tangential aside to this discussion, but I wish there was a dedicated effort to writing a new DOM, and standardizing interfaces and methods and deprecating old ones. Like, I'd love if someone attacked the problem of "if we built a DOM from scratch in 2019, what would it look like?"

calebdwilliams commented 5 years ago

Yeah, I guess none of that actually answered the question of if StyleSheetList could be made to solve our current problem. If it exposes "no useful reading methods," what would that look like? Could that class be outfitted with the add, remove, replace, order, etc. methods that would meet the needs of CSS modules?

rniwa commented 5 years ago

Yup, StyleSheetList is just one of those shitty legacy "whoops I guess we have to act like Java" interfaces that the old DOM is plagued with. We didn't want to spread it further, since it's kinda actively hostile to web authors.

I disagree with that proposition. Consistency is important.

Yeah, I guess none of that actually answered the question of if StyleSheetList could be made to solve our current problem.

Yes. In fact, I've suggested that we use StyleSheetList in numerous occasions.

domenic commented 5 years ago

StyleSheetList can't be made modifiable because then it would no longer serve well for document.styleSheets, which is a read-only view onto the list of link/style-element generated style sheets.

Jamesernator commented 5 years ago

It's not possible to make an array that the browser "watches" for changes. So, if it were a normal array,

Could it work to have an exotic subclass of Array that traps all operations on it to update the document on potential changes?

~e.g. If implemented in JS~:

Old example ```js class StyleSheetArray extends Array { #invalidateCurrentStyles() { /* Recompute styles, etc, whatever browsers do when new style sheets are added */ } #maybeInvalidateStyles() { /* Test if stuff has changed and if so invalidate current styles */ } constructor() { super(); return new Proxy(this, { get(...args) { const value = Reflect.get(...args); if (typeof value === 'function') { const that = this; return function(...args) { const result = Reflect.apply(value, this, args); this.#maybeInvalidateStyles(); return result; } } return value; }, set(...args) { const result = Reflect.set(...args); this.#invalidateCurrentStyles(); return result; }, /* etc for all the other traps */ }); } } ```

EDIT: Actually array methods are generic things so just having exotic behavior on defineProperty/deleteProperty is probably enough to observe all the changes.

e.g.:

'use strict';

function isArrayIndex(string) {
    if (typeof string !== 'string') {
        return false;
    }
    const number = Number(string);
    return Number.isSafeInteger(number)
        && String(number) === string
        && number >= 0;
}

class StyleSheetArray extends Array {
    #updateStyles() {
         /* Do usual updates when style list changes */
    }

    constructor(...args) {
        super(...args);
        return new Proxy(this, {
            defineProperty: (target, prop, descriptor) => {
                try {
                    return Reflect.defineProperty(target, prop, descriptor);
                } finally {
                    if (prop === 'length' || isArrayIndex(prop)) {
                        this.#updateStyles();
                    }
                }
            },

            deleteProperty: (target, prop) => {
                try {
                    return Reflect.deleteProperty(target, prop);
                } finally {
                    if (prop === 'length' || isArrayIndex(prop)) {
                        this.#updateStyles();
                    }
                }
            },
        });
    }
}
calebdwilliams commented 5 years ago

Should this feature be a sort of LinkedList/LinkedSet? I realize there's no true analog for those concepts in JavaScript/DOM, but implementing that based on current JS should be fairly trivial (in userland at least, I obviously can't speak for browser implementation, but I'm pretty sure most other languages have this concept built in).

That sort of object should have the following methods:

Ideally I'd like to see these added to a subclass of StyleSheetList that adoptedStyleSheets could inherit from (for consistency sake). I understand that these can't be added to the base object for the reasons described in #4, but creating this sort of object should be easy enough and could then be made to respond to user actions.

Thoughts:

Edit: Overly-simplistic demo

emilio commented 5 years ago

I think a more consistent naming and API with the existing CSSOM interfaces for inserting / removing rules would be good... CSSOM interfaces have:

And same for CSSGroupingRule. So maybe:

would be the bare minimum? I agree that maybe an appendSheet would be on point, though iteration and includes you'd get for free via the FrozenArray returned by adoptedStyleSheets.

calebdwilliams commented 5 years ago

@emilio, I like the thought, but that would require additional methods on document as well. Maybe DocumentOrShadowRoot.adoptedStyleSheets.insertSheet. Maybe to preserve current functionality, adoptedStyleSheets' setter could take any iterable of CSSStyleSheets?

heycam commented 5 years ago

There is some discussion about an IDL-level feature better than FrozenArray, aimed at solving use cases like adoptedStyleSheets, here: https://github.com/heycam/webidl/issues/796

fergald commented 4 years ago

Repeating a suggestion I made at TPAC...

If we give style elements the ability to adopt a style sheet then I think a lot of problems go away. They can be inserted and deleted just as DOM elements and their ordering vs parsed styles sheets is obvious (and it makes it possible to insert one before, after or even between parsed style elements rather then having to pick before/after in the spec).

Pro:

Con:

E.g.

# shadow root
<style>
  foo { ...}
</style>
<style adopted id=adopted></style>
<style>
  bar { ...}
</style>
adopted.adoptedStyleSheet = new CSSStyleSheet();
...
Jamesernator commented 4 years ago

If we give style elements the ability to adopt a style sheet then I think a lot of problems go away. They can be inserted and deleted just as DOM elements and their ordering vs parsed styles sheets is obvious (and it makes it possible to insert one before, after or even between parsed style elements rather then having to pick before/after in the spec).

I like this cause it means we could also integrate CSS modules to be loaded via both JS or HTML.

e.g.:

<style adopted src="./component.css"></style>
import styleSheet from './component.css';

const styleElement = shadowRoot.querySelector("style[adopted]");

styleElement.adoptedStyleSheet === styleSheet; // true
emilio commented 4 years ago

One of the biggest arguments that people have been using to push this API is that having the stylesheet tied to the DOM causes headaches for web component frameworks because they need to mutate the DOM in the component. Wouldn't that re-introduce the same issue?

justinfagnani commented 4 years ago

I would definitely rather deal with a mutable array than the DOM for this case.

At this point we have multiple actors needing to manage parts of the adoptedStyleSheets array, and they sometimes need to do things like remove the sheets they added. This is tractable by iterating over adoptedStyleSheets and checking for reference equality (Set semantics would be nicer I guess), but either keeping references to <style> element, or query selecting and iterating, seem more cumbersome and slower.

Putting elements in to DOM risks interfering with template systems too.

fergald commented 4 years ago

@justinfagnani

This is tractable by iterating over adoptedStyleSheets and checking for reference equality (Set semantics would be nicer I guess), but either keeping references to