diff --git a/README.md b/README.md
index af41c91..c286d2f 100644
--- a/README.md
+++ b/README.md
@@ -173,6 +173,37 @@ const styles = StyleSheet.create({
Aphrodite will ensure that the global `@font-face` rule for this font is only inserted once, no matter how many times it's referenced.
+# Descendant selectors
+
+Descendant rules (i.e. a rule like `.parent:hover .child { ... }` in CSS) are created by adding values to your styles indicating the styles to apply to the child. To distinguish the styles from normal CSS keys, the keys start with a `>>`, like `'>>child'`.
+
+```js
+const styles = StyleSheet.create({
+ parent: {
+ ":hover": {
+ ">>my_child": {
+ // Styles to apply to the child when the parent is
+ // hovered over.
+ color: "red",
+ fontWeight: "bold"
+ }
+ }
+ }
+});
+```
+
+Then, apply the `parent` style using `css(styles.parent)`, and add the child's style to a descendant of the parent using `css(styles.parent.my_child)`, where `my_child` is the name of the key you used, without the `>>`.
+
+For example, using React's jsx syntax:
+
+```jsx
+
+
+ This turns red and bold when the parent is hovered over
+
+
+```
+
# Caveats
## Assigning a string to a content property for a pseudo-element
diff --git a/dist/aphrodite.js b/dist/aphrodite.js
index 32a42b5..e719c56 100644
--- a/dist/aphrodite.js
+++ b/dist/aphrodite.js
@@ -60,10 +60,48 @@ module.exports =
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }
+
+ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
var _util = __webpack_require__(2);
var _inject = __webpack_require__(3);
+ // TODO(emily): Make a 'production' mode which doesn't prepend the class name
+ // here, to make the generated CSS smaller.
+ var makeClassName = function makeClassName(key, vals) {
+ return key + '_' + (0, _util.hashObject)(vals);
+ };
+
+ // Find all of the references to descendent styles in a given definition,
+ // generates a class name for each of them based on the class name of the
+ // parent, and stores that name in a special `_names` object on the style.
+ var findAndTagDescendants = function findAndTagDescendants(styles, base) {
+ var names = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
+
+ Object.keys(styles).forEach(function (key) {
+ if (key[0] === ':' || key[0] === '@') {
+ findAndTagDescendants(styles[key], base, names);
+ } else if (key[0] === '>' && key[1] === '>') {
+ findAndTagDescendants(styles[key], base, names);
+
+ var _name = base + '__' + key.slice(2);
+
+ names[key.slice(2)] = {
+ _isPlainClassName: true,
+ _className: _name
+ };
+
+ styles[key]._names = _defineProperty({}, _name, true);
+ }
+ });
+
+ return names;
+ };
+
var StyleSheet = {
create: function create(sheetDefinition) {
return (0, _util.mapObj)(sheetDefinition, function (_ref) {
@@ -72,12 +110,12 @@ module.exports =
var key = _ref2[0];
var val = _ref2[1];
- return [key, {
- // TODO(emily): Make a 'production' mode which doesn't prepend
- // the class name here, to make the generated CSS smaller.
- _name: key + '_' + (0, _util.hashObject)(val),
+ var name = makeClassName(key, val);
+
+ return [key, _extends({
+ _name: name,
_definition: val
- }];
+ }, findAndTagDescendants(val, name))];
});
},
@@ -105,6 +143,10 @@ module.exports =
}
};
+ var isPlainClassName = function isPlainClassName(def) {
+ return def._isPlainClassName;
+ };
+
var css = function css() {
for (var _len = arguments.length, styleDefinitions = Array(_len), _key = 0; _key < _len; _key++) {
styleDefinitions[_key] = arguments[_key];
@@ -121,14 +163,29 @@ module.exports =
return "";
}
- var className = validDefinitions.map(function (s) {
+ // Filter out "plain class name" arguments, which just want us to add a
+ // classname to the end result, instead of generating styles.
+ var plainClassNames = validDefinitions.filter(isPlainClassName).map(function (def) {
+ return def._className;
+ });
+
+ var otherDefinitions = validDefinitions.filter(function (def) {
+ return !isPlainClassName(def);
+ });
+
+ // If there are only plain class names, just join those.
+ if (otherDefinitions.length === 0) {
+ return plainClassNames.join(" ");
+ }
+
+ var className = otherDefinitions.map(function (s) {
return s._name;
}).join("-o_O-");
- (0, _inject.injectStyleOnce)(className, '.' + className, validDefinitions.map(function (d) {
+ (0, _inject.injectStyleOnce)(className, '.' + className, otherDefinitions.map(function (d) {
return d._definition;
}));
- return className;
+ return [className].concat(_toConsumableArray(plainClassNames)).join(" ");
};
exports['default'] = {
@@ -218,6 +275,9 @@ module.exports =
*/
var isUnitlessNumber = {
animationIterationCount: true,
+ borderImageOutset: true,
+ borderImageSlice: true,
+ borderImageWidth: true,
boxFlex: true,
boxFlexGroup: true,
boxOrdinalGroup: true,
@@ -243,8 +303,11 @@ module.exports =
// SVG-related properties
fillOpacity: true,
+ floodOpacity: true,
stopOpacity: true,
+ strokeDasharray: true,
strokeDashoffset: true,
+ strokeMiterlimit: true,
strokeOpacity: true,
strokeWidth: true
};
@@ -350,13 +413,13 @@ module.exports =
};
exports.hashObject = hashObject;
- var importantRegexp = /^([^:]+:.*?)( !important)?$/;
+ var IMPORTANT_RE = /^([^:]+:.*?)( !important)?$/;
// Given a style string like "a: b; c: d;", adds !important to each of the
// properties to generate "a: b !important; c: d !important;".
var importantify = function importantify(string) {
return string.split(";").map(function (str) {
- return str.replace(importantRegexp, function (_, base, important) {
+ return str.replace(IMPORTANT_RE, function (_, base, important) {
return base + " !important";
});
}).join(";");
@@ -878,6 +941,8 @@ module.exports =
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _inlineStylePrefixer = __webpack_require__(7);
@@ -886,32 +951,75 @@ module.exports =
var _util = __webpack_require__(2);
+ // In a bundle of styles, find all of the different class names that a single
+ // named descendant selector can have.
+ var findNamesForDescendants = function findNamesForDescendants(styles, names) {
+ Object.keys(styles).forEach(function (key) {
+ if (key[0] === ':' || key[0] === '@') {
+ // Recurse for pseudo or @media styles
+ findNamesForDescendants(styles[key], names);
+ } else if (key[0] === '>' && key[1] === '>') {
+ // Recurse for descendant styles
+ findNamesForDescendants(styles[key], names);
+
+ // Pluck out all of the names in the _names object.
+ Object.keys(styles[key]._names).forEach(function (name) {
+ names[key] = names[key] || [];
+ names[key].push(name);
+ });
+ }
+ });
+ };
+
var generateCSS = function generateCSS(selector, styleTypes, stringHandlers, useImportant) {
var merged = styleTypes.reduce(_util.recursiveMerge);
+ var classNamesForDescendant = {};
+ findNamesForDescendants(merged, classNamesForDescendant);
+
+ return generateCSSInner(selector, merged, stringHandlers, useImportant, classNamesForDescendant);
+ };
+
+ exports.generateCSS = generateCSS;
+ var generateCSSInner = function generateCSSInner(selector, style, stringHandlers, useImportant, classNamesForDescendant) {
var declarations = {};
var mediaQueries = {};
+ var descendants = {};
var pseudoStyles = {};
- Object.keys(merged).forEach(function (key) {
+ Object.keys(style).forEach(function (key) {
if (key[0] === ':') {
- pseudoStyles[key] = merged[key];
+ pseudoStyles[key] = style[key];
} else if (key[0] === '@') {
- mediaQueries[key] = merged[key];
+ mediaQueries[key] = style[key];
+ } else if (key[0] === '>' && key[1] === '>') {
+ (function () {
+ // So we don't generate weird "_names: [Object object]" styles,
+ // make a copy of the styles and get rid of the _names value.
+ var stylesWithoutNames = _extends({}, style[key]);
+ delete stylesWithoutNames._names;
+
+ // Since our child might have many different names, generate the
+ // styles for all of the possible ones.
+ classNamesForDescendant[key].forEach(function (name) {
+ descendants[name] = stylesWithoutNames;
+ });
+ })();
} else {
- declarations[key] = merged[key];
+ declarations[key] = style[key];
}
});
return generateCSSRuleset(selector, declarations, stringHandlers, useImportant) + Object.keys(pseudoStyles).map(function (pseudoSelector) {
- return generateCSSRuleset(selector + pseudoSelector, pseudoStyles[pseudoSelector], stringHandlers, useImportant);
+ return generateCSSInner(selector + pseudoSelector, pseudoStyles[pseudoSelector], stringHandlers, useImportant, classNamesForDescendant);
}).join("") + Object.keys(mediaQueries).map(function (mediaQuery) {
- var ruleset = generateCSS(selector, [mediaQueries[mediaQuery]], stringHandlers, useImportant);
+ var ruleset = generateCSSInner(selector, mediaQueries[mediaQuery], stringHandlers, useImportant, classNamesForDescendant);
return mediaQuery + '{' + ruleset + '}';
+ }).join("") + Object.keys(descendants).map(function (descendant) {
+ return generateCSSInner(selector + ' .' + descendant, descendants[descendant], stringHandlers, useImportant, classNamesForDescendant);
}).join("");
};
- exports.generateCSS = generateCSS;
var runStringHandlers = function runStringHandlers(declarations, stringHandlers) {
var result = {};
diff --git a/examples/src/StyleTester.js b/examples/src/StyleTester.js
index 38b7269..ae73d9c 100644
--- a/examples/src/StyleTester.js
+++ b/examples/src/StyleTester.js
@@ -38,6 +38,8 @@ const StyleTester = React.createClass({
This should turn red on hover and ???? (blue or red) on active,
This should be centered inside the outer box, even in IE 10.
,
This should animate,
+ Hover over me and this (which should start green) should turn blue
+ ,
];
return
@@ -144,6 +146,20 @@ const styles = StyleSheet.create({
animationDuration: '2s',
animationIterationCount: 'infinite',
},
+
+ hoverbase: {
+ ':hover': {
+ '>>hoverchild': {
+ color: 'blue',
+ },
+ },
+ },
+
+ hoverextra: {
+ '>>hoverchild': {
+ color: 'green',
+ },
+ },
});
const styles2 = StyleSheet.create({
diff --git a/src/generate.js b/src/generate.js
index d7748fc..79eff4c 100644
--- a/src/generate.js
+++ b/src/generate.js
@@ -2,24 +2,68 @@ import Prefixer from 'inline-style-prefixer';
import {
objectToPairs, kebabifyStyleName, recursiveMerge, stringifyValue,
- importantify
+ importantify,
} from './util';
+// In a bundle of styles, find all of the different class names that a single
+// named descendant selector can have.
+const findNamesForDescendants = (styles, names) => {
+ Object.keys(styles).forEach(key => {
+ if (key[0] === ':' || key[0] === '@') {
+ // Recurse for pseudo or @media styles
+ findNamesForDescendants(styles[key], names);
+ } else if (key[0] === '>' && key[1] === '>') {
+ // Recurse for descendant styles
+ findNamesForDescendants(styles[key], names);
+
+ // Pluck out all of the names in the _names object.
+ Object.keys(styles[key]._names).forEach(name => {
+ names[key] = names[key] || [];
+ names[key].push(name);
+ });
+ }
+ });
+};
+
export const generateCSS = (selector, styleTypes, stringHandlers,
- useImportant) => {
+ useImportant) => {
const merged = styleTypes.reduce(recursiveMerge);
+ const classNamesForDescendant = {};
+ findNamesForDescendants(merged, classNamesForDescendant);
+
+ return generateCSSInner(
+ selector, merged, stringHandlers, useImportant,
+ classNamesForDescendant);
+}
+
+const generateCSSInner = (selector, style, stringHandlers,
+ useImportant, classNamesForDescendant) => {
const declarations = {};
const mediaQueries = {};
+ const descendants = {};
const pseudoStyles = {};
- Object.keys(merged).forEach(key => {
+ Object.keys(style).forEach(key => {
if (key[0] === ':') {
- pseudoStyles[key] = merged[key];
+ pseudoStyles[key] = style[key];
} else if (key[0] === '@') {
- mediaQueries[key] = merged[key];
+ mediaQueries[key] = style[key];
+ } else if (key[0] === '>' && key[1] === '>') {
+ // So we don't generate weird "_names: [Object object]" styles,
+ // make a copy of the styles and get rid of the _names value.
+ const stylesWithoutNames = {
+ ...style[key],
+ };
+ delete stylesWithoutNames._names;
+
+ // Since our child might have many different names, generate the
+ // styles for all of the possible ones.
+ classNamesForDescendant[key].forEach(name => {
+ descendants[name] = stylesWithoutNames;
+ });
} else {
- declarations[key] = merged[key];
+ declarations[key] = style[key];
}
});
@@ -27,14 +71,20 @@ export const generateCSS = (selector, styleTypes, stringHandlers,
generateCSSRuleset(selector, declarations, stringHandlers,
useImportant) +
Object.keys(pseudoStyles).map(pseudoSelector => {
- return generateCSSRuleset(selector + pseudoSelector,
- pseudoStyles[pseudoSelector],
- stringHandlers, useImportant);
+ return generateCSSInner(
+ selector + pseudoSelector, pseudoStyles[pseudoSelector],
+ stringHandlers, useImportant, classNamesForDescendant);
}).join("") +
Object.keys(mediaQueries).map(mediaQuery => {
- const ruleset = generateCSS(selector, [mediaQueries[mediaQuery]],
- stringHandlers, useImportant);
+ const ruleset = generateCSSInner(
+ selector, mediaQueries[mediaQuery], stringHandlers,
+ useImportant, classNamesForDescendant);
return `${mediaQuery}{${ruleset}}`;
+ }).join("") +
+ Object.keys(descendants).map(descendant => {
+ return generateCSSInner(
+ `${selector} .${descendant}`, descendants[descendant],
+ stringHandlers, useImportant, classNamesForDescendant);
}).join("")
);
};
diff --git a/src/index.js b/src/index.js
index 21b8573..4c14a60 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,14 +5,43 @@ import {
addRenderedClassNames, getRenderedClassNames
} from './inject';
+// TODO(emily): Make a 'production' mode which doesn't prepend the class name
+// here, to make the generated CSS smaller.
+const makeClassName = (key, vals) => `${key}_${hashObject(vals)}`;
+
+// Find all of the references to descendent styles in a given definition,
+// generates a class name for each of them based on the class name of the
+// parent, and stores that name in a special `_names` object on the style.
+const findAndTagDescendants = (styles, base, names={}) => {
+ Object.keys(styles).forEach(key => {
+ if (key[0] === ':' || key[0] === '@') {
+ findAndTagDescendants(styles[key], base, names);
+ } else if (key[0] === '>' && key[1] === '>') {
+ findAndTagDescendants(styles[key], base, names);
+
+ const name = `${base}__${key.slice(2)}`;
+
+ names[key.slice(2)] = {
+ _isPlainClassName: true,
+ _className: name,
+ };
+
+ styles[key]._names = { [name]: true };
+ }
+ });
+
+ return names;
+};
+
const StyleSheet = {
create(sheetDefinition) {
return mapObj(sheetDefinition, ([key, val]) => {
+ const name = makeClassName(key, val);
+
return [key, {
- // TODO(emily): Make a 'production' mode which doesn't prepend
- // the class name here, to make the generated CSS smaller.
- _name: `${key}_${hashObject(val)}`,
- _definition: val
+ _name: name,
+ _definition: val,
+ ...findAndTagDescendants(val, name),
}];
});
},
@@ -39,6 +68,8 @@ const StyleSheetServer = {
},
};
+const isPlainClassName = (def) => def._isPlainClassName;
+
const css = (...styleDefinitions) => {
// Filter out falsy values from the input, to allow for
// `css(a, test && c)`
@@ -49,11 +80,23 @@ const css = (...styleDefinitions) => {
return "";
}
- const className = validDefinitions.map(s => s._name).join("-o_O-");
+ // Filter out "plain class name" arguments, which just want us to add a
+ // classname to the end result, instead of generating styles.
+ const plainClassNames = validDefinitions.filter(isPlainClassName).map(def => def._className);
+
+ const otherDefinitions = validDefinitions.filter(
+ def => !isPlainClassName(def));
+
+ // If there are only plain class names, just join those.
+ if (otherDefinitions.length === 0) {
+ return plainClassNames.join(" ");
+ }
+
+ const className = otherDefinitions.map(s => s._name).join("-o_O-");
injectStyleOnce(className, `.${className}`,
- validDefinitions.map(d => d._definition));
+ otherDefinitions.map(d => d._definition));
- return className;
+ return [className, ...plainClassNames].join(" ");
};
export default {
diff --git a/tests/generate_test.js b/tests/generate_test.js
index d4f3f92..0a2523b 100644
--- a/tests/generate_test.js
+++ b/tests/generate_test.js
@@ -134,4 +134,120 @@ describe('generateCSS', () => {
display: 'flex',
}], '.foo{display:-webkit-box !important;display:-moz-box !important;display:-ms-flexbox !important;display:-webkit-flex !important;display:flex !important;}');
});
+
+ it('generates descendant styles', () => {
+ assertCSS('.foo', [{
+ color: 'red',
+ '>>blue': {
+ color: 'blue',
+ '>>green': {
+ color: 'green',
+ _names: {
+ 'foo__green': true,
+ },
+ },
+ _names: {
+ 'foo__blue': true,
+ },
+ }
+ }], '.foo{color:red !important;}' +
+ '.foo .foo__blue{color:blue !important;}' +
+ '.foo .foo__blue .foo__green{color:green !important;}');
+ });
+
+ it('handles merging of descendant styles', () => {
+ assertCSS('.foo', [{
+ '>>blue': {
+ color: 'blue',
+ _names: {
+ 'foo_abcdef__blue': true,
+ },
+ },
+ }, {
+ '>>blue': {
+ color: 'green',
+ _names: {
+ 'foo_123456__blue': true,
+ },
+ },
+ }], '.foo .foo_abcdef__blue{color:green !important;}' +
+ '.foo .foo_123456__blue{color:green !important;}');
+ });
+
+ it('handles multiples of the same descendant name', () => {
+ assertCSS('.foo', [{
+ '>>blue': {
+ color: 'blue',
+ _names: {
+ 'foo__blue': true,
+ },
+ },
+ ':hover': {
+ '>>blue': {
+ color: 'green',
+ _names: {
+ 'foo__blue': true,
+ },
+ },
+ },
+ }], '.foo:hover .foo__blue{color:green !important;}' +
+ '.foo .foo__blue{color:blue !important;}');
+ });
+
+ it('handles merging and multiples for descenant styles', () => {
+ assertCSS('.foo', [{
+ '>>child': {
+ color: 'blue',
+ _names: {
+ 'foo_abcdef__child': true,
+ },
+ },
+ ':hover': {
+ '>>child': {
+ color: 'green',
+ _names: {
+ 'foo_abcdef__child': true,
+ },
+ },
+ },
+ }, {
+ ':hover': {
+ '>>child': {
+ color: 'red',
+ _names: {
+ 'foo_123456__child': true,
+ },
+ },
+ },
+ }], '.foo:hover .foo_abcdef__child{color:red !important;}' +
+ '.foo:hover .foo_123456__child{color:red !important;}' +
+ '.foo .foo_abcdef__child{color:blue !important;}' +
+ '.foo .foo_123456__child{color:blue !important;}');
+ });
+
+ it('generates descendant styles with @media queries', () => {
+ assertCSS('.foo', [{
+ '@media screen': {
+ '>>blue': {
+ color: 'blue',
+ _names: {
+ 'foo__blue': true,
+ },
+ }
+ }
+ }], '@media screen{.foo .foo__blue{color:blue !important;}}');
+ });
+
+ it('generates descendant styles with pseudo-styles', () => {
+ assertCSS('.foo', [{
+ ':hover': {
+ '>>blue': {
+ color: 'blue',
+ _names: {
+ 'foo__blue': true,
+ },
+ }
+ }
+ }], '.foo:hover .foo__blue{color:blue !important;}');
+ });
});
diff --git a/tests/index_test.js b/tests/index_test.js
index 8df02b1..5251e6f 100644
--- a/tests/index_test.js
+++ b/tests/index_test.js
@@ -4,6 +4,7 @@ import jsdom from 'jsdom';
import { StyleSheet, StyleSheetServer, css } from '../src/index.js';
import { reset } from '../src/inject.js';
+import { hashObject } from '../src/util.js';
describe('css', () => {
beforeEach(() => {
@@ -118,6 +119,44 @@ describe('css', () => {
done();
});
});
+
+ it('adds extra class names from descendants', () => {
+ const sheet = StyleSheet.create({
+ base: {
+ '>>child': {
+ color: 'red',
+ }
+ },
+
+ blue: {
+ color: 'blue',
+ },
+ });
+
+ assert.equal(
+ css(sheet.blue, sheet.base.child),
+ `blue_${hashObject({ color: "blue" })} ${sheet.base._name}__child`);
+ });
+
+ it('uses the same class name for two different descendants', () => {
+ const sheet = StyleSheet.create({
+ base: {
+ '>>child': {
+ color: 'red',
+ },
+
+ ":hover": {
+ '>>child': {
+ color: 'blue',
+ },
+ },
+ },
+ });
+
+ assert.deepEqual(
+ sheet.base._definition[">>child"]._names,
+ sheet.base._definition[":hover"][">>child"]._names);
+ });
});
describe('StyleSheet.create', () => {