Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use insertRule to inject styles #240

Merged
merged 7 commits into from
Jan 31, 2018
Merged

Conversation

reklawnos
Copy link
Contributor

Instead of appending a new text node to a style tag or appending to the cssText property of the stylesheet (which broke media queries in IE as documented in #238), use insertRule to add CSS rules to the style element.

This change requires styles to be generated as an array of strings with one rule per string, rather than a single large string that contains multiple rules.

This was (at least provisionally) implemented in #83 and #157 and discussed in #76, but it didn't seem like those were going anywhere.

@khanbot
Copy link

khanbot commented May 12, 2017

CLA signature looks good 👍

src/generate.js Outdated
@@ -94,7 +94,7 @@ export const defaultSelectorHandlers = [
}
// Generate the styles normally, and then wrap them in the media query.
const generated = generateSubtreeStyles(baseSelector);
return `${selector}{${generated}}`;
return [`${selector}{${generated}}`];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we actually need to create a new array here or if we can continue returning a string and handle it properly elsewhere. Also, do we need to change pseudoSelectors above to also return an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateSubtreeStyles should have the same return type as generateCSS so pseudoSelectors should already return an array. Would it make sense to make the Flow type for generateSubtreeStyles more specific?

We could have generateCSS handle a string being returned here, but we never want a selector handler to return strings containing multiple rules, as insertRule expects a single rule to be passed. My thought here was that selector handlers should always return an array of strings with one rule per string.

This would be a breaking change to the current API, so perhaps we could, say, wrap strings returned by a selector handler in a @media all {...} query and warn about that return type being deprecated. Or just bump by a major version.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. The alternative would be to do something like this on line 173:

if (Array.isArray(result)) {
  generatedStyles.push(...result);
} else {
  generatedStyles.push(result);
}

I think making the breaking change is okay. @xymostech do you have any thoughts on this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, yes I think it would be good to improve the flow types where we can.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. I think the funky @media all{} with a warning solution sounds the best to me, but I'm not that attached to it. I don't know how many people are using extensions right now, but I'd prefer if we didn't break it already?

(and as a ping, the psuedoSelectors function above needs to be changed too, right?)

src/inject.js Outdated
if (sheet.insertRule) {
cssContents.forEach((rule) => {
try {
sheet.insertRule(rule, sheet.cssRules.length);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth moving this out into a variable that we increment, to avoid having to read this value every time.

let numberOfRules = sheet.cssRules.length;
cssContents.forEach((rule) => {
  try {
    sheet.insertRule(rule, numberOfRules);
    numberOfRules += 1;
  } catch(e) { ... }
});

src/inject.js Outdated
// $FlowFixMe: legacy Internet Explorer compatibility
styleTag.styleSheet.cssText += cssContents;
if (sheet.insertRule) {
cssContents.forEach((rule) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should make this a regular for loop for perf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check how this affects performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran both versions through your benchmark several times. There was no noticeable difference between the forEach version and the for loop version, but a small speedup when storing sheet.cssRules.length in a variable.

Copy link
Contributor

@xymostech xymostech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh! Thank you so much for doing this! I've been meaning to get this out for a while but haven't had the time!

I left a bunch of comments about things to look at! Overall though, this change looks great. :)

src/inject.js Outdated
const currentStyleTag = styleTag;
cssContents.forEach((rule) => {
currentStyleTag.innerText = (currentStyleTag.innerText || '') + rule;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to batch update this, like currentStyleTag.innerText = (currentStyleTag.innerText || '') + cssContents.join('');.

@@ -19,17 +19,17 @@ type ProcessedStyleDefinitions = {
// inserted anything yet. We could find this each time using
// `document.querySelector("style[data-aphrodite"])`, but holding onto it is
// faster.
let styleTag = null;
let styleTag /* : ?HTMLStyleElement */ = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3


if (styleTag.styleSheet) {
// $FlowFixMe: legacy Internet Explorer compatibility
styleTag.styleSheet.cssText += cssContents;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you test this change in IE? Looks like this "IE compatibility" branch got changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insertRule is supported on ≥ IE 9. I tested it on IE 9 and AFAICT it worked just fine. What browsers does Aphrodite guarantee support for? The innerText fallback should work for browsers without insertRule, but I just want to get idea of how far back I should test this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing in IE 9 sounds good enough to me! Thanks!

src/inject.js Outdated
@@ -137,7 +146,7 @@ const stringHandlers = {
let alreadyInjected = {};

// This is the buffer of styles which have not yet been flushed.
let injectionBuffer = "";
let injectionBuffer = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give this a : string[] type to help flow make sure that these strings can't be null?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping on this?

src/generate.js Outdated
@@ -94,7 +94,7 @@ export const defaultSelectorHandlers = [
}
// Generate the styles normally, and then wrap them in the media query.
const generated = generateSubtreeStyles(baseSelector);
return `${selector}{${generated}}`;
return [`${selector}{${generated}}`];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. I think the funky @media all{} with a warning solution sounds the best to me, but I'm not that attached to it. I don't know how many people are using extensions right now, but I'd prefer if we didn't break it already?

(and as a ping, the psuedoSelectors function above needs to be changed too, right?)

src/inject.js Outdated

// Inject a string of styles into a <style> tag in the head of the document. This
// will automatically create a style tag and then continue to use it for
// multiple injections. It will also use a style tag with the `data-aphrodite`
// tag on it if that exists in the DOM. This could be used for e.g. reusing the
// same style tag that server-side rendering inserts.
const injectStyleTag = (cssContents /* : string */) => {
const injectStyleTag = (cssContents /* : string[] */) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this argument name and docstring be updated?

@reklawnos
Copy link
Contributor Author

@lencioni @xymostech PTAL, addressed comments and updated to use the @media all solution I suggested.

Copy link
Contributor

@xymostech xymostech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me! I'll let @lencioni do the final approve and merge to make sure that their concerns were addressed.

src/inject.js Outdated
@@ -137,7 +146,7 @@ const stringHandlers = {
let alreadyInjected = {};

// This is the buffer of styles which have not yet been flushed.
let injectionBuffer = "";
let injectionBuffer = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping on this?

'^': {
color: 'red',
},
}], ['@media all {.foo::before{color:red;} .foo::after{color:red;}}'], [handler], {}, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

@lencioni
Copy link
Collaborator

lencioni commented May 18, 2017 via email

@Trolleymusic
Copy link

Hey @reklawnos @xymostech, any plans to merge this soon?

Instead of appeding a new text node to a style tag or appending to the
`cssText` property of the stylesheet (which broke media queries in IE as
documented in Khan#238), use `insertRule` to add CSS rules to the `style`
element.

This change requires styles to be generated as an array of strings with
one rule per string, rather than a single large string that contains
multiple rules.
assertStylesInclude('0%{opacity:0;-webkit-transform:scale(0.75) translate3d(1px, 2px, 0);-ms-transform:scale(0.75) translate3d(1px, 2px, 0);transform:scale(0.75) translate3d(1px, 2px, 0);}');
assertStylesInclude('100%{opacity:1;-webkit-transform:scale(1) translate3d(1px, 2px, 0);-ms-transform:scale(1) translate3d(1px, 2px, 0);transform:scale(1) translate3d(1px, 2px, 0);}');
assertStylesInclude('animation-name:keyframe_d35t13');
assertStylesInclude('0% {opacity: 0; -webkit-transform: scale(0.75) translate3d(1px,2px,0); -ms-transform: scale(0.75) translate3d(1px,2px,0); transform: scale(0.75) translate3d(1px,2px,0);}');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some of the spaces you removed here are still there in the test

@lencioni
Copy link
Collaborator

I think this looks good. There is one test that has a minor failure on it. I'll follow up with a fix for that after merging this. Thanks for your work on this!

@lencioni lencioni merged commit d3a0fbf into Khan:master Jan 31, 2018
'Returning a string containing multiple rules is deprecated.',
handler,
);
generatedStyles.push(`@media all {${result}}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may be a semver major change, since it could theoretically cause some styles to stop working in browsers that don't support media queries. For IE, this applies to IE8 and older.

Practically speaking, however, I would be surprised if this actually breaks anything for anyone, so I am tempted to go with semver minor.

What do you think @xymostech?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just my 2c, but if I understand correctly, if someone was not using media queries before this change, and needed to support IE8 for some reason, then it seems like this should likely be considered a breaking change, as all styles would no longer work?

Is there a concern that bumping semver major would cause an unnecessary burden to consumers?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think you are correct. My main concern is that I don't want to do a major release if there is no practical benefit to it, but I guess there's no way to know so probably best to stay on the safe side.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if opening a new PR to bump the version of inline-style-prefixer https://github.com/rofrischmann/inline-style-prefixer/blob/master/Changelog.md, which contains a few bug fixes and reported 30% performance improvement since , along with noting some potential internal perf benefits would be of practical benefit?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that sounds great to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants