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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 37 additions & 27 deletions src/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ const prefixAll = createPrefixer(staticData);
/* ::
import type { SheetDefinition } from './index.js';
type StringHandlers = { [id:string]: Function };
type SelectorCallback = (selector: string) => any;
type SelectorCallback = (selector: string) => string[];
export type SelectorHandler = (
selector: string,
baseSelector: string,
callback: SelectorCallback
) => string | null;
) => string[] | string | null;
*/

/**
Expand Down Expand Up @@ -49,9 +49,10 @@ export type SelectorHandler = (
* callback('.foo:nth-child(2n):hover')
*
* to generate its subtree `{ color: 'red' }` styles with a
* '.foo:nth-child(2n):hover' selector. The callback would return CSS like
* '.foo:nth-child(2n):hover' selector. The callback would return an array of CSS
* rules like
*
* '.foo:nth-child(2n):hover{color:red !important;}'
* ['.foo:nth-child(2n):hover{color:red !important;}']
*
* and the handler would then return that resulting CSS.
*
Expand All @@ -67,34 +68,26 @@ export type SelectorHandler = (
* @param {function} generateSubtreeStyles: A function which can be called to
* generate CSS for the subtree of styles corresponding to the selector.
* Accepts a new baseSelector to use for generating those styles.
* @returns {?string} The generated CSS for this selector, or null if we don't
* handle this selector.
* @returns {string[] | string | null} The generated CSS for this selector, or
* null if we don't handle this selector.
*/
export const defaultSelectorHandlers = [
export const defaultSelectorHandlers /* : SelectorHandler[] */ = [
// Handle pseudo-selectors, like :hover and :nth-child(3n)
function pseudoSelectors(
selector /* : string */,
baseSelector /* : string */,
generateSubtreeStyles /* : Function */
) /* */ {
function pseudoSelectors(selector, baseSelector, generateSubtreeStyles) {
if (selector[0] !== ":") {
return null;
}
return generateSubtreeStyles(baseSelector + selector);
},

// Handle media queries (or font-faces)
function mediaQueries(
selector /* : string */,
baseSelector /* : string */,
generateSubtreeStyles /* : Function */
) /* */ {
function mediaQueries(selector, baseSelector, generateSubtreeStyles) {
if (selector[0] !== "@") {
return null;
}
// Generate the styles normally, and then wrap them in the media query.
const generated = generateSubtreeStyles(baseSelector);
return `${selector}{${generated}}`;
return [`${selector}{${generated.join('')}}`];
},
];

Expand Down Expand Up @@ -147,15 +140,15 @@ export const generateCSS = (
selectorHandlers /* : SelectorHandler[] */,
stringHandlers /* : StringHandlers */,
useImportant /* : boolean */
) /* : string */ => {
) /* : string[] */ => {
const merged = new OrderedElements();

for (let i = 0; i < styleTypes.length; i++) {
merged.addStyleType(styleTypes[i]);
}

const plainDeclarations = new OrderedElements();
let generatedStyles = "";
const generatedStyles = [];

// TODO(emily): benchmark this to see if a plain for loop would be faster.
merged.forEach((val, key) => {
Expand All @@ -170,7 +163,17 @@ export const generateCSS = (
if (result != null) {
// If the handler returned something, add it to the generated
// CSS and stop looking for another handler.
generatedStyles += result;
if (Array.isArray(result)) {
generatedStyles.push(...result);
} else {
// eslint-disable-next-line
console.warn(
'WARNING: Selector handlers should return an array of rules.' +
'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!

}
return true;
}
});
Expand All @@ -180,13 +183,20 @@ export const generateCSS = (
plainDeclarations.set(key, val, true);
}
});

return (
generateCSSRuleset(
selector, plainDeclarations, stringHandlers, useImportant,
selectorHandlers) +
generatedStyles
const generatedRuleset = generateCSSRuleset(
selector,
plainDeclarations,
stringHandlers,
useImportant,
selectorHandlers,
);


if (generatedRuleset) {
generatedStyles.unshift(generatedRuleset);
}

return generatedStyles;
};

/**
Expand Down
50 changes: 31 additions & 19 deletions src/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -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


// Inject a string of styles into a <style> tag in the head of the document. This
// Inject a set of rules 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 = (cssRules /* : string[] */) => {
if (styleTag == null) {
// Try to find a style tag with the `data-aphrodite` attribute first.
styleTag = document.querySelector("style[data-aphrodite]");
styleTag = ((document.querySelector("style[data-aphrodite]") /* : any */) /* : ?HTMLStyleElement */);

// If that doesn't work, generate a new style tag.
if (styleTag == null) {
Expand All @@ -44,12 +44,20 @@ const injectStyleTag = (cssContents /* : string */) => {
}
}

const sheet = ((styleTag.styleSheet || styleTag.sheet /* : any */) /* : CSSStyleSheet */);

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!

if (sheet.insertRule) {
let numRules = sheet.cssRules.length;
cssRules.forEach((rule) => {
try {
sheet.insertRule(rule, numRules);
numRules += 1;
} catch(e) {
// The selector for this rule wasn't compatible with the browser
}
});
} else {
styleTag.appendChild(document.createTextNode(cssContents));
styleTag.innerText = (styleTag.innerText || '') + cssRules.join('');
}
};

Expand Down Expand Up @@ -113,17 +121,17 @@ const stringHandlers = {
if (val instanceof OrderedElements) {
val.forEach((valVal, valKey) => {
finalVal += generateCSS(
valKey, [valVal], selectorHandlers, stringHandlers, false);
valKey, [valVal], selectorHandlers, stringHandlers, false).join('');
});
} else {
Object.keys(val).forEach(key => {
finalVal += generateCSS(
key, [val[key]], selectorHandlers, stringHandlers, false);
key, [val[key]], selectorHandlers, stringHandlers, false).join('');
});
}
finalVal += '}';

injectGeneratedCSSOnce(name, finalVal);
injectGeneratedCSSOnce(name, [finalVal]);

return name;
} else {
Expand All @@ -137,7 +145,7 @@ const stringHandlers = {
let alreadyInjected = {};

// This is the buffer of styles which have not yet been flushed.
let injectionBuffer = "";
let injectionBuffer /* : string[] */ = [];

// A flag to tell if we are already buffering styles. This could happen either
// because we scheduled a flush call already, so newly added styles will
Expand All @@ -163,7 +171,7 @@ const injectGeneratedCSSOnce = (key, generatedCSS) => {
asap(flushToStyleTag);
}

injectionBuffer += generatedCSS;
injectionBuffer.push(...generatedCSS);
alreadyInjected[key] = true;
}

Expand All @@ -186,7 +194,7 @@ export const injectStyleOnce = (
};

export const reset = () => {
injectionBuffer = "";
injectionBuffer = [];
alreadyInjected = {};
isBuffering = false;
styleTag = null;
Expand All @@ -200,17 +208,21 @@ export const startBuffering = () => {
isBuffering = true;
};

export const flushToString = () => {
const flushToArray = () => {
isBuffering = false;
const ret = injectionBuffer;
injectionBuffer = "";
injectionBuffer = [];
return ret;
};

export const flushToString = () => {
return flushToArray().join('');
};

export const flushToStyleTag = () => {
const cssContent = flushToString();
if (cssContent.length > 0) {
injectStyleTag(cssContent);
const cssRules = flushToArray();
if (cssRules.length > 0) {
injectStyleTag(cssRules);
}
};

Expand Down
36 changes: 27 additions & 9 deletions tests/generate_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ describe('generateCSS', () => {
stringHandlers = {}, useImportant = true) => {
const actual = generateCSS(className, styleTypes, selectorHandlers,
stringHandlers, useImportant);
const expectedNormalized = expected.split('\n').map(x => x.trim()).join('');
const formatStyles = (styles) => styles.replace(/(;|{|})/g, '$1\n');
assert.equal(
const expectedArray = [].concat(expected);
const expectedNormalized = expectedArray.map(rule => rule.split('\n').map(x => x.trim()).join(''));
const formatStyles = (styles) => styles.map(style => style.replace(/(;|{|})/g, '$1\n')).join('');
assert.deepEqual(
actual,
expectedNormalized,
`
Expand Down Expand Up @@ -273,17 +274,17 @@ ${formatStyles(actual)}
padding: 30,
}
}
], `
@media (min-width: 200px){
], [
`@media (min-width: 200px){
.foo{
padding:20px !important;
}
}
@media (min-width: 400px){
}`,
`@media (min-width: 400px){
.foo{
padding:30px !important;
}
}`, defaultSelectorHandlers);
}`], defaultSelectorHandlers);
});

it('supports custom string handlers', () => {
Expand Down Expand Up @@ -345,7 +346,24 @@ ${formatStyles(actual)}
color: 'red',
},
color: 'blue',
}], '.foo{color:blue;}.bar .foo{color:red;}', [handler], {}, false);
}], ['.foo{color:blue;}','.bar .foo{color:red;}'], [handler], {}, false);
});

it('supports selector handlers that return strings containing multiple rules', () => {
const handler = (selector, baseSelector, callback) => {
if (selector[0] !== '^') {
return null;
}
const generatedBefore = callback(baseSelector + '::before');
const generatedAfter = callback(baseSelector + '::after');
return `${generatedBefore} ${generatedAfter}`;
};

assertCSS('.foo', [{
'^': {
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

});

it('correctly prefixes border-color transition properties', () => {
Expand Down
34 changes: 18 additions & 16 deletions tests/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
css
} from '../src/index.js';
import { reset } from '../src/inject.js';
import { getSheetText } from './testUtils.js';

describe('css', () => {
beforeEach(() => {
Expand Down Expand Up @@ -80,9 +81,10 @@ describe('css', () => {
asap(() => {
const styleTags = global.document.getElementsByTagName("style");
const lastTag = styleTags[styleTags.length - 1];
const style = getSheetText(lastTag.sheet);

assert.include(lastTag.textContent, `${sheet.red._name}{`);
assert.match(lastTag.textContent, /color:red !important/);
assert.include(style, `${sheet.red._name} {`);
assert.match(style, /color: red !important/);
done();
});
});
Expand Down Expand Up @@ -132,10 +134,10 @@ describe('css', () => {
asap(() => {
const styleTags = global.document.getElementsByTagName("style");
assert.equal(styleTags.length, 1);
const styles = styleTags[0].textContent;
const styles = getSheetText(styleTags[0].sheet);

assert.include(styles, `${sheet.red._name}{`);
assert.include(styles, 'color:red');
assert.include(styles, `${sheet.red._name} {`);
assert.include(styles, 'color: red');

done();
});
Expand Down Expand Up @@ -292,14 +294,14 @@ describe('rehydrate', () => {
asap(() => {
const styleTags = global.document.getElementsByTagName("style");
assert.equal(styleTags.length, 1);
const styles = styleTags[0].textContent;
const styles = getSheetText(styleTags[0].sheet);

assert.notInclude(styles, `.${sheet.red._name}{`);
assert.notInclude(styles, `.${sheet.blue._name}{`);
assert.include(styles, `.${sheet.green._name}{`);
assert.notMatch(styles, /color:blue/);
assert.notMatch(styles, /color:red/);
assert.match(styles, /color:green/);
assert.notInclude(styles, `.${sheet.red._name} {`);
assert.notInclude(styles, `.${sheet.blue._name} {`);
assert.include(styles, `.${sheet.green._name} {`);
assert.notMatch(styles, /color: blue/);
assert.notMatch(styles, /color: red/);
assert.match(styles, /color: green/);

done();
});
Expand Down Expand Up @@ -364,14 +366,14 @@ describe('StyleSheet.extend', () => {
asap(() => {
const styleTags = global.document.getElementsByTagName("style");
assert.equal(styleTags.length, 1);
const styles = styleTags[0].textContent;
const styles = getSheetText(styleTags[0].sheet);

assert.notInclude(styles, '^bar');
assert.include(styles, '.bar .foo');
assert.include(styles, '.baz .bar .foo');
assert.include(styles, 'color:red');
assert.include(styles, 'color:blue');
assert.include(styles, 'color:orange');
assert.include(styles, 'color: red');
assert.include(styles, 'color: blue');
assert.include(styles, 'color: orange');

done();
});
Expand Down
Loading