diff --git a/README.md b/README.md index 926e909..4b53214 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ export default Component.extend({ ``` ```css -/* app/styles/app.css-compose */ +/* app/styles/app.css */ @composer { @a-list-component { composes: 'my-list', 'background-g3', 'font-size-big'; @@ -82,15 +82,42 @@ export default Component.extend({ {{/each}} ``` -## The Pieces +### Dynamically Generated Class Names -1. A Broccoli Plugin for finding the config files and processing in order from addon up to app. - - Should create an in memory file for converting a class name into a list of class names -2. The `classNamesMacro` for converting `classNameBindings` that have truthy/falsey values. -3. Babel Plugin to transpile component files looking for string definitions beginning with `@` - - Imports file from step 1 -4. HTMLBars Plugin to transpile template files looking for `class="@"` - - Imports file from step 2 +In the event that you need to dynamically generate class names, you can directly import the `classify` +utility to convert a key into a list of class names. For example: + +```css +/* app/styles/app.css */ +@composer { + @myComponent-red { + composes: 'red'; + } + + @myComponent-blue { + composes: 'blue'; + } + +} +``` + +```js +import Component from 'ember-component'; +import computed from 'ember-computed'; +import get from 'ember-metal/get'; +import { classify } from 'ember-css-composer'; + +export default Component.extend({ + classNameBindings: ['colorClass'], + color: 'red', + + colorClass: computed('color', { + get() { + return classify(`myComponent-${get(this, 'color')}`); + } + }) +}); +``` ## Installation @@ -113,4 +140,4 @@ export default Component.extend({ For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). -![Amadeus](http://24.media.tumblr.com/tumblr_mcbz1pZFKN1qllovxo1_500.gif) \ No newline at end of file +![Amadeus](http://24.media.tumblr.com/tumblr_mcbz1pZFKN1qllovxo1_500.gif) diff --git a/addon/[css-classes-json.js] b/addon/[css-classes-json.js] new file mode 100644 index 0000000..5cc27b0 --- /dev/null +++ b/addon/[css-classes-json.js] @@ -0,0 +1,10 @@ +/** + This is a placeholder file. At build time, the application will generate a + real file that exports a POJO that maps from lookup names to output names. + + Something like: + */ + +export default { + 'component-a': ['class-1', 'class-2', 'class-3'] +}; diff --git a/addon/index.js b/addon/index.js new file mode 100644 index 0000000..544a6d3 --- /dev/null +++ b/addon/index.js @@ -0,0 +1,5 @@ +import classify from './classify'; + +export { + classify +}; diff --git a/index.js b/index.js index 5c0074e..8c51c80 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ /* jshint node: true */ 'use strict'; +const LOOKUP_OUTPUT = 'css-classes-json.js'; const CSStoJSON = require('./lib/css-plugin/index'); +const CleanCSSCompose = require('./lib/css-plugin/clean-composer'); const BabelPlugin = require('./lib/babel-plugin/index'); const BabelTranspiler = require('broccoli-babel-transpiler'); const MergeTrees = require('broccoli-merge-trees'); @@ -16,6 +18,14 @@ module.exports = { return; } + registry.add('css', { + name: 'ember-css-composer-clean-css', + toTree: function(inputTree) { + return new CleanCSSCompose(inputTree); + } + }); + + registry.add('js', { name: 'ember-css-composer-babel', toTree: function(inputTree) { @@ -39,11 +49,11 @@ module.exports = { var addonTree = this._super.treeForAddon.call(this, tree); let cssComposeTree = new Funnel(this.app.trees.app, { - include: ['**/*.css-compose'] + include: ['**/*.css'] }); let configFileTree = CSStoJSON(cssComposeTree, { - outputFile: 'modules/ember-css-composer/css-classes-json.js' + outputFile: `modules/ember-css-composer/${LOOKUP_OUTPUT}` }); return new MergeTrees([addonTree, configFileTree]); diff --git a/lib/css-plugin/clean-composer.js b/lib/css-plugin/clean-composer.js new file mode 100644 index 0000000..8889601 --- /dev/null +++ b/lib/css-plugin/clean-composer.js @@ -0,0 +1,33 @@ +const postcss = require('postcss'); +const Filter = require('broccoli-persistent-filter'); + +CSSStripCompose.prototype = Object.create(Filter.prototype); +CSSStripCompose.prototype.constructor = CSSStripCompose; +CSSStripCompose.prototype.extensions = ['css']; +CSSStripCompose.prototype.targetExtension = 'css'; + +function CSSStripCompose(inputTree, options = {}) { + if (!(this instanceof CSSStripCompose)) { + return new CSSStripCompose(inputTree, options); + } + + Filter.call(this, inputTree, options); +} + +CSSStripCompose.prototype.processString = function(content) { + return removeComposesDecls(content); +}; + +function removeComposesDecls(content) { + let ast = postcss.parse(content); + + ast.walkAtRules((node) => { + if (node.name === 'composer') { + node.remove(); + } + }); + + return ast.toResult().css; +} + +module.exports = CSSStripCompose; diff --git a/node-test/fixtures/css-clean-plugin/test-1/input/app.css b/node-test/fixtures/css-clean-plugin/test-1/input/app.css new file mode 100644 index 0000000..a3549c8 --- /dev/null +++ b/node-test/fixtures/css-clean-plugin/test-1/input/app.css @@ -0,0 +1,17 @@ +@composer { + @test-1 { + composes: 'class-a', 'class-b', 'class-c'; + } + + @test-2 { + composes: 'class-d','class-e'; + } +} + +.class-a { + background: red; +} + +.class-b { + background: green; +} diff --git a/node-test/fixtures/css-clean-plugin/test-1/output/app.css b/node-test/fixtures/css-clean-plugin/test-1/output/app.css new file mode 100644 index 0000000..bff4029 --- /dev/null +++ b/node-test/fixtures/css-clean-plugin/test-1/output/app.css @@ -0,0 +1,7 @@ +.class-a { + background: red; +} + +.class-b { + background: green; +} diff --git a/node-test/fixtures/css-clean-plugin/test-2/input/app-2.css b/node-test/fixtures/css-clean-plugin/test-2/input/app-2.css new file mode 100644 index 0000000..6f4b44f --- /dev/null +++ b/node-test/fixtures/css-clean-plugin/test-2/input/app-2.css @@ -0,0 +1,13 @@ +.class-e { + font-size: 12px; +} + +@composer { + @test-2 { + composes: 'class-d','class-e'; + } +} + +.other-styles { + color: blue; +} diff --git a/node-test/fixtures/css-clean-plugin/test-2/input/app.css b/node-test/fixtures/css-clean-plugin/test-2/input/app.css new file mode 100644 index 0000000..068628a --- /dev/null +++ b/node-test/fixtures/css-clean-plugin/test-2/input/app.css @@ -0,0 +1,13 @@ +@composer { + @test-1 { + composes: 'class-a', 'class-b', 'class-c'; + } +} + +.class-a { + background: red; +} + +.class-b { + background: green; +} diff --git a/node-test/fixtures/css-clean-plugin/test-2/output/app-2.css b/node-test/fixtures/css-clean-plugin/test-2/output/app-2.css new file mode 100644 index 0000000..c1ec887 --- /dev/null +++ b/node-test/fixtures/css-clean-plugin/test-2/output/app-2.css @@ -0,0 +1,7 @@ +.class-e { + font-size: 12px; +} + +.other-styles { + color: blue; +} diff --git a/node-test/fixtures/css-clean-plugin/test-2/output/app.css b/node-test/fixtures/css-clean-plugin/test-2/output/app.css new file mode 100644 index 0000000..bff4029 --- /dev/null +++ b/node-test/fixtures/css-clean-plugin/test-2/output/app.css @@ -0,0 +1,7 @@ +.class-a { + background: red; +} + +.class-b { + background: green; +} diff --git a/node-test/fixtures/css-plugin/test-1/input/app.css-compose b/node-test/fixtures/css-plugin/test-1/input/app.css similarity index 98% rename from node-test/fixtures/css-plugin/test-1/input/app.css-compose rename to node-test/fixtures/css-plugin/test-1/input/app.css index cc133db..5408eda 100644 --- a/node-test/fixtures/css-plugin/test-1/input/app.css-compose +++ b/node-test/fixtures/css-plugin/test-1/input/app.css @@ -6,4 +6,4 @@ @test-2 { composes: 'class-d','class-e'; } -} \ No newline at end of file +} diff --git a/node-test/fixtures/css-plugin/test-2/input/app-2.css-compose b/node-test/fixtures/css-plugin/test-2/input/app-2.css similarity index 100% rename from node-test/fixtures/css-plugin/test-2/input/app-2.css-compose rename to node-test/fixtures/css-plugin/test-2/input/app-2.css diff --git a/node-test/fixtures/css-plugin/test-2/input/app.css-compose b/node-test/fixtures/css-plugin/test-2/input/app.css similarity index 100% rename from node-test/fixtures/css-plugin/test-2/input/app.css-compose rename to node-test/fixtures/css-plugin/test-2/input/app.css diff --git a/node-test/helpers/clean-compose-css-file.js b/node-test/helpers/clean-compose-css-file.js new file mode 100644 index 0000000..e8634b5 --- /dev/null +++ b/node-test/helpers/clean-compose-css-file.js @@ -0,0 +1,28 @@ +const broccoli = require('broccoli'); +const plugin = require('../../lib/css-plugin/clean-composer'); +const fs = require('fs'); +const path = require('path'); +const filesPath = 'node-test/fixtures/css-clean-plugin'; +const outputFile = 'some/file/tmp.js'; + +module.exports = { + transform(treePath) { + let tree = plugin(`${filesPath}/${treePath}/input`, { outputFile }); + let builder = new broccoli.Builder(tree); + + return builder.build().then(() => { + return filesMap(builder.outputPath); + }); + }, + getExpectedResult(path) { + let fullPath = `${filesPath}/${path}/output`; + return filesMap(fullPath); + } +} + +function filesMap(folder) { + return fs.readdirSync(folder).reduce((hash, file) => { + hash[file] = fs.readFileSync(path.join(folder, file), 'utf8'); + return hash; + }, {}); +} diff --git a/node-test/lib/css-plugin/clean-composer-nodetest.js b/node-test/lib/css-plugin/clean-composer-nodetest.js new file mode 100644 index 0000000..a6aaf70 --- /dev/null +++ b/node-test/lib/css-plugin/clean-composer-nodetest.js @@ -0,0 +1,33 @@ +const { transform, getExpectedResult } = require('../../helpers/clean-compose-css-file'); +const should = require('should'); + +describe('css plugin', function() { + it('works with a single file', function() { + // Make sure it strips out any @compose declarations. + return transform('test-1').then((result) => { + let expected = getExpectedResult('test-1'); + assertKeys(expected, result); + }); + }); + + it('works with multiple files', function() { + // Make sure it strips out any @compose declarations. + return transform('test-2').then((result) => { + let expected = getExpectedResult('test-2'); + assertKeys(expected, result); + }); + }); +}); + +function assertKeys(expectedHash, resultHash) { + let expectedKeys = Object.keys(expectedHash); + let resultKeys = Object.keys(resultHash); + + // Assert the same keys + expectedKeys.should.deepEqual(resultKeys); + + // Assert each of the values are the same + expectedKeys.forEach((key) => { + expectedHash[key].should.equal(resultHash[key]); + }); +} diff --git a/node-test/lib/css-plugin/index-nodetest.js b/node-test/lib/css-plugin/index-nodetest.js index 9cb497d..7d035f4 100644 --- a/node-test/lib/css-plugin/index-nodetest.js +++ b/node-test/lib/css-plugin/index-nodetest.js @@ -15,4 +15,4 @@ describe('css plugin', function() { result.should.equal(expected); }); }); -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index 62b5d7f..2ff8c09 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "postcss": "^5.2.8" }, "devDependencies": { + "babel-core": "^5.8.38", "babel-preset-es2015": "^6.18.0", "babelify": "^7.3.0", "broccoli-asset-rev": "^2.4.5", diff --git a/tests/dummy/app/styles/app.css b/tests/dummy/app/styles/app.css index e69de29..e942d53 100644 --- a/tests/dummy/app/styles/app.css +++ b/tests/dummy/app/styles/app.css @@ -0,0 +1,17 @@ +@composer { + @testing-basic { + composes: 'tb-1', 'tb-2'; + } + + @testing-basic__open { + composes: 'tb-3', 'tb-4'; + } + + @testing-basic__closed { + composes: 'tb-5'; + } + + @testing-basic__inner { + composes: 'tb-6', 'tb-7', 'tb-8'; + } +} diff --git a/tests/dummy/app/styles/config.css-compose b/tests/dummy/app/styles/config.css-compose deleted file mode 100644 index 8475e3a..0000000 --- a/tests/dummy/app/styles/config.css-compose +++ /dev/null @@ -1,17 +0,0 @@ -@composer { - @testing-basic { - composes: 'tb-1', 'tb-2'; - } - - @testing-basic__open { - composes: 'tb-3', 'tb-4'; - } - - @testing-basic__closed { - composes: 'tb-5'; - } - - @testing-basic__inner { - composes: 'tb-6', 'tb-7', 'tb-8'; - } -} \ No newline at end of file