├── src ├── config.js ├── index.js ├── parse-class-expression.js ├── utils.js └── create-element.js ├── .gitignore ├── assets └── logo.png ├── test └── index.test.js ├── lib ├── config.js ├── index.js ├── parse-class-expression.js ├── utils.js └── create-element.js ├── .travis.yml ├── demo ├── styles │ └── button.module.css ├── template-with-outer-modules.vue ├── jsx.vue ├── template-with-inline-modules-global.vue ├── template-with-inline-modules.vue ├── renderFn.vue ├── renderFn-functional.vue └── index.js ├── .editorconfig ├── bdr.config.js ├── LICENSE ├── CHANGELOG.md ├── package.json ├── dist ├── vue-css-modules.min.js ├── vue-css-modules.es.js ├── vue-css-modules.cjs.js └── vue-css-modules.js ├── README_zh-CN.md └── README.md /src/config.js: -------------------------------------------------------------------------------- 1 | export const INJECT_ATTR = 'styleName' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.log 4 | coverage 5 | .vscode 6 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fjc0k/vue-css-modules/HEAD/assets/logo.png -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import app from '../src' 2 | 3 | test('test', () => { 4 | expect(app).toBe(app) 5 | }) 6 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.INJECT_ATTR = void 0; 5 | var INJECT_ATTR = 'styleName'; 6 | exports.INJECT_ATTR = INJECT_ATTR; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 9 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - npm install 9 | script: 10 | - yarn test 11 | after_success: 12 | - yarn codecov 13 | -------------------------------------------------------------------------------- /demo/styles/button.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | font-size: 18px; 4 | padding: 10px; 5 | border: 1px solid green; 6 | } 7 | 8 | .disabled { 9 | opacity: .5; 10 | color: black; 11 | } 12 | 13 | .mini { 14 | font-size: 12px; 15 | } 16 | 17 | .primary { 18 | color: green; 19 | } 20 | 21 | .danger { 22 | color: red; 23 | } 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | # Minified JavaScript files shouldn't be changed 17 | [**.min.js] 18 | indent_style = ignore 19 | insert_final_newline = ignore 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /demo/template-with-outer-modules.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | -------------------------------------------------------------------------------- /demo/jsx.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | import createElement from './create-element' 3 | 4 | const CSSModules = styles => ({ 5 | beforeCreate() { 6 | this.original$createElement = this.original$createElement || this.$createElement 7 | this.original_c = this.original_c || this._c 8 | this.$createElement = createElement.bind(this, { 9 | createElement: this.original$createElement, 10 | context: this, 11 | styles 12 | }) 13 | this._c = createElement.bind(this, { 14 | createElement: this.original_c, 15 | context: this, 16 | styles 17 | }) 18 | } 19 | }) 20 | 21 | CSSModules.install = Vue => { 22 | Vue.mixin(CSSModules()) 23 | } 24 | 25 | export default CSSModules 26 | -------------------------------------------------------------------------------- /demo/template-with-inline-modules-global.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 43 | -------------------------------------------------------------------------------- /demo/template-with-inline-modules.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 49 | -------------------------------------------------------------------------------- /bdr.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | 'vue-css-modules': [ 4 | 'src/index.js', 5 | 'VueCSSModules' 6 | ] 7 | }, 8 | getUmdMinSize(rawSize, gzippedSize) { 9 | const path = require('path') 10 | const fs = require('fs') 11 | 12 | const tasks = [ 13 | ['README.md', /[^-]+(?=-blue\.svg\?MIN)/, /[^-]+(?=-blue\.svg\?MZIP)/], 14 | ['README_zh-CN.md', /[^-]+(?=-blue\.svg\?MIN)/, /[^-]+(?=-blue\.svg\?MZIP)/] 15 | ] 16 | 17 | tasks.forEach(task => { 18 | const filePath = path.resolve(__dirname, task[0]) 19 | const content = fs.readFileSync(filePath) 20 | fs.writeFileSync( 21 | filePath, 22 | String(content) 23 | .replace(task[1], encodeURIComponent(rawSize)) 24 | .replace(task[2], encodeURIComponent(gzippedSize)) 25 | ) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/renderFn.vue: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /demo/renderFn-functional.vue: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /src/parse-class-expression.js: -------------------------------------------------------------------------------- 1 | import { includes, camelCase } from './utils' 2 | 3 | const cache = Object.create(null) 4 | 5 | export default expression => { 6 | if (cache[expression]) return cache[expression] 7 | 8 | let className 9 | let binding 10 | let bindingValue 11 | let role 12 | 13 | if (includes(expression, '=', 1)) { // eg: disabled=isDisabled 14 | [className, binding] = expression.split('=') 15 | } else { 16 | const modifier = expression[0] 17 | if (modifier === '$') { // eg: $type 18 | binding = expression.substr(1) 19 | bindingValue = true 20 | } else if (modifier === '@') { // eg: @button 21 | className = expression.substr(1) 22 | role = className 23 | } else if (modifier === ':') { // eg: :disabled 24 | className = expression.substr(1) 25 | binding = camelCase(className) 26 | } else { 27 | className = expression 28 | } 29 | } 30 | 31 | cache[expression] = { 32 | className, 33 | binding, 34 | bindingValue, 35 | role 36 | } 37 | 38 | return cache[expression] 39 | } 40 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.default = void 0; 5 | 6 | var _createElement = _interopRequireDefault(require("./create-element")); 7 | 8 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 9 | 10 | /* eslint camelcase: 0 */ 11 | var CSSModules = function CSSModules(styles) { 12 | return { 13 | beforeCreate: function beforeCreate() { 14 | this.original$createElement = this.original$createElement || this.$createElement; 15 | this.original_c = this.original_c || this._c; 16 | this.$createElement = _createElement.default.bind(this, { 17 | createElement: this.original$createElement, 18 | context: this, 19 | styles: styles 20 | }); 21 | this._c = _createElement.default.bind(this, { 22 | createElement: this.original_c, 23 | context: this, 24 | styles: styles 25 | }); 26 | } 27 | }; 28 | }; 29 | 30 | CSSModules.install = function (Vue) { 31 | Vue.mixin(CSSModules()); 32 | }; 33 | 34 | var _default = CSSModules; 35 | exports.default = _default; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 fjc0k 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [1.2.0](https://github.com/fjc0k/vue-css-modules/compare/v1.1.0...v1.2.0) (2018-06-29) 7 | 8 | 9 | ### Features 10 | 11 | * support global installation ([0e6174d](https://github.com/fjc0k/vue-css-modules/commit/0e6174d)) 12 | 13 | 14 | 15 | 16 | # [1.1.0](https://github.com/fjc0k/vue-css-modules/compare/v1.0.5...v1.1.0) (2018-05-18) 17 | 18 | 19 | ### Features 20 | 21 | * extract styles from context ([b846fb8](https://github.com/fjc0k/vue-css-modules/commit/b846fb8)) 22 | 23 | 24 | 25 | 26 | ## [1.0.5](https://github.com/fjc0k/vue-css-modules/compare/v1.0.4...v1.0.5) (2018-04-18) 27 | 28 | 29 | 30 | 31 | ## [1.0.4](https://github.com/fjc0k/vue-css-modules/compare/v1.0.3...v1.0.4) (2018-04-18) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * postbump ([1de199b](https://github.com/fjc0k/vue-css-modules/commit/1de199b)) 37 | 38 | 39 | 40 | 41 | ## 1.0.3 (2018-04-18) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * prepend the class names to data.staticClass ([277423f](https://github.com/fjc0k/vue-css-modules/commit/277423f)) 47 | -------------------------------------------------------------------------------- /lib/parse-class-expression.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.default = void 0; 5 | 6 | var _utils = require("./utils"); 7 | 8 | var cache = Object.create(null); 9 | 10 | var _default = function _default(expression) { 11 | if (cache[expression]) return cache[expression]; 12 | var className; 13 | var binding; 14 | var bindingValue; 15 | var role; 16 | 17 | if ((0, _utils.includes)(expression, '=', 1)) { 18 | // eg: disabled=isDisabled 19 | var _expression$split = expression.split('='); 20 | 21 | className = _expression$split[0]; 22 | binding = _expression$split[1]; 23 | } else { 24 | var modifier = expression[0]; 25 | 26 | if (modifier === '$') { 27 | // eg: $type 28 | binding = expression.substr(1); 29 | bindingValue = true; 30 | } else if (modifier === '@') { 31 | // eg: @button 32 | className = expression.substr(1); 33 | role = className; 34 | } else if (modifier === ':') { 35 | // eg: :disabled 36 | className = expression.substr(1); 37 | binding = (0, _utils.camelCase)(className); 38 | } else { 39 | className = expression; 40 | } 41 | } 42 | 43 | cache[expression] = { 44 | className: className, 45 | binding: binding, 46 | bindingValue: bindingValue, 47 | role: role 48 | }; 49 | return cache[expression]; 50 | }; 51 | 52 | exports.default = _default; -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import CSSModules from '../src' 3 | 4 | import renderFn from './renderFn' 5 | import renderFnFunctional from './renderFn-functional' 6 | import templateWithOuterModules from './template-with-outer-modules' 7 | import templateWithInlineModules from './template-with-inline-modules' 8 | import templateWithInlineModulesGlobal from './template-with-inline-modules-global' 9 | 10 | Vue.use(CSSModules) 11 | 12 | const demos = { 13 | 'renderFn-functional': renderFnFunctional, 14 | renderFn: renderFn, 15 | 'template-with-inline-modules': templateWithInlineModules, 16 | 'template-with-inline-modules-global': templateWithInlineModulesGlobal, 17 | 'template-with-outer-modules': templateWithOuterModules 18 | } 19 | 20 | // eslint-disable-next-line 21 | new Vue({ 22 | el: '#app', 23 | data: { 24 | currentDemo: renderFn 25 | }, 26 | render(h) { 27 | return ( 28 | h('div', [ 29 | h('div', 'Click button'), 30 | h('hr'), 31 | h(this.currentDemo, { 32 | class: 'custom' 33 | }), 34 | h('hr'), 35 | Object.keys(demos).map(demo => ( 36 | h('button', { 37 | style: 'margin:10px;font-size:18px;' + ( 38 | this.currentDemo === demos[demo] ? 'border-color:red;' : '' 39 | ), 40 | on: { 41 | click: () => { 42 | this.currentDemo = demos[demo] 43 | } 44 | } 45 | }, demo) 46 | )), 47 | h('hr'), 48 | h('pre', [JSON.stringify( 49 | this.currentDemo, 50 | null, 51 | 2 52 | )]) 53 | ]) 54 | ) 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function includes(arrayLike, element, fromIndex = 0) { 2 | for (let i = fromIndex, len = arrayLike.length; i < len; i++) { 3 | if (arrayLike[i] === element) { 4 | return true 5 | } 6 | } 7 | return false 8 | } 9 | 10 | export function isObject(value) { 11 | return value !== null && typeof value === 'object' 12 | } 13 | 14 | export function isFunction(value) { 15 | return typeof value === 'function' 16 | } 17 | 18 | export function isString(value) { 19 | return typeof value === 'string' 20 | } 21 | 22 | const camelCaseCache = Object.create(null) 23 | export function camelCase(value) { 24 | if (camelCaseCache[value]) return camelCaseCache[value] 25 | let result = '' 26 | let shouldUpperCase = false 27 | for (let i = 0, len = value.length; i < len; i++) { 28 | const char = value[i] 29 | if (char === '-') { 30 | shouldUpperCase = true 31 | } else { 32 | result += (result && shouldUpperCase) ? char.toUpperCase() : char 33 | shouldUpperCase = false 34 | } 35 | } 36 | camelCaseCache[value] = result 37 | return result 38 | } 39 | 40 | const dashCaseCache = Object.create(null) 41 | export function dashCase(value) { 42 | if (dashCaseCache[value]) return dashCaseCache[value] 43 | let result = '' 44 | let shouldAddDash = false 45 | for (let i = value.length - 1; i >= 0; i--) { 46 | const char = value[i] 47 | const charCode = char.charCodeAt(0) 48 | if (charCode >= 65 && charCode <= 90) { 49 | shouldAddDash = true 50 | result = char.toLowerCase() + result 51 | } else { 52 | result = char + (shouldAddDash ? '-' : '') + result 53 | shouldAddDash = false 54 | } 55 | } 56 | dashCaseCache[value] = result 57 | return result 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-css-modules", 3 | "version": "1.2.0", 4 | "description": "Seamless mapping of class names to CSS modules inside of Vue components.", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "unpkg": "dist/vue-css-modules.min.js", 8 | "jsdelivr": "dist/vue-css-modules.min.js", 9 | "homepage": "https://github.com/fjc0k/vue-css-modules", 10 | "author": { 11 | "name": "fjc0k", 12 | "email": "fjc0kb@gmail.com", 13 | "url": "https://github.com/fjc0k" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:fjc0k/vue-css-modules.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/fjc0k/vue-css-modules/issues" 21 | }, 22 | "keywords": [ 23 | "vue", 24 | "vuejs", 25 | "css", 26 | "css-modules", 27 | "postcss", 28 | "sass", 29 | "less", 30 | "stylus" 31 | ], 32 | "files": [ 33 | "dist", 34 | "lib" 35 | ], 36 | "scripts": { 37 | "demo": "bdr dev demo/index.js --css.modules", 38 | "test": "jest --coverage", 39 | "build": "bdr transform & bdr build", 40 | "release": "standard-version -a", 41 | "postrelease": "git push --follow-tags origin master && npm publish" 42 | }, 43 | "standard-version": { 44 | "scripts": { 45 | "postbump": "yarn build && git add -A" 46 | } 47 | }, 48 | "eslintConfig": { 49 | "root": true, 50 | "extends": "@fir-ui/fir" 51 | }, 52 | "eslintIgnore": [ 53 | "dist", 54 | "lib" 55 | ], 56 | "babel": { 57 | "presets": [ 58 | [ 59 | "@bdr/bdr" 60 | ] 61 | ] 62 | }, 63 | "devDependencies": { 64 | "@babel/core": "^7.0.0-beta.44", 65 | "@fir-ui/eslint-config-fir": "^0.3.2", 66 | "babel-core": "^7.0.0-bridge.0", 67 | "babel-jest": "^22.4.3", 68 | "bdr": "^1.3.4", 69 | "codecov": "^3.0.0", 70 | "eslint": "^4.19.1", 71 | "jest": "^22.4.3", 72 | "standard-version": "^4.3.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.__esModule = true; 4 | exports.includes = includes; 5 | exports.isObject = isObject; 6 | exports.isFunction = isFunction; 7 | exports.isString = isString; 8 | exports.camelCase = camelCase; 9 | exports.dashCase = dashCase; 10 | 11 | function includes(arrayLike, element, fromIndex) { 12 | if (fromIndex === void 0) { 13 | fromIndex = 0; 14 | } 15 | 16 | for (var i = fromIndex, len = arrayLike.length; i < len; i++) { 17 | if (arrayLike[i] === element) { 18 | return true; 19 | } 20 | } 21 | 22 | return false; 23 | } 24 | 25 | function isObject(value) { 26 | return value !== null && typeof value === 'object'; 27 | } 28 | 29 | function isFunction(value) { 30 | return typeof value === 'function'; 31 | } 32 | 33 | function isString(value) { 34 | return typeof value === 'string'; 35 | } 36 | 37 | var camelCaseCache = Object.create(null); 38 | 39 | function camelCase(value) { 40 | if (camelCaseCache[value]) return camelCaseCache[value]; 41 | var result = ''; 42 | var shouldUpperCase = false; 43 | 44 | for (var i = 0, len = value.length; i < len; i++) { 45 | var char = value[i]; 46 | 47 | if (char === '-') { 48 | shouldUpperCase = true; 49 | } else { 50 | result += result && shouldUpperCase ? char.toUpperCase() : char; 51 | shouldUpperCase = false; 52 | } 53 | } 54 | 55 | camelCaseCache[value] = result; 56 | return result; 57 | } 58 | 59 | var dashCaseCache = Object.create(null); 60 | 61 | function dashCase(value) { 62 | if (dashCaseCache[value]) return dashCaseCache[value]; 63 | var result = ''; 64 | var shouldAddDash = false; 65 | 66 | for (var i = value.length - 1; i >= 0; i--) { 67 | var char = value[i]; 68 | var charCode = char.charCodeAt(0); 69 | 70 | if (charCode >= 65 && charCode <= 90) { 71 | shouldAddDash = true; 72 | result = char.toLowerCase() + result; 73 | } else { 74 | result = char + (shouldAddDash ? '-' : '') + result; 75 | shouldAddDash = false; 76 | } 77 | } 78 | 79 | dashCaseCache[value] = result; 80 | return result; 81 | } -------------------------------------------------------------------------------- /dist/vue-css-modules.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-css-modules v1.2.0 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueCSSModules=e()}(this,function(){"use strict";var t=Object.create(null);var e=Object.create(null),n=function(n){if(e[n])return e[n];var i,r,s,a;if(function(t,e,n){void 0===n&&(n=0);for(var i=n,r=t.length;i 18 | 19 | ## CSS Modules:局部作用域 & 模块化 20 | 21 | [`CSS Modules`](https://github.com/css-modules/css-modules) 为每一个局部类赋予全局唯一的类名,这样组件样式间就不会相互影响了。如: 22 | 23 | ```css 24 | /* button.css */ 25 | .button { 26 | font-size: 16px; 27 | } 28 | .mini { 29 | font-size: 12px; 30 | } 31 | ``` 32 | 33 | 它会被转换为类似这样: 34 | 35 | ```css 36 | /* button.css */ 37 | .button__button--d8fj3 { 38 | font-size: 16px; 39 | } 40 | .button__mini--f90jc { 41 | font-size: 12px; 42 | } 43 | ``` 44 | 45 | 当导入一个 CSS 模块文件时,它会将局部类名到全局类名的映射对象提供给我们。就像这样: 46 | 47 | ```javascript 48 | import styles from './button.css' 49 | // styles = { 50 | // button: 'button__button--d8fj3', 51 | // mini: 'button__mini--f90jc' 52 | // } 53 | 54 | element.innerHTML = ' 122 | ``` 123 | 124 | 这等同于: 125 | 126 | ```html 127 | 128 | ``` 129 | 130 | 这让你能在外部重置组件的样式: 131 | 132 | ```css 133 | .form [data-component-button] { 134 | font-size: 20px; 135 | } 136 | ``` 137 | 138 | ### $type 139 | 140 | ```html 141 | 142 | ``` 143 | 144 | 这等同于: 145 | 146 | ```html 147 | 148 | ``` 149 | 150 | ### :mini 151 | 152 | ```html 153 | 154 | ``` 155 | 156 | 这等同于: 157 | 158 | ```html 159 | 160 | ``` 161 | 162 | ### disabled=isDisabled 163 | 164 | ```html 165 | 166 | ``` 167 | 168 | 这等同于: 169 | 170 | ```html 171 | 172 | ``` 173 | 174 | ## 使用方法 175 | 176 | ### 在 Vue 模板中使用 177 | 178 | #### 引入模板外部的 CSS 模块 179 | 180 | ```html 181 | 188 | 189 | 198 | ``` 199 | 200 | #### 使用模板内部的 CSS 模块 201 | 202 | ```html 203 | 210 | 211 | 219 | 220 | 228 | ``` 229 | 230 | ### 在 Vue JSX 中使用 231 | 232 | ```javascript 233 | import CSSModules from 'vue-css-modules' 234 | import styles from './button.css' 235 | 236 | export default { 237 | mixins: [CSSModules(styles)], 238 | props: { mini: Boolean }, 239 | render() { 240 | return ( 241 | 242 | ) 243 | } 244 | } 245 | ``` 246 | 247 | ### 在 Vue 渲染函数中使用 248 | 249 | ```javascript 250 | import CSSModules from 'vue-css-modules' 251 | import styles from './button.css' 252 | 253 | export default { 254 | mixins: [CSSModules(styles)], 255 | props: { mini: Boolean }, 256 | render(h) { 257 | return h('button', { 258 | styleName: '@button :mini' 259 | }, '点我') 260 | } 261 | } 262 | ``` 263 | 264 | ## 实现原理 265 | 266 | `vue-css-modules` 注册了 [`beforeCreate`](https://cn.vuejs.org/v2/api/#beforeCreate) 钩子,在钩子中劫持了组件的渲染函数。对于传给渲染函数的参数,将会解析其 `data` 或 `data.attrs` 中的 `styleName` 属性生成全局类名字符串,并将它附着在 `data.staticClass` 值的后面。 267 | -------------------------------------------------------------------------------- /dist/vue-css-modules.es.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-css-modules v1.2.0 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | function includes(arrayLike, element, fromIndex) { 7 | if (fromIndex === void 0) { 8 | fromIndex = 0; 9 | } 10 | 11 | for (var i = fromIndex, len = arrayLike.length; i < len; i++) { 12 | if (arrayLike[i] === element) { 13 | return true; 14 | } 15 | } 16 | 17 | return false; 18 | } 19 | function isObject(value) { 20 | return value !== null && typeof value === 'object'; 21 | } 22 | function isFunction(value) { 23 | return typeof value === 'function'; 24 | } 25 | function isString(value) { 26 | return typeof value === 'string'; 27 | } 28 | var camelCaseCache = Object.create(null); 29 | function camelCase(value) { 30 | if (camelCaseCache[value]) return camelCaseCache[value]; 31 | var result = ''; 32 | var shouldUpperCase = false; 33 | 34 | for (var i = 0, len = value.length; i < len; i++) { 35 | var char = value[i]; 36 | 37 | if (char === '-') { 38 | shouldUpperCase = true; 39 | } else { 40 | result += result && shouldUpperCase ? char.toUpperCase() : char; 41 | shouldUpperCase = false; 42 | } 43 | } 44 | 45 | camelCaseCache[value] = result; 46 | return result; 47 | } 48 | 49 | var cache = Object.create(null); 50 | var parseClassExpression = (function (expression) { 51 | if (cache[expression]) return cache[expression]; 52 | var className; 53 | var binding; 54 | var bindingValue; 55 | var role; 56 | 57 | if (includes(expression, '=', 1)) { 58 | // eg: disabled=isDisabled 59 | var _expression$split = expression.split('='); 60 | 61 | className = _expression$split[0]; 62 | binding = _expression$split[1]; 63 | } else { 64 | var modifier = expression[0]; 65 | 66 | if (modifier === '$') { 67 | // eg: $type 68 | binding = expression.substr(1); 69 | bindingValue = true; 70 | } else if (modifier === '@') { 71 | // eg: @button 72 | className = expression.substr(1); 73 | role = className; 74 | } else if (modifier === ':') { 75 | // eg: :disabled 76 | className = expression.substr(1); 77 | binding = camelCase(className); 78 | } else { 79 | className = expression; 80 | } 81 | } 82 | 83 | cache[expression] = { 84 | className: className, 85 | binding: binding, 86 | bindingValue: bindingValue, 87 | role: role 88 | }; 89 | return cache[expression]; 90 | }); 91 | 92 | var INJECT_ATTR = 'styleName'; 93 | 94 | /* eslint max-depth: 0 guard-for-in: 0 */ 95 | 96 | function createElement(_) { 97 | var args = [].slice.call(arguments, 1); // for functional component 98 | 99 | if (isFunction(_)) { 100 | return createElement.bind(_, { 101 | functional: true, 102 | createElement: _, 103 | styles: args[0], 104 | context: args[1] 105 | }); 106 | } 107 | 108 | var _$functional = _.functional, 109 | functional = _$functional === void 0 ? false : _$functional, 110 | h = _.createElement, 111 | _$context = _.context, 112 | context = _$context === void 0 ? {} : _$context, 113 | _$styles = _.styles, 114 | styles = _$styles === void 0 ? context.$style || {} : _$styles; 115 | 116 | if (isString(styles)) { 117 | styles = (functional ? (context.injections || {})[styles] : context[styles]) || {}; 118 | } 119 | 120 | if (functional) { 121 | context = context.props || {}; 122 | } 123 | 124 | var data = args[1]; 125 | 126 | if (isObject(data)) { 127 | if (!data.staticClass) { 128 | data.staticClass = ''; 129 | } 130 | 131 | if (!data.attrs) { 132 | data.attrs = {}; 133 | } 134 | 135 | var modules = data[INJECT_ATTR] || data.attrs[INJECT_ATTR] || ''; 136 | 137 | if (modules.length) { 138 | var _modules = Array.isArray(modules) ? modules : [modules]; 139 | 140 | for (var i in _modules) { 141 | var module = _modules[i]; 142 | 143 | if (module && typeof module === 'string') { 144 | var classExpressions = module.split(/\s+/g); 145 | 146 | for (var _i in classExpressions) { 147 | var classExpression = classExpressions[_i]; 148 | 149 | var _parseClassExpression = parseClassExpression(classExpression), 150 | className = _parseClassExpression.className, 151 | binding = _parseClassExpression.binding, 152 | bindingValue = _parseClassExpression.bindingValue, 153 | role = _parseClassExpression.role; 154 | 155 | if (bindingValue) { 156 | className = context[binding]; 157 | binding = undefined; 158 | } 159 | 160 | if ((binding ? context[binding] : true) && styles[className]) { 161 | data.staticClass += " " + styles[className]; 162 | data.staticClass = data.staticClass.trim(); 163 | } 164 | 165 | if (role) { 166 | data.attrs["data-component-" + role] = ''; 167 | } 168 | } 169 | } 170 | } 171 | } // remove styleName attr 172 | 173 | 174 | delete data[INJECT_ATTR]; 175 | delete data.attrs[INJECT_ATTR]; 176 | } 177 | 178 | return h.apply(null, args); 179 | } 180 | 181 | /* eslint camelcase: 0 */ 182 | 183 | var CSSModules = function CSSModules(styles) { 184 | return { 185 | beforeCreate: function beforeCreate() { 186 | this.original$createElement = this.original$createElement || this.$createElement; 187 | this.original_c = this.original_c || this._c; 188 | this.$createElement = createElement.bind(this, { 189 | createElement: this.original$createElement, 190 | context: this, 191 | styles: styles 192 | }); 193 | this._c = createElement.bind(this, { 194 | createElement: this.original_c, 195 | context: this, 196 | styles: styles 197 | }); 198 | } 199 | }; 200 | }; 201 | 202 | CSSModules.install = function (Vue) { 203 | Vue.mixin(CSSModules()); 204 | }; 205 | 206 | export default CSSModules; 207 | -------------------------------------------------------------------------------- /dist/vue-css-modules.cjs.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-css-modules v1.2.0 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | 'use strict'; 7 | 8 | function includes(arrayLike, element, fromIndex) { 9 | if (fromIndex === void 0) { 10 | fromIndex = 0; 11 | } 12 | 13 | for (var i = fromIndex, len = arrayLike.length; i < len; i++) { 14 | if (arrayLike[i] === element) { 15 | return true; 16 | } 17 | } 18 | 19 | return false; 20 | } 21 | function isObject(value) { 22 | return value !== null && typeof value === 'object'; 23 | } 24 | function isFunction(value) { 25 | return typeof value === 'function'; 26 | } 27 | function isString(value) { 28 | return typeof value === 'string'; 29 | } 30 | var camelCaseCache = Object.create(null); 31 | function camelCase(value) { 32 | if (camelCaseCache[value]) return camelCaseCache[value]; 33 | var result = ''; 34 | var shouldUpperCase = false; 35 | 36 | for (var i = 0, len = value.length; i < len; i++) { 37 | var char = value[i]; 38 | 39 | if (char === '-') { 40 | shouldUpperCase = true; 41 | } else { 42 | result += result && shouldUpperCase ? char.toUpperCase() : char; 43 | shouldUpperCase = false; 44 | } 45 | } 46 | 47 | camelCaseCache[value] = result; 48 | return result; 49 | } 50 | 51 | var cache = Object.create(null); 52 | var parseClassExpression = (function (expression) { 53 | if (cache[expression]) return cache[expression]; 54 | var className; 55 | var binding; 56 | var bindingValue; 57 | var role; 58 | 59 | if (includes(expression, '=', 1)) { 60 | // eg: disabled=isDisabled 61 | var _expression$split = expression.split('='); 62 | 63 | className = _expression$split[0]; 64 | binding = _expression$split[1]; 65 | } else { 66 | var modifier = expression[0]; 67 | 68 | if (modifier === '$') { 69 | // eg: $type 70 | binding = expression.substr(1); 71 | bindingValue = true; 72 | } else if (modifier === '@') { 73 | // eg: @button 74 | className = expression.substr(1); 75 | role = className; 76 | } else if (modifier === ':') { 77 | // eg: :disabled 78 | className = expression.substr(1); 79 | binding = camelCase(className); 80 | } else { 81 | className = expression; 82 | } 83 | } 84 | 85 | cache[expression] = { 86 | className: className, 87 | binding: binding, 88 | bindingValue: bindingValue, 89 | role: role 90 | }; 91 | return cache[expression]; 92 | }); 93 | 94 | var INJECT_ATTR = 'styleName'; 95 | 96 | /* eslint max-depth: 0 guard-for-in: 0 */ 97 | 98 | function createElement(_) { 99 | var args = [].slice.call(arguments, 1); // for functional component 100 | 101 | if (isFunction(_)) { 102 | return createElement.bind(_, { 103 | functional: true, 104 | createElement: _, 105 | styles: args[0], 106 | context: args[1] 107 | }); 108 | } 109 | 110 | var _$functional = _.functional, 111 | functional = _$functional === void 0 ? false : _$functional, 112 | h = _.createElement, 113 | _$context = _.context, 114 | context = _$context === void 0 ? {} : _$context, 115 | _$styles = _.styles, 116 | styles = _$styles === void 0 ? context.$style || {} : _$styles; 117 | 118 | if (isString(styles)) { 119 | styles = (functional ? (context.injections || {})[styles] : context[styles]) || {}; 120 | } 121 | 122 | if (functional) { 123 | context = context.props || {}; 124 | } 125 | 126 | var data = args[1]; 127 | 128 | if (isObject(data)) { 129 | if (!data.staticClass) { 130 | data.staticClass = ''; 131 | } 132 | 133 | if (!data.attrs) { 134 | data.attrs = {}; 135 | } 136 | 137 | var modules = data[INJECT_ATTR] || data.attrs[INJECT_ATTR] || ''; 138 | 139 | if (modules.length) { 140 | var _modules = Array.isArray(modules) ? modules : [modules]; 141 | 142 | for (var i in _modules) { 143 | var module = _modules[i]; 144 | 145 | if (module && typeof module === 'string') { 146 | var classExpressions = module.split(/\s+/g); 147 | 148 | for (var _i in classExpressions) { 149 | var classExpression = classExpressions[_i]; 150 | 151 | var _parseClassExpression = parseClassExpression(classExpression), 152 | className = _parseClassExpression.className, 153 | binding = _parseClassExpression.binding, 154 | bindingValue = _parseClassExpression.bindingValue, 155 | role = _parseClassExpression.role; 156 | 157 | if (bindingValue) { 158 | className = context[binding]; 159 | binding = undefined; 160 | } 161 | 162 | if ((binding ? context[binding] : true) && styles[className]) { 163 | data.staticClass += " " + styles[className]; 164 | data.staticClass = data.staticClass.trim(); 165 | } 166 | 167 | if (role) { 168 | data.attrs["data-component-" + role] = ''; 169 | } 170 | } 171 | } 172 | } 173 | } // remove styleName attr 174 | 175 | 176 | delete data[INJECT_ATTR]; 177 | delete data.attrs[INJECT_ATTR]; 178 | } 179 | 180 | return h.apply(null, args); 181 | } 182 | 183 | /* eslint camelcase: 0 */ 184 | 185 | var CSSModules = function CSSModules(styles) { 186 | return { 187 | beforeCreate: function beforeCreate() { 188 | this.original$createElement = this.original$createElement || this.$createElement; 189 | this.original_c = this.original_c || this._c; 190 | this.$createElement = createElement.bind(this, { 191 | createElement: this.original$createElement, 192 | context: this, 193 | styles: styles 194 | }); 195 | this._c = createElement.bind(this, { 196 | createElement: this.original_c, 197 | context: this, 198 | styles: styles 199 | }); 200 | } 201 | }; 202 | }; 203 | 204 | CSSModules.install = function (Vue) { 205 | Vue.mixin(CSSModules()); 206 | }; 207 | 208 | module.exports = CSSModules; 209 | -------------------------------------------------------------------------------- /dist/vue-css-modules.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-css-modules v1.2.0 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 8 | typeof define === 'function' && define.amd ? define(factory) : 9 | (global.VueCSSModules = factory()); 10 | }(this, (function () { 'use strict'; 11 | 12 | function includes(arrayLike, element, fromIndex) { 13 | if (fromIndex === void 0) { 14 | fromIndex = 0; 15 | } 16 | 17 | for (var i = fromIndex, len = arrayLike.length; i < len; i++) { 18 | if (arrayLike[i] === element) { 19 | return true; 20 | } 21 | } 22 | 23 | return false; 24 | } 25 | function isObject(value) { 26 | return value !== null && typeof value === 'object'; 27 | } 28 | function isFunction(value) { 29 | return typeof value === 'function'; 30 | } 31 | function isString(value) { 32 | return typeof value === 'string'; 33 | } 34 | var camelCaseCache = Object.create(null); 35 | function camelCase(value) { 36 | if (camelCaseCache[value]) return camelCaseCache[value]; 37 | var result = ''; 38 | var shouldUpperCase = false; 39 | 40 | for (var i = 0, len = value.length; i < len; i++) { 41 | var char = value[i]; 42 | 43 | if (char === '-') { 44 | shouldUpperCase = true; 45 | } else { 46 | result += result && shouldUpperCase ? char.toUpperCase() : char; 47 | shouldUpperCase = false; 48 | } 49 | } 50 | 51 | camelCaseCache[value] = result; 52 | return result; 53 | } 54 | 55 | var cache = Object.create(null); 56 | var parseClassExpression = (function (expression) { 57 | if (cache[expression]) return cache[expression]; 58 | var className; 59 | var binding; 60 | var bindingValue; 61 | var role; 62 | 63 | if (includes(expression, '=', 1)) { 64 | // eg: disabled=isDisabled 65 | var _expression$split = expression.split('='); 66 | 67 | className = _expression$split[0]; 68 | binding = _expression$split[1]; 69 | } else { 70 | var modifier = expression[0]; 71 | 72 | if (modifier === '$') { 73 | // eg: $type 74 | binding = expression.substr(1); 75 | bindingValue = true; 76 | } else if (modifier === '@') { 77 | // eg: @button 78 | className = expression.substr(1); 79 | role = className; 80 | } else if (modifier === ':') { 81 | // eg: :disabled 82 | className = expression.substr(1); 83 | binding = camelCase(className); 84 | } else { 85 | className = expression; 86 | } 87 | } 88 | 89 | cache[expression] = { 90 | className: className, 91 | binding: binding, 92 | bindingValue: bindingValue, 93 | role: role 94 | }; 95 | return cache[expression]; 96 | }); 97 | 98 | var INJECT_ATTR = 'styleName'; 99 | 100 | /* eslint max-depth: 0 guard-for-in: 0 */ 101 | 102 | function createElement(_) { 103 | var args = [].slice.call(arguments, 1); // for functional component 104 | 105 | if (isFunction(_)) { 106 | return createElement.bind(_, { 107 | functional: true, 108 | createElement: _, 109 | styles: args[0], 110 | context: args[1] 111 | }); 112 | } 113 | 114 | var _$functional = _.functional, 115 | functional = _$functional === void 0 ? false : _$functional, 116 | h = _.createElement, 117 | _$context = _.context, 118 | context = _$context === void 0 ? {} : _$context, 119 | _$styles = _.styles, 120 | styles = _$styles === void 0 ? context.$style || {} : _$styles; 121 | 122 | if (isString(styles)) { 123 | styles = (functional ? (context.injections || {})[styles] : context[styles]) || {}; 124 | } 125 | 126 | if (functional) { 127 | context = context.props || {}; 128 | } 129 | 130 | var data = args[1]; 131 | 132 | if (isObject(data)) { 133 | if (!data.staticClass) { 134 | data.staticClass = ''; 135 | } 136 | 137 | if (!data.attrs) { 138 | data.attrs = {}; 139 | } 140 | 141 | var modules = data[INJECT_ATTR] || data.attrs[INJECT_ATTR] || ''; 142 | 143 | if (modules.length) { 144 | var _modules = Array.isArray(modules) ? modules : [modules]; 145 | 146 | for (var i in _modules) { 147 | var module = _modules[i]; 148 | 149 | if (module && typeof module === 'string') { 150 | var classExpressions = module.split(/\s+/g); 151 | 152 | for (var _i in classExpressions) { 153 | var classExpression = classExpressions[_i]; 154 | 155 | var _parseClassExpression = parseClassExpression(classExpression), 156 | className = _parseClassExpression.className, 157 | binding = _parseClassExpression.binding, 158 | bindingValue = _parseClassExpression.bindingValue, 159 | role = _parseClassExpression.role; 160 | 161 | if (bindingValue) { 162 | className = context[binding]; 163 | binding = undefined; 164 | } 165 | 166 | if ((binding ? context[binding] : true) && styles[className]) { 167 | data.staticClass += " " + styles[className]; 168 | data.staticClass = data.staticClass.trim(); 169 | } 170 | 171 | if (role) { 172 | data.attrs["data-component-" + role] = ''; 173 | } 174 | } 175 | } 176 | } 177 | } // remove styleName attr 178 | 179 | 180 | delete data[INJECT_ATTR]; 181 | delete data.attrs[INJECT_ATTR]; 182 | } 183 | 184 | return h.apply(null, args); 185 | } 186 | 187 | /* eslint camelcase: 0 */ 188 | 189 | var CSSModules = function CSSModules(styles) { 190 | return { 191 | beforeCreate: function beforeCreate() { 192 | this.original$createElement = this.original$createElement || this.$createElement; 193 | this.original_c = this.original_c || this._c; 194 | this.$createElement = createElement.bind(this, { 195 | createElement: this.original$createElement, 196 | context: this, 197 | styles: styles 198 | }); 199 | this._c = createElement.bind(this, { 200 | createElement: this.original_c, 201 | context: this, 202 | styles: styles 203 | }); 204 | } 205 | }; 206 | }; 207 | 208 | CSSModules.install = function (Vue) { 209 | Vue.mixin(CSSModules()); 210 | }; 211 | 212 | return CSSModules; 213 | 214 | }))); 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [🇨🇳中文](./README_zh-CN.md) 2 | 3 | # Vue CSS Modules 4 | 5 | [![Travis](https://img.shields.io/travis/fjc0k/vue-css-modules.svg)](https://travis-ci.org/fjc0k/vue-css-modules) 6 | [![minified size](https://img.shields.io/badge/minified%20size-2.14%20KB-blue.svg?MIN)](https://github.com/fjc0k/vue-css-modules/blob/master/dist/vue-css-modules.min.js) 7 | [![minzipped size](https://img.shields.io/badge/minzipped%20size-1.05%20KB-blue.svg?MZIP)](https://github.com/fjc0k/vue-css-modules/blob/master/dist/vue-css-modules.min.js) 8 | 9 | Seamless mapping of class names to CSS modules inside of Vue components. 10 | 11 | ```shell 12 | yarn add vue-css-modules 13 | ``` 14 | 15 | CDN: [jsDelivr](//www.jsdelivr.com/package/npm/vue-css-modules) | [UNPKG](//unpkg.com/vue-css-modules/) (Avaliable as `window.VueCSSModules`) 16 | 17 | 18 | 19 | ## CSS Modules: local scope & modular 20 | 21 | [`CSS Modules`](https://github.com/css-modules/css-modules) assigns a local class a global unique name, so a component styles will not affect other components. e.g. 22 | 23 | ```css 24 | /* button.css */ 25 | .button { 26 | font-size: 16px; 27 | } 28 | .mini { 29 | font-size: 12px; 30 | } 31 | ``` 32 | 33 | It's will transformed to something similar to: 34 | 35 | ```css 36 | /* button.css */ 37 | .button__button--d8fj3 { 38 | font-size: 16px; 39 | } 40 | .button__mini--f90jc { 41 | font-size: 12px; 42 | } 43 | ``` 44 | 45 | When importing the CSS Module from a JS Module, it exports an object with all mappings from local names to global names. Just like this: 46 | 47 | ```javascript 48 | import styles from './button.css' 49 | // styles = { 50 | // button: 'button__button--d8fj3', 51 | // mini: 'button__mini--f90jc' 52 | // } 53 | 54 | element.innerHTML = ' 122 | ``` 123 | 124 | This is the equivalent to: 125 | 126 | ```html 127 | 128 | ``` 129 | 130 | This allows you to override component styles in context: 131 | 132 | ```css 133 | .form [data-component-button] { 134 | font-size: 20px; 135 | } 136 | ``` 137 | 138 | ### $type 139 | 140 | ```html 141 | 142 | ``` 143 | 144 | This is the equivalent to: 145 | 146 | ```html 147 | 148 | ``` 149 | 150 | ### :mini 151 | 152 | ```html 153 | 154 | ``` 155 | 156 | This is the equivalent to: 157 | 158 | ```html 159 | 160 | ``` 161 | 162 | ### disabled=isDisabled 163 | 164 | ```html 165 | 166 | ``` 167 | 168 | This is the equivalent to: 169 | 170 | ```html 171 | 172 | ``` 173 | 174 | ## Usage 175 | 176 | ### In templates 177 | 178 | #### CSS Modules outside the template 179 | 180 | ```html 181 | 188 | 189 | 198 | ``` 199 | 200 | #### CSS Modules inside the template 201 | 202 | ```html 203 | 210 | 211 | 219 | 220 | 228 | ``` 229 | 230 | ### In JSX 231 | 232 | ```javascript 233 | import CSSModules from 'vue-css-modules' 234 | import styles from './button.css' 235 | 236 | export default { 237 | mixins: [CSSModules(styles)], 238 | props: { mini: Boolean }, 239 | render() { 240 | return ( 241 | 242 | ) 243 | } 244 | } 245 | ``` 246 | 247 | ### In render functions 248 | 249 | ```javascript 250 | import CSSModules from 'vue-css-modules' 251 | import styles from './button.css' 252 | 253 | export default { 254 | mixins: [CSSModules(styles)], 255 | props: { mini: Boolean }, 256 | render(h) { 257 | return h('button', { 258 | styleName: '@button :mini' 259 | }, 'Click me') 260 | } 261 | } 262 | ``` 263 | 264 | ## The implementation 265 | 266 | `vue-css-modules` extends `$createElement` method of the current component. It will use the value of `styleName` in `data` or `data.attrs` to look for CSS Modules in the associated styles object and will append the matching unique CSS class names to the `data.staticClass` value. 267 | --------------------------------------------------------------------------------