marcj / css-element-queries

CSS Element-Queries aka Container Queries. High-speed element dimension/media queries in valid css.
http://marcj.github.io/css-element-queries/
MIT License
4.27k stars 487 forks source link

[bug] Occasionally cannot find CSS rules when using ElementQueries.listen() in an Angular app #286

Open henrahmagix opened 4 years ago

henrahmagix commented 4 years ago

About 10% of the time in Safari in an Angular 8 app, there are 0 stylesheets when DOMContentLoaded is triggered, which means ElementQueries cannot find any CSS rules to polyfill and basically does not work for us. The above was gleaned from local testing: refreshing and hard-refreshing multiple times until the page loaded and a known container query wasn't applied; debugging the source revealed that document.styleSheets.length was 0 when DOMContentLoaded was triggered, so we must wait a little bit longer to initialise.

Basically, if document.styleSheets.length === 0 when init() is called, this loop does not run, no css rules are detected, and the library doesn't keep checking so future stylesheet additions are not parsed: https://github.com/marcj/css-element-queries/blob/df88b1e9a738206cb0e624da424a19699d14638d/src/ElementQueries.js#L440-L449

Our solution is to call ElementQueries.init() when document.styleSheets.length > 0. We wait by using requestAnimationFrame() after DOMContentLoaded, which unfortunately means we're not getting the nice browser compatibility of this library's domLoaded function.

// polyfills.ts
import { ElementQueries } from 'css-element-queries';
document.addEventListener('DOMContentLoaded', initElementQueriesWhenDefinitelyReady, false);
function initElementQueriesWhenDefinitelyReady() {
  if (document.styleSheets.length === 0) {
    // Don't impose a time or iteration limit: if there are no stylesheets, then the page cannot reasonably be ready, so we don't need to
    // worry about looping forever.
    requestAnimationFrame(initElementQueriesWhenDefinitelyReady);
    return;
  }
  ElementQueries.init();
}

I'm not exactly sure how this library could work around this. In our app, we're fine with looping infinitely with requestAnimationFrame until stylesheets become available, because we know there will be some; I'm guessing this library doesn't want to assume that in all cases, i.e. if you added this library to an empty page and it had the above solution by default, it would loop indefinitely rather than just do nothing.

Maybe there could be an additional option to use this solution for developers of SPA apps? Perhaps ElementQueries.listenForMinimumStylesheets(1)?

P.S. I love this library! It's a really smart solution đź’Ž

marcj commented 4 years ago

That's interesting. I was working lately with a solution that used MutationObserver on the <head> and triggers a ElementQueries.init every time a new style node is found. I think making such an implementation available behind a flag (because MutationObserver costs performance) would solve it for all kind of frameworks.

henrahmagix commented 4 years ago

That sounds great! Some fab explanations of MutationObserver performance here, for those (like me) who aren't aware: https://stackoverflow.com/questions/31659567/performance-of-mutationobserver-to-detect-nodes-in-entire-dom/39332340

marcj commented 4 years ago

Yeah maybe it's not a performance issue at all anymore and we could just make this available for everyone. Would definitely make working with SPA frameworks very easy and increases compatibility with those (especially Angular). I don't know yet where React/Vue puts their stylesheets for loaded components, but I hope in the head. For better performance however we should add a new method ElementQueries.parseStyles(stylesNode) (because init parses all styles) that parses only style rules from given (newly added) node.

FirstVertex commented 3 years ago

this is working for our Angular application, in the root app.component.ts

export class AppComponent {
  constructor(zone: NgZone) {
    zone.runOutsideAngular(() => ElementQueries.init());
  }
}