├── src ├── constructors │ ├── test │ │ ├── css.test.js │ │ ├── styled.test.js │ │ └── injectGlobal.test.js │ ├── css.js │ ├── injectGlobal.js │ ├── keyframes.js │ └── styled.js ├── utils │ ├── commonHtmlAttributes.js │ ├── isStyledComponent.js │ ├── interleave.js │ ├── isTag.js │ ├── normalizeProps.js │ ├── isVueComponent.js │ ├── isValidElementType.js │ ├── hyphenateStyleName.js │ ├── test │ │ ├── generateAlphabeticName.test.js │ │ ├── interleave.test.js │ │ └── flatten.test.js │ ├── generateAlphabeticName.js │ ├── flatten.js │ └── domElements.js ├── providers │ └── ThemeProvider.js ├── vendor │ ├── README.md │ └── glamor │ │ └── sheet.js ├── models │ ├── GlobalStyle.js │ ├── StyleSheet.js │ ├── ComponentStyle.js │ ├── test │ │ └── StyleSheet.test.js │ └── StyledComponent.js ├── index.js └── test │ ├── withComponent.test.js │ ├── extending-styles.test.js │ ├── keyframes.test.js │ ├── props.test.js │ ├── utils.js │ ├── as.test.js │ ├── css.test.js │ ├── component-features.test.js │ ├── basic.test.js │ ├── extending-components.test.js │ └── styles.test.js ├── .codesandbox └── ci.json ├── .eslintignore ├── .gitignore ├── mocha-bootstrap.js ├── .editorconfig ├── .npmignore ├── .babelrc.js ├── .eslintrc.js ├── .github └── workflows │ └── npm-publish.yml ├── LICENSE ├── example ├── devServer.js └── index.html ├── rollup.config.js ├── package.json ├── index.d.ts └── README.md /src/constructors/test/css.test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["c5778"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/test/utils.js 2 | *.test.js 3 | example/ 4 | lib/ 5 | src/vendor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules/ 4 | dist 5 | lib 6 | npm-debug.log 7 | npm-debug.log.* 8 | bundle-stats.html 9 | .idea 10 | -------------------------------------------------------------------------------- /mocha-bootstrap.js: -------------------------------------------------------------------------------- 1 | // jsdom setup must be done before importing Vue 2 | import jsdom from 'jsdom-global' 3 | jsdom() 4 | process.env.NODE_ENV = 'test' 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/utils/commonHtmlAttributes.js: -------------------------------------------------------------------------------- 1 | // List of common html attributes that should always be passed on to a styled html tag 2 | export const commonHtmlAttributes = ['value', 'name', 'type', 'id', 'href'] 3 | -------------------------------------------------------------------------------- /src/utils/isStyledComponent.js: -------------------------------------------------------------------------------- 1 | export default function isStyledComponent (target) { 2 | return target && 3 | target.methods && 4 | typeof target.methods.generateAndInjectStyles === 'function' 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/interleave.js: -------------------------------------------------------------------------------- 1 | export default ( 2 | strings, 3 | interpolations, 4 | ) => ( 5 | interpolations.reduce((array, interp, i) => ( 6 | array.concat(interp, strings[i + 1]) 7 | ), [strings[0]]) 8 | ) 9 | -------------------------------------------------------------------------------- /src/utils/isTag.js: -------------------------------------------------------------------------------- 1 | 2 | import domElements from './domElements' 3 | 4 | export default function isTag (target) { 5 | if (typeof target === 'string') { 6 | return domElements.indexOf(target) !== -1 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/constructors/css.js: -------------------------------------------------------------------------------- 1 | import interleave from '../utils/interleave' 2 | import flatten from '../utils/flatten' 3 | 4 | export default (rules, ...interpolations) => ( 5 | flatten(interleave(rules, interpolations)) 6 | ) 7 | -------------------------------------------------------------------------------- /src/utils/normalizeProps.js: -------------------------------------------------------------------------------- 1 | import zipObject from 'lodash.zipobject' 2 | 3 | export default function normalizeProps (props = {}) { 4 | if (Array.isArray(props)) { 5 | return zipObject(props) 6 | } else { 7 | return props 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .vscode 4 | npm-debug.log 5 | npm-debug.log.* 6 | test/unit/coverage 7 | test/e2e/reports 8 | selenium-debug.log 9 | rollup.config.js 10 | .eslintrc 11 | .eslintignore 12 | .babelrc 13 | example/ 14 | src/**/*test.js -------------------------------------------------------------------------------- /src/utils/isVueComponent.js: -------------------------------------------------------------------------------- 1 | export default function isVueComponent (target) { 2 | return target && 3 | ( 4 | typeof target.setup === 'function' || 5 | typeof target.render === 'function' || 6 | typeof target.template === 'string' 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@babel/preset-env" 4 | ], 5 | comments: false, 6 | plugins: [ 7 | "add-module-exports", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | "@babel/plugin-proposal-class-properties" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/providers/ThemeProvider.js: -------------------------------------------------------------------------------- 1 | import { h, provide } from 'vue' 2 | 3 | export default { 4 | props: { 5 | theme: Object 6 | }, 7 | setup (props, { slots }) { 8 | provide('theme', props.theme) 9 | }, 10 | 11 | render () { 12 | return h('div', {}, this.$slots.default()) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/constructors/injectGlobal.js: -------------------------------------------------------------------------------- 1 | import css from './css' 2 | import GlobalStyle from '../models/GlobalStyle' 3 | 4 | const injectGlobal = (strings, ...interpolations) => { 5 | const globalStyle = new GlobalStyle(css(strings, ...interpolations)) 6 | globalStyle.generateAndInject() 7 | } 8 | 9 | export default injectGlobal 10 | -------------------------------------------------------------------------------- /src/utils/isValidElementType.js: -------------------------------------------------------------------------------- 1 | import isTag from './isTag' 2 | import isVueComponent from './isVueComponent' 3 | import isStyledComponent from './isStyledComponent' 4 | 5 | export default function isValidElementType (target) { 6 | return isStyledComponent(target) || 7 | isVueComponent(target) || 8 | isTag(target) 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/hyphenateStyleName.js: -------------------------------------------------------------------------------- 1 | const _uppercasePattern = /([A-Z])/g 2 | const msPattern = /^ms-/ 3 | 4 | function hyphenate (string) { 5 | return string.replace(_uppercasePattern, '-$1').toLowerCase() 6 | } 7 | 8 | function hyphenateStyleName (string) { 9 | return hyphenate(string).replace(msPattern, '-ms-') 10 | } 11 | 12 | module.exports = hyphenateStyleName 13 | -------------------------------------------------------------------------------- /src/constructors/test/styled.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import styled from '../../index' 3 | import domElements from '../../utils/domElements' 4 | 5 | describe('styled', () => { 6 | it('should have all valid HTML5 elements defined as properties', () => { 7 | domElements.forEach(domElement => { 8 | expect(styled[domElement]).toBeDefined() 9 | }) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/utils/test/generateAlphabeticName.test.js: -------------------------------------------------------------------------------- 1 | import generateAlphabeticName from '../generateAlphabeticName'; 2 | import expect from 'expect'; 3 | 4 | describe('generateAlphabeticName', () => { 5 | it('should create alphabetic names for number input data', () => { 6 | expect(generateAlphabeticName(1000000000)).toEqual('cGNYzm'); 7 | expect(generateAlphabeticName(2000000000)).toEqual('fnBWYy'); 8 | }); 9 | }); -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | extends: 'vue', 8 | // required to lint *.vue files 9 | plugins: [ 10 | 'html' 11 | ], 12 | // add your custom rules here 13 | 'rules': { 14 | // allow debugger during development 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/generateAlphabeticName.js: -------------------------------------------------------------------------------- 1 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') 2 | 3 | /* Some high number, usually 9-digit base-10. Map it to base-😎 */ 4 | const generateAlphabeticName = (code) => { 5 | const lastDigit = chars[code % chars.length] 6 | return code > chars.length 7 | ? `${generateAlphabeticName(Math.floor(code / chars.length))}${lastDigit}` 8 | : lastDigit 9 | } 10 | 11 | export default generateAlphabeticName 12 | -------------------------------------------------------------------------------- /src/vendor/README.md: -------------------------------------------------------------------------------- 1 | Vendored glamor/sheet.js as of [582dde4](https://github.com/threepointone/glamor/blob/582dde44713bcbe9212a961706c06a34a4ebccb0/src/sheet.js) 2 | 3 | Then hacked things around: 4 | 5 | * Deleted `previous-map.js` and all references to it because it `require('fs')`ed 6 | * Made `StyleSheet.insert()` return something with an `update()` method 7 | * Replaced nested `require` statements with `import` declarations for the sake of a leaner bundle. This entails adding empty imports to three files to guarantee correct ordering – see https://github.com/styled-components/styled-components/pull/100 8 | -------------------------------------------------------------------------------- /src/models/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | 2 | import flatten from '../utils/flatten' 3 | import styleSheet from './StyleSheet' 4 | import stylis from 'stylis' 5 | 6 | export default class ComponentStyle { 7 | 8 | constructor (rules, selector) { 9 | this.rules = rules 10 | this.selector = selector 11 | } 12 | 13 | generateAndInject () { 14 | if (!styleSheet.injected) styleSheet.inject() 15 | const flatCSS = flatten(this.rules).join('') 16 | const cssString = this.selector ? `${this.selector} { ${flatCSS} }` : flatCSS 17 | const css = stylis('', cssString, false, false) 18 | styleSheet.insert(css, { global: true }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import generateAlphabeticName from './utils/generateAlphabeticName' 2 | import css from './constructors/css' 3 | import keyframes from './constructors/keyframes' 4 | import injectGlobal from './constructors/injectGlobal' 5 | import ThemeProvider from './providers/ThemeProvider' 6 | import _styledComponent from './models/StyledComponent' 7 | import _componentStyle from './models/ComponentStyle' 8 | import _styled from './constructors/styled' 9 | 10 | const styled = _styled( 11 | _styledComponent(_componentStyle(generateAlphabeticName)) 12 | ) 13 | 14 | export default styled 15 | 16 | export { css, injectGlobal, keyframes, ThemeProvider } 17 | -------------------------------------------------------------------------------- /src/test/withComponent.test.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import { assert } from 'chai' 3 | 4 | import { resetStyled } from './utils' 5 | 6 | let styled 7 | 8 | describe('extending styled', () => { 9 | beforeEach(() => { 10 | styled = resetStyled() 11 | }) 12 | 13 | it('should change the target element', () => { 14 | const OldTarget = styled.div`color: blue;` 15 | const NewTarget = OldTarget.withComponent('a') 16 | 17 | const o = createApp(OldTarget).mount('body') 18 | const n = createApp(NewTarget).mount('body') 19 | 20 | assert(o.$el instanceof HTMLDivElement); 21 | assert(n.$el instanceof HTMLAnchorElement); 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/constructors/keyframes.js: -------------------------------------------------------------------------------- 1 | import StyleSheet from '../models/StyleSheet' 2 | import hashStr from 'glamor/lib/hash' 3 | import generateAlphabeticName from '../utils/generateAlphabeticName' 4 | 5 | const replaceWhitespace = str => str.replace(/\s|\\n/g, '') 6 | 7 | const makeAnimation = (name, css) => ` 8 | @keyframes ${name} { 9 | ${css} 10 | } 11 | ` 12 | 13 | export default css => { 14 | const name = generateAlphabeticName( 15 | hashStr(replaceWhitespace(JSON.stringify(css))) 16 | ) 17 | 18 | const animation = makeAnimation(name, css) 19 | 20 | if (!StyleSheet.injected) StyleSheet.inject() 21 | StyleSheet.insert(animation) 22 | 23 | return name 24 | } 25 | -------------------------------------------------------------------------------- /src/constructors/styled.js: -------------------------------------------------------------------------------- 1 | import css from './css' 2 | import domElements from '../utils/domElements' 3 | import isValidElementType from '../utils/isValidElementType' 4 | 5 | export default (createStyledComponent) => { 6 | const styled = (tagName, props = {}) => { 7 | if (!isValidElementType(tagName)) { 8 | throw new Error(tagName + ' is not allowed for styled tag type.') 9 | } 10 | return (cssRules, ...interpolations) => ( 11 | createStyledComponent(tagName, css(cssRules, ...interpolations), props) 12 | ) 13 | } 14 | 15 | domElements.forEach((domElement) => { 16 | styled[domElement] = styled(domElement) 17 | }) 18 | 19 | return styled 20 | } 21 | -------------------------------------------------------------------------------- /src/test/extending-styles.test.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | 3 | import { resetStyled, expectCSSMatches } from './utils' 4 | 5 | let styled 6 | 7 | describe('extending styled', () => { 8 | beforeEach(() => { 9 | styled = resetStyled() 10 | }) 11 | 12 | it('should append extended styled to the original class', () => { 13 | const Base = styled.div` 14 | color: blue; 15 | ` 16 | const Extended = Base.extend` 17 | background: green; 18 | ` 19 | 20 | const b = createApp(Base).mount('body') 21 | const e = createApp(Extended).mount('body') 22 | 23 | expectCSSMatches('.a {color: blue;} .b {color: blue;background: green;}') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/test/keyframes.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import keyframes from '../constructors/keyframes' 3 | 4 | import { resetStyled, expectCSSMatches } from './utils' 5 | 6 | let styled 7 | 8 | describe('css features', () => { 9 | beforeEach(() => { 10 | styled = resetStyled() 11 | }) 12 | 13 | it('should add vendor prefixes in the right order', () => { 14 | const rotate = keyframes` 15 | from { 16 | transform: rotate(0deg); 17 | } 18 | 19 | to { 20 | transform: rotate(360deg); 21 | } 22 | ` 23 | 24 | expectCSSMatches( 25 | '@keyframes iVXCSc { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }', 26 | { rotate } 27 | ) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/utils/test/interleave.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import interleave from '../interleave' 3 | 4 | describe('interleave', () => { 5 | it('blindly interleave', () => { 6 | expect(interleave([], [])).toEqual([undefined]) 7 | expect(interleave(['foo'], [])).toEqual(['foo']) 8 | expect(interleave(['foo'], [1])).toEqual(['foo', 1, undefined]) 9 | expect(interleave(['foo', 'bar'], [1])).toEqual(['foo', 1, 'bar']) 10 | }) 11 | it('should be driven off the number of interpolations', () => { 12 | expect(interleave(['foo', 'bar'], [])).toEqual(['foo']) 13 | expect(interleave(['foo', 'bar', 'baz'], [1])).toEqual(['foo', 1, 'bar']) 14 | expect(interleave([], [1])).toEqual([undefined, 1, undefined]) 15 | expect(interleave(['foo'], [1, 2, 3])).toEqual(['foo', 1, undefined, 2, undefined, 3, undefined]) 16 | }) 17 | }) -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | - run: npm build 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: 12 30 | registry-url: https://registry.npmjs.org/ 31 | - run: npm ci 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lorenzo Girardi 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. -------------------------------------------------------------------------------- /src/test/props.test.js: -------------------------------------------------------------------------------- 1 | import { h, createApp } from 'vue' 2 | 3 | import { resetStyled, expectCSSMatches } from './utils' 4 | import ThemeProvider from "../providers/ThemeProvider" 5 | 6 | 7 | let styled 8 | 9 | describe('props', () => { 10 | beforeEach(() => { 11 | styled = resetStyled() 12 | }) 13 | 14 | it('should execute interpolations and fall back', () => { 15 | const compProps = { fg: String } 16 | const Comp = styled('div', compProps)` 17 | color: ${props => props.fg || 'black'}; 18 | ` 19 | const vm = createApp(Comp).mount('body') 20 | expectCSSMatches('.a {color: black;}') 21 | }) 22 | 23 | it('should add any injected theme to the component', () => { 24 | const theme = { 25 | blue: "blue", 26 | } 27 | 28 | const Comp = styled.div` 29 | color: ${props => props.theme.blue}; 30 | ` 31 | const Themed = { 32 | render: function() { 33 | return h( 34 | ThemeProvider, 35 | { 36 | theme, 37 | }, 38 | [ 39 | h(Comp) 40 | ] 41 | ) 42 | } 43 | } 44 | 45 | const vm = createApp(Themed).mount('body') 46 | expectCSSMatches('.a {color: blue;}') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/utils/flatten.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash.isplainobject' 2 | import hyphenateStyleName from './hyphenateStyleName' 3 | 4 | export const objToCss = (obj, prevKey) => { 5 | const css = Object.keys(obj).map(key => { 6 | if (isPlainObject(obj[key])) return objToCss(obj[key], key) 7 | return `${hyphenateStyleName(key)}: ${obj[key]};` 8 | }).join(' ') 9 | return prevKey ? `${prevKey} { 10 | ${css} 11 | }` : css 12 | } 13 | 14 | const flatten = (chunks, executionContext) => ( 15 | chunks.reduce((ruleSet, chunk) => { 16 | /* Remove falsey values */ 17 | if (chunk === undefined || chunk === null || chunk === false || chunk === '') return ruleSet 18 | /* Flatten ruleSet */ 19 | if (Array.isArray(chunk)) return [...ruleSet, ...flatten(chunk, executionContext)] 20 | /* Either execute or defer the function */ 21 | if (typeof chunk === 'function') { 22 | return executionContext 23 | ? ruleSet.concat(...flatten([chunk(executionContext)], executionContext)) 24 | : ruleSet.concat(chunk) 25 | } 26 | 27 | /* Handle objects */ 28 | // $FlowFixMe have to add %checks somehow to isPlainObject 29 | return ruleSet.concat(isPlainObject(chunk) ? objToCss(chunk) : chunk.toString()) 30 | }, []) 31 | ) 32 | 33 | export default flatten 34 | -------------------------------------------------------------------------------- /src/models/StyleSheet.js: -------------------------------------------------------------------------------- 1 | /* Wraps glamor's stylesheet and exports a singleton for styled components 2 | to use. */ 3 | import { StyleSheet as GlamorSheet } from '../vendor/glamor/sheet' 4 | 5 | class StyleSheet { 6 | constructor () { 7 | /* Don't specify a maxLength for the global sheet, since these rules 8 | * are defined at initialization and should remain static after that */ 9 | this.globalStyleSheet = new GlamorSheet({ speedy: false }) 10 | this.componentStyleSheet = new GlamorSheet({ speedy: false, maxLength: 40 }) 11 | } 12 | get injected () { 13 | return this.globalStyleSheet.injected && this.componentStyleSheet.injected 14 | } 15 | inject () { 16 | this.globalStyleSheet.inject() 17 | this.componentStyleSheet.inject() 18 | } 19 | flush () { 20 | if (this.globalStyleSheet.sheet) this.globalStyleSheet.flush() 21 | if (this.componentStyleSheet.sheet) this.componentStyleSheet.flush() 22 | } 23 | insert (rule, opts = { global: false }) { 24 | const sheet = opts.global ? this.globalStyleSheet : this.componentStyleSheet 25 | return sheet.insert(rule) 26 | } 27 | rules () { 28 | return this.globalStyleSheet.rules().concat(this.componentStyleSheet.rules()) 29 | } 30 | } 31 | 32 | /* Export stylesheet as a singleton class */ 33 | export default new StyleSheet() 34 | -------------------------------------------------------------------------------- /src/test/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This sets up our end-to-end test suite, which essentially makes sure 3 | * our public API works the way we promise/want 4 | */ 5 | import expect from 'expect' 6 | 7 | import _styled from '../constructors/styled' 8 | import mainStyleSheet from '../models/StyleSheet' 9 | import _styledComponent from '../models/StyledComponent' 10 | import _ComponentStyle from '../models/ComponentStyle' 11 | 12 | /* Ignore hashing, just return class names sequentially as .a .b .c etc */ 13 | let index = 0 14 | const classNames = () => String.fromCodePoint(97 + index++) 15 | 16 | export const resetStyled = () => { 17 | mainStyleSheet.flush() 18 | index = 0 19 | return _styled(_styledComponent(_ComponentStyle(classNames))) 20 | } 21 | 22 | const stripWhitespace = str => str.trim() 23 | .replace(/\s+/g, ' ') 24 | .replace(/\s+\{/g, '{') 25 | .replace(/\:\s+/g, ':') 26 | 27 | export const expectCSSMatches = ( 28 | expectation, 29 | opts = {} 30 | ) => { 31 | const { ignoreWhitespace = true, styleSheet = mainStyleSheet } = opts 32 | const css = styleSheet.rules().map(rule => rule.cssText).join('\n') 33 | 34 | if (ignoreWhitespace) { 35 | expect(stripWhitespace(css)).toEqual(stripWhitespace(expectation)) 36 | } else { 37 | expect(css).toEqual(expectation) 38 | } 39 | return css 40 | } 41 | -------------------------------------------------------------------------------- /src/models/ComponentStyle.js: -------------------------------------------------------------------------------- 1 | 2 | import hashStr from 'glamor/lib/hash' 3 | import flatten from '../utils/flatten' 4 | import styleSheet from './StyleSheet' 5 | import stylis from 'stylis' 6 | 7 | export default (nameGenerator) => { 8 | const inserted = {} 9 | 10 | class ComponentStyle { 11 | constructor (rules) { 12 | this.rules = rules 13 | stylis.set({ keyframe: false }) 14 | if (!styleSheet.injected) styleSheet.inject() 15 | this.insertedRule = styleSheet.insert('') 16 | } 17 | 18 | /* 19 | * Flattens a rule set into valid CSS 20 | * Hashes it, wraps the whole chunk in a ._hashName {} 21 | * Parses that with PostCSS then runs PostCSS-Nested on it 22 | * Returns the hash to be injected on render() 23 | * */ 24 | generateAndInjectStyles (executionContext) { 25 | const flatCSS = flatten(this.rules, executionContext).join('') 26 | .replace(/^\s*\/\/.*$/gm, '') // replace JS comments 27 | const hash = hashStr(flatCSS) 28 | if (!inserted[hash]) { 29 | const selector = nameGenerator(hash) 30 | inserted[hash] = selector 31 | const css = stylis(`.${selector}`, flatCSS) 32 | this.insertedRule.appendRule(css) 33 | } 34 | return inserted[hash] 35 | } 36 | } 37 | 38 | return ComponentStyle 39 | } 40 | -------------------------------------------------------------------------------- /example/devServer.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const exec = require('child_process').exec 3 | const Express = require('express') 4 | const watch = require('node-watch') 5 | 6 | const srcPath = __dirname.split('/example')[0] + '/src'; 7 | 8 | const hotBuild = () => exec('npm run build:dist', (err, stdout, stderr) => { 9 | if (err) throw err 10 | if (stdout) { 11 | console.log(`npm run build:dist --- ${stdout}`) 12 | } 13 | if (stderr) { 14 | console.log(`npm run build:dist --- ${stderr}`) 15 | } 16 | }) 17 | 18 | watch(srcPath, { recursive: true }, (evt, filename) => { 19 | console.log(`${evt} - ${filename} file has changed`) 20 | hotBuild() 21 | }) 22 | 23 | const app = new Express() 24 | const port = 3000 25 | 26 | app.use(Express.static('dist')) 27 | 28 | app.get('/with-perf.html', (req, res) => { 29 | res.sendFile(path.join(__dirname, 'with-perf.html')) 30 | }) 31 | 32 | app.get('/*', (req, res) => { 33 | res.sendFile(path.join(__dirname, 'index.html')) 34 | }) 35 | 36 | app.listen(port, error => { 37 | /* eslint-disable no-console */ 38 | if (error) { 39 | console.error(error) 40 | } else { 41 | console.info( 42 | '🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.', 43 | port, 44 | port 45 | ) 46 | } 47 | /* eslint-enable no-console */ 48 | }) 49 | -------------------------------------------------------------------------------- /src/test/as.test.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue' 2 | import expect from 'expect' 3 | import { resetStyled, expectCSSMatches } from './utils' 4 | 5 | let styled 6 | 7 | describe('"as" polymorphic prop', () => { 8 | beforeEach(() => { 9 | styled = resetStyled() 10 | }) 11 | 12 | it('should render "as" polymorphic prop element', () => { 13 | const Base = styled.div` 14 | color: blue; 15 | ` 16 | const b = createApp({ 17 | render: () => h(Base, { 18 | as: 'button' 19 | }) 20 | }).mount('body') 21 | expect(b.$el.tagName.toLowerCase()).toEqual('button') 22 | }) 23 | 24 | 25 | it('should append base class to new components composing lower level styled components', () => { 26 | const Base = styled.div` 27 | color: blue; 28 | ` 29 | const Composed = styled(Base, { 30 | bg: String, 31 | })` 32 | background: ${props => props.bg}; 33 | ` 34 | 35 | const b = createApp(Base).mount('body') 36 | const c = createApp({ 37 | render: () => h(Composed, { 38 | bg: 'yellow', 39 | as: 'dialog' 40 | }) 41 | }).mount('body') 42 | 43 | expect(c.$el.tagName.toLowerCase()).toEqual('dialog') 44 | expect(c.$el.classList.contains(b.$el.classList.toString())).toBeTruthy() 45 | expectCSSMatches('.a{color: blue;} .b{background:yellow;}') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/constructors/test/injectGlobal.test.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import expect from 'expect' 3 | 4 | import injectGlobal from '../injectGlobal' 5 | import styleSheet from '../../models/StyleSheet' 6 | import { expectCSSMatches, resetStyled } from '../../test/utils' 7 | 8 | let styled = resetStyled() 9 | const rule1 = 'width: 100%;' 10 | const rule2 = 'margin: 0;' 11 | const rule3 = 'color: blue;' 12 | 13 | describe('injectGlobal', () => { 14 | beforeEach(() => { 15 | resetStyled() 16 | }) 17 | 18 | it(`should inject rules into the head`, () => { 19 | injectGlobal` 20 | html { 21 | ${rule1} 22 | } 23 | ` 24 | expect(styleSheet.injected).toBe(true) 25 | }) 26 | 27 | it(`should non-destructively inject styles when called repeatedly`, () => { 28 | injectGlobal` 29 | html { 30 | ${rule1} 31 | } 32 | ` 33 | 34 | injectGlobal` 35 | a { 36 | ${rule2} 37 | } 38 | ` 39 | expectCSSMatches(` 40 | html {${rule1}} 41 | a {${rule2}} 42 | `, { styleSheet }) 43 | }) 44 | 45 | it(`should inject styles in a separate sheet from a component`, () => { 46 | const Comp = styled.div` 47 | ${rule3} 48 | ` 49 | const vm = createApp(Comp).mount('body'); 50 | 51 | injectGlobal` 52 | html { 53 | ${rule1} 54 | } 55 | ` 56 | // Test the component sheet 57 | expectCSSMatches(` 58 | .a {${rule3}} 59 | `, { styleSheet: styleSheet.componentStyleSheet }) 60 | // Test the global sheet 61 | expectCSSMatches(` 62 | html {${rule1}} 63 | `, { styleSheet: styleSheet.globalStyleSheet }) 64 | }) 65 | }); 66 | -------------------------------------------------------------------------------- /src/test/css.test.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue'; 2 | 3 | import { resetStyled, expectCSSMatches } from './utils' 4 | 5 | let styled 6 | const stripLineBreaks = (str) => str.split('\n').map(l => l.trim()).join('') 7 | 8 | describe('css features', () => { 9 | beforeEach(() => { 10 | styled = resetStyled() 11 | }) 12 | 13 | it('should add vendor prefixes in the right order', () => { 14 | const Comp = styled.div` 15 | transition: opacity 0.3s; 16 | ` 17 | const vm = createApp(Comp).mount('body') 18 | expectCSSMatches('.a {-webkit-transition: opacity 0.3s;transition: opacity 0.3s;}') 19 | }) 20 | 21 | it('should add vendor prefixes for display', () => { 22 | const Comp = styled.div` 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | ` 27 | const vm = createApp(Comp).mount('body') 28 | expectCSSMatches(stripLineBreaks(` 29 | .a { 30 | display: -webkit-box; 31 | display: -webkit-flex; 32 | display: -ms-flexbox; 33 | display: flex; 34 | -webkit-flex-direction: column; 35 | -ms-flex-direction: column; 36 | flex-direction: column; 37 | -webkit-align-items: center; 38 | -webkit-box-align: center; 39 | -ms-flex-align: center; 40 | align-items: center; 41 | } 42 | `)) 43 | }) 44 | 45 | it('should handle CSS calc()', () => { 46 | const Comp = styled.div` 47 | margin-bottom: calc(15px - 0.5rem) !important; 48 | ` 49 | const vm = createApp(Comp).mount('body') 50 | expectCSSMatches('.a {margin-bottom: calc(15px - 0.5rem) !important;}') 51 | }) 52 | 53 | it('should pass through custom properties', () => { 54 | const Comp = styled.div` 55 | --custom-prop: some-val; 56 | ` 57 | const vm = createApp(Comp).mount('body') 58 | expectCSSMatches('.a {--custom-prop: some-val;}') 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/utils/domElements.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handy list of valid HTML tags 3 | * 4 | */ 5 | 6 | export default [ 7 | 'a', 8 | 'abbr', 9 | 'address', 10 | 'area', 11 | 'article', 12 | 'aside', 13 | 'audio', 14 | 'b', 15 | 'base', 16 | 'bdi', 17 | 'bdo', 18 | 'big', 19 | 'blockquote', 20 | 'body', 21 | 'br', 22 | 'button', 23 | 'canvas', 24 | 'caption', 25 | 'cite', 26 | 'code', 27 | 'col', 28 | 'colgroup', 29 | 'data', 30 | 'datalist', 31 | 'dd', 32 | 'del', 33 | 'details', 34 | 'dfn', 35 | 'dialog', 36 | 'div', 37 | 'dl', 38 | 'dt', 39 | 'em', 40 | 'embed', 41 | 'fieldset', 42 | 'figcaption', 43 | 'figure', 44 | 'footer', 45 | 'form', 46 | 'h1', 47 | 'h2', 48 | 'h3', 49 | 'h4', 50 | 'h5', 51 | 'h6', 52 | 'head', 53 | 'header', 54 | 'hgroup', 55 | 'hr', 56 | 'html', 57 | 'i', 58 | 'iframe', 59 | 'img', 60 | 'input', 61 | 'ins', 62 | 'kbd', 63 | 'keygen', 64 | 'label', 65 | 'legend', 66 | 'li', 67 | 'link', 68 | 'main', 69 | 'map', 70 | 'mark', 71 | 'menu', 72 | 'menuitem', 73 | 'meta', 74 | 'meter', 75 | 'nav', 76 | 'noscript', 77 | 'object', 78 | 'ol', 79 | 'optgroup', 80 | 'option', 81 | 'output', 82 | 'p', 83 | 'param', 84 | 'picture', 85 | 'pre', 86 | 'progress', 87 | 'q', 88 | 'rp', 89 | 'rt', 90 | 'ruby', 91 | 's', 92 | 'samp', 93 | 'script', 94 | 'section', 95 | 'select', 96 | 'small', 97 | 'source', 98 | 'span', 99 | 'strong', 100 | 'style', 101 | 'sub', 102 | 'summary', 103 | 'sup', 104 | 'table', 105 | 'tbody', 106 | 'td', 107 | 'textarea', 108 | 'tfoot', 109 | 'th', 110 | 'thead', 111 | 'time', 112 | 'title', 113 | 'tr', 114 | 'track', 115 | 'u', 116 | 'ul', 117 | 'var', 118 | 'video', 119 | 'wbr', 120 | 121 | // SVG 122 | 'circle', 123 | 'clipPath', 124 | 'defs', 125 | 'ellipse', 126 | 'g', 127 | 'image', 128 | 'line', 129 | 'linearGradient', 130 | 'mask', 131 | 'path', 132 | 'pattern', 133 | 'polygon', 134 | 'polyline', 135 | 'radialGradient', 136 | 'rect', 137 | 'stop', 138 | 'svg', 139 | 'text', 140 | 'tspan' 141 | ] 142 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve' 2 | import replace from 'rollup-plugin-replace' 3 | import commonjs from 'rollup-plugin-commonjs' 4 | import inject from 'rollup-plugin-inject' 5 | import babel from 'rollup-plugin-babel' 6 | import json from 'rollup-plugin-json' 7 | import { terser } from 'rollup-plugin-terser' 8 | import builtins from 'rollup-plugin-node-builtins' 9 | import visualizer from 'rollup-plugin-visualizer' 10 | 11 | const processShim = '\0process-shim' 12 | 13 | const prod = process.env.PRODUCTION 14 | const mode = prod ? 'production' : 'development' 15 | 16 | console.log(`Creating ${mode} bundle...`) 17 | 18 | const moduleName = 'styled' 19 | const exports = 'named' 20 | 21 | const globals = { vue: 'Vue' } 22 | 23 | const prodOutput = [ 24 | { exports, file: 'dist/vue-styled-components.min.js', format: 'umd', name: moduleName } 25 | ] 26 | 27 | const devOutput = [ 28 | { exports, globals, file: 'dist/vue-styled-components.js', format: 'umd', name: moduleName }, 29 | { exports, globals, file: 'dist/vue-styled-components.es.js', format: 'es', name: moduleName } 30 | ] 31 | 32 | const external = ['vue'] 33 | 34 | const output = prod ? prodOutput : devOutput 35 | 36 | const plugins = [ 37 | commonjs(), 38 | babel({ 39 | babelrc: true 40 | }), 41 | // Unlike Webpack and Browserify, Rollup doesn't automatically shim Node 42 | // builtins like `process`. This ad-hoc plugin creates a 'virtual module' 43 | // which includes a shim containing just the parts the bundle needs. 44 | { 45 | resolveId (importee) { 46 | if (importee === processShim) return importee 47 | return null 48 | }, 49 | load (id) { 50 | if (id === processShim) return 'export default { argv: [], env: {} }' 51 | return null 52 | } 53 | }, 54 | builtins(), 55 | nodeResolve({ 56 | mainFields: ['module', 'main', 'jsnext', 'browser'] 57 | }), 58 | replace({ 59 | 'process.env.NODE_ENV': JSON.stringify(prod ? 'production' : 'development') 60 | }), 61 | inject({ 62 | process: processShim 63 | }), 64 | json() 65 | ] 66 | 67 | if (prod) plugins.push(terser(), visualizer({ filename: './bundle-stats.html' })) 68 | 69 | export default { 70 | input: 'src/index.js', 71 | output, 72 | plugins, 73 | external 74 | } 75 | -------------------------------------------------------------------------------- /src/test/component-features.test.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue'; 2 | import expect from 'expect' 3 | 4 | import styleSheet from '../models/StyleSheet' 5 | import { resetStyled } from './utils' 6 | 7 | let styled 8 | 9 | describe('component features', () => { 10 | /** 11 | * Make sure the setup is the same for every test 12 | */ 13 | beforeEach(() => { 14 | styled = resetStyled() 15 | }) 16 | 17 | it('default slot', () => { 18 | const Comp = { 19 | template: `
FallbackContent
` 20 | } 21 | const StyledComp = styled(Comp)` 22 | color: blue; 23 | ` 24 | const vm = createApp({ 25 | components: { StyledComp }, 26 | template: `ActualContent` 27 | }).mount('body') 28 | expect(vm.$el.innerHTML).toEqual('ActualContent') 29 | }) 30 | it('named slot', () => { 31 | const Comp = { 32 | template: `
FallbackContent
` 33 | } 34 | const StyledComp = styled(Comp)` 35 | color: blue; 36 | ` 37 | const vm = createApp({ 38 | components: { StyledComp }, 39 | template: ` 40 | 41 | 42 | ` 43 | }).mount('body') 44 | expect(vm.$el.innerHTML).toEqual('ActualContent') 45 | }) 46 | it('scoped slot', () => { 47 | const Comp = { 48 | template: `
FallbackContent
` 49 | } 50 | const StyledComp = styled(Comp)` 51 | color: blue; 52 | ` 53 | const vm = createApp({ 54 | components: { StyledComp }, 55 | template: ` 56 | 57 | 58 | ` 59 | }).mount('body') 60 | expect(vm.$el.innerHTML).toEqual('ActualContent') 61 | }) 62 | it('named scoped slot', () => { 63 | const Comp = { 64 | template: `
FallbackContent
` 65 | } 66 | const StyledComp = styled(Comp)` 67 | color: blue; 68 | ` 69 | const vm = createApp({ 70 | components: { StyledComp }, 71 | template: ` 72 | 73 | 74 | ` 75 | }).mount('body') 76 | expect(vm.$el.innerHTML).toEqual('ActualContent') 77 | }) 78 | 79 | }) 80 | -------------------------------------------------------------------------------- /src/models/test/StyleSheet.test.js: -------------------------------------------------------------------------------- 1 | import styleSheet from '../StyleSheet' 2 | import { resetStyled } from '../../test/utils' 3 | import expect from 'expect' 4 | 5 | describe('stylesheet', () => { 6 | beforeEach(() => { 7 | resetStyled() 8 | }) 9 | 10 | describe('inject', () => { 11 | beforeEach(() => { 12 | styleSheet.inject() 13 | }) 14 | it('should inject the global sheet', () => { 15 | expect(styleSheet.globalStyleSheet.injected).toBe(true) 16 | }) 17 | it('should inject the component sheet', () => { 18 | expect(styleSheet.componentStyleSheet.injected).toBe(true) 19 | }) 20 | it('should specify that the sheets have been injected', () => { 21 | expect(styleSheet.injected).toBe(true) 22 | }) 23 | }) 24 | 25 | describe('flush', () => { 26 | beforeEach(() => { 27 | styleSheet.flush() 28 | }) 29 | it('should flush the global sheet', () => { 30 | expect(styleSheet.globalStyleSheet.injected).toBe(false) 31 | }) 32 | it('should flush the component sheet', () => { 33 | expect(styleSheet.componentStyleSheet.injected).toBe(false) 34 | }) 35 | it('should specify that the sheets are no longer injected', () => { 36 | expect(styleSheet.injected).toBe(false) 37 | }) 38 | }) 39 | 40 | // it('should return both rules for both sheets', () => { 41 | // styleSheet.insert('a { color: green }', { global: true }) 42 | // styleSheet.insert('.hash1234 { color: blue }') 43 | 44 | // expect(styleSheet.rules()).toEqual([ 45 | // { cssText: 'a { color: green }' }, 46 | // { cssText: '.hash1234 { color: blue }' } 47 | // ]) 48 | // }) 49 | 50 | describe('insert with the global option', () => { 51 | beforeEach(() => { 52 | styleSheet.insert('a { color: green }', { global: true }) 53 | }) 54 | it('should insert into the global sheet', () => { 55 | expect(styleSheet.globalStyleSheet.rules()).toEqual([ 56 | { cssText: 'a { color: green }' }, 57 | ]) 58 | }) 59 | it('should not inject into the component sheet', () => { 60 | expect(styleSheet.componentStyleSheet.rules()).toEqual([]) 61 | }) 62 | }) 63 | 64 | describe('insert without the global option', () => { 65 | beforeEach(() => { 66 | styleSheet.insert('.hash1234 { color: blue }') 67 | }) 68 | it('should inject into the component sheet', () => { 69 | expect(styleSheet.componentStyleSheet.rules()).toEqual([ 70 | { cssText: '.hash1234 { color: blue }' }, 71 | ]) 72 | }) 73 | it('should not inject into the global sheet', () => { 74 | expect(styleSheet.globalStyleSheet.rules()).toEqual([]) 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/models/StyledComponent.js: -------------------------------------------------------------------------------- 1 | import { h, inject } from 'vue' 2 | import css from '../constructors/css' 3 | import isVueComponent from '../utils/isVueComponent' 4 | import normalizeProps from '../utils/normalizeProps' 5 | import { commonHtmlAttributes as attributesToAlwaysPassOn } from '../utils/commonHtmlAttributes' 6 | 7 | export default (ComponentStyle) => { 8 | const createStyledComponent = (tagOrComponent, rules, propDefinitions) => { 9 | const componentStyle = new ComponentStyle(rules) 10 | const targetPropDefinitions = normalizeProps(tagOrComponent.props) 11 | const ownPropDefinitions = normalizeProps(propDefinitions) 12 | 13 | const targetPropDefinitionKeys = tagOrComponent.props ? Object.keys( 14 | targetPropDefinitions 15 | ) : attributesToAlwaysPassOn 16 | 17 | const combinedPropDefinition = tagOrComponent.props 18 | ? { ...ownPropDefinitions, ...targetPropDefinitions } 19 | : ownPropDefinitions 20 | 21 | return { 22 | props: { 23 | as: [String, Object], 24 | modelValue: null, 25 | ...combinedPropDefinition 26 | }, 27 | 28 | emits: ['input', 'update:modelValue'], 29 | 30 | setup (props, { slots, attrs, emit }) { 31 | const theme = inject('theme') 32 | 33 | return () => { 34 | const styleClass = componentStyle.generateAndInjectStyles({ theme, ...props, ...attrs }) 35 | const classes = [styleClass] 36 | 37 | if (attrs.class) { 38 | classes.push(attrs.class) 39 | } 40 | 41 | const targetProps = {} 42 | 43 | if (targetPropDefinitionKeys.length) { 44 | for (const [key, value] of Object.entries(props)) { 45 | if (targetPropDefinitionKeys.includes(key)) { 46 | targetProps[key] = value 47 | } 48 | } 49 | } 50 | 51 | return h( 52 | isVueComponent(tagOrComponent) ? tagOrComponent : props.as || tagOrComponent, 53 | { 54 | value: props.modelValue, 55 | ...attrs, 56 | ...targetProps, 57 | class: classes, 58 | onInput: (e) => { 59 | emit('update:modelValue', e.target.value) 60 | emit('input', e) 61 | } 62 | }, 63 | slots 64 | ) 65 | } 66 | }, 67 | 68 | extend (cssRules, ...interpolations) { 69 | const extendedRules = css(cssRules, ...interpolations) 70 | return createStyledComponent(tagOrComponent, rules.concat(extendedRules), propDefinitions) 71 | }, 72 | withComponent (newTarget) { 73 | return createStyledComponent(newTarget, rules, propDefinitions) 74 | } 75 | } 76 | } 77 | 78 | return createStyledComponent 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/test/flatten.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import flatten from '../flatten' 3 | 4 | describe('flatten', () => { 5 | it('doesnt merge strings', () => { 6 | expect(flatten(['foo', 'bar', 'baz'])).toEqual(['foo', 'bar', 'baz']) 7 | }) 8 | it('drops nulls', () => { 9 | expect(flatten(['foo', false, 'bar', undefined, 'baz', null])).toEqual(['foo', 'bar', 'baz']) 10 | }) 11 | it('doesnt drop any numbers', () => { 12 | expect(flatten(['foo', 0, 'bar', NaN, 'baz', -1])).toEqual(['foo', '0', 'bar', 'NaN', 'baz', '-1']) 13 | }) 14 | it('toStrings everything', () => { 15 | expect(flatten([1, true])).toEqual(['1', 'true']) 16 | }) 17 | it('hypenates objects', () => { 18 | const obj = { 19 | fontSize: '14px', 20 | WebkitFilter: 'blur(2px)', 21 | } 22 | const css = 'font-size: 14px; -webkit-filter: blur(2px);' 23 | expect(flatten([obj])).toEqual([css]) 24 | expect(flatten(['some:thing;', obj, 'something: else;'])).toEqual(['some:thing;', css, 'something: else;']) 25 | }) 26 | it('handles nested objects', () => { 27 | const obj = { 28 | fontSize: '14px', 29 | '@media screen and (min-width: 250px)': { 30 | fontSize: '16px', 31 | }, 32 | '&:hover': { 33 | fontWeight: 'bold', 34 | }, 35 | } 36 | const css = 'font-size: 14px; @media screen and (min-width: 250px) {\n font-size: 16px;\n} &:hover {\n font-weight: bold;\n}' 37 | expect(flatten([obj])).toEqual([css]) 38 | expect(flatten(['some:thing;', obj, 'something: else;'])).toEqual(['some:thing;', css, 'something: else;']) 39 | }) 40 | it('toStrings class instances', () => { 41 | class SomeClass { 42 | toString() { 43 | return 'some: thing;' 44 | } 45 | } 46 | expect(flatten([new SomeClass()])).toEqual(['some: thing;']) 47 | }) 48 | it('flattens subarrays', () => { 49 | expect(flatten([1, 2, [3, 4, 5], 'come:on;', 'lets:ride;'])).toEqual(['1', '2', '3', '4', '5', 'come:on;', 'lets:ride;']) 50 | }) 51 | it('defers functions', () => { 52 | const func = () => 'bar' 53 | // $FlowFixMe 54 | const funcWFunc = () => ['static', subfunc => subfunc ? 'bar' : 'baz'] 55 | expect(flatten(['foo', func, 'baz'])).toEqual(['foo', func, 'baz']) 56 | expect(flatten(['foo', funcWFunc, 'baz'])).toEqual(['foo', funcWFunc, 'baz']) 57 | }) 58 | it('executes functions', () => { 59 | const func = () => 'bar' 60 | expect(flatten(['foo', func, 'baz'], { bool: true })).toEqual(['foo', 'bar', 'baz']) 61 | }) 62 | it('passes values to function', () => { 63 | const func = ({ bool }) => bool ? 'bar' : 'baz' 64 | expect(flatten(['foo', func], { bool: true })).toEqual(['foo', 'bar']) 65 | expect(flatten(['foo', func], { bool: false })).toEqual(['foo', 'baz']) 66 | }) 67 | it('recursively calls functions', () => { 68 | // $FlowFixMe 69 | const func = () => ['static', ({ bool }) => bool ? 'bar' : 'baz'] 70 | expect(flatten(['foo', func], { bool: true })).toEqual(['foo', 'static', 'bar']) 71 | expect(flatten(['foo', func], { bool: false })).toEqual(['foo', 'static', 'baz']) 72 | }) 73 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-styled-components", 3 | "version": "1.2.1", 4 | "description": "Visual primitives for the component age. A simple port of styled-components 💅 for Vue", 5 | "main": "lib/index.js", 6 | "module": "dist/vue-styled-components.es.js", 7 | "author": "Lorenzo Girardi", 8 | "license": "MIT", 9 | "bugs": { 10 | "url": "https://github.com/UX-and-I/vue3-styled-components/issues" 11 | }, 12 | "scripts": { 13 | "build": "npm run build:lib && npm run build:dist", 14 | "prebuild:lib": "rm -rf lib/*", 15 | "build:lib": "babel --out-dir lib src", 16 | "prebuild:umd": "rm -rf dist/*", 17 | "prebuild:dist": "rm -rf dist/*", 18 | "build:dist": "rollup -c && rollup -c --environment PRODUCTION", 19 | "build:watch": "npm run build:lib -- --watch", 20 | "test": "mocha \"./src/**/*.test.js\" --require @babel/register --require ./mocha-bootstrap --timeout 5000", 21 | "test:watch": "npm run test -- --watch", 22 | "lint": "eslint src", 23 | "prepublish": "npm run build", 24 | "lint-staged": "lint-staged", 25 | "dev": "node example/devServer.js" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/UX-and-I/vue3-styled-components.git" 30 | }, 31 | "keywords": [ 32 | "vue", 33 | "css", 34 | "css-in-js" 35 | ], 36 | "dependencies": { 37 | "glamor": "^2.20.40", 38 | "inline-style-prefixer": "^6.0.0", 39 | "lodash.isplainobject": "^4.0.6", 40 | "lodash.zipobject": "^4.1.3", 41 | "stylis": "^3.5.4" 42 | }, 43 | "devDependencies": { 44 | "@babel/cli": "^7.8.4", 45 | "@babel/core": "^7.9.0", 46 | "@babel/plugin-external-helpers": "^7.8.3", 47 | "@babel/plugin-proposal-class-properties": "^7.8.3", 48 | "@babel/plugin-proposal-object-rest-spread": "^7.9.5", 49 | "@babel/preset-env": "^7.9.5", 50 | "@babel/register": "^7.9.0", 51 | "babel-eslint": "^10.1.0", 52 | "babel-loader": "^8.1.0", 53 | "babel-plugin-add-module-exports": "^1.0.2", 54 | "chai": "^4.2.0", 55 | "chokidar": "^3.3.1", 56 | "danger": "^10.1.1", 57 | "eslint": "^6.8.0", 58 | "eslint-config-vue": "^2.0.2", 59 | "eslint-plugin-html": "^6.0.2", 60 | "eslint-plugin-vue": "^6.2.2", 61 | "expect": "^25.4.0", 62 | "express": "^4.17.1", 63 | "jsdom": "^16.2.2", 64 | "jsdom-global": "^3.0.2", 65 | "lint-staged": "^10.1.7", 66 | "lodash": "^4.17.15", 67 | "mocha": "^7.1.1", 68 | "node-watch": "^0.6.3", 69 | "pre-commit": "^1.2.2", 70 | "rollup": "^2.7.1", 71 | "rollup-plugin-babel": "^4.4.0", 72 | "rollup-plugin-commonjs": "^10.1.0", 73 | "rollup-plugin-inject": "^3.0.2", 74 | "rollup-plugin-json": "^4.0.0", 75 | "rollup-plugin-node-builtins": "^2.1.2", 76 | "rollup-plugin-node-resolve": "^5.2.0", 77 | "rollup-plugin-replace": "^2.2.0", 78 | "rollup-plugin-terser": "^5.3.0", 79 | "rollup-plugin-visualizer": "^4.0.4", 80 | "rollup-plugin-vue2": "^0.8.1", 81 | "vue": "^3.0.0" 82 | }, 83 | "lint-staged": { 84 | "*.js": [ 85 | "eslint --fix", 86 | "git add" 87 | ] 88 | }, 89 | "pre-commit": "lint-staged", 90 | "types": "index.d.ts" 91 | } 92 | -------------------------------------------------------------------------------- /src/test/basic.test.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import expect from 'expect' 3 | 4 | import styleSheet from '../models/StyleSheet' 5 | import { resetStyled, expectCSSMatches } from './utils' 6 | 7 | let styled 8 | 9 | describe('basic', () => { 10 | /** 11 | * Make sure the setup is the same for every test 12 | */ 13 | beforeEach(() => { 14 | styled = resetStyled() 15 | }) 16 | 17 | it('should not throw an error when called', () => { 18 | styled.div`` 19 | }) 20 | 21 | it('should inject a stylesheet when a component is created', () => { 22 | const Comp = styled.div`` 23 | const vm = createApp(Comp).mount('body') 24 | expect(styleSheet.injected).toBe(true) 25 | }) 26 | 27 | it('should not generate any styles by default', () => { 28 | styled.div`` 29 | expectCSSMatches('') 30 | }) 31 | 32 | it('should throw an error when called', () => { 33 | expect(() => styled``).toThrow() 34 | expect(() => styled.notExistTag``).toThrow() 35 | }) 36 | 37 | it('should allow for inheriting components that are not styled', () => { 38 | const componentConfig = { name: 'Parent', template: '
', methods: {} } 39 | expect(() => styled(componentConfig, {})``).not.toThrow() 40 | }) 41 | 42 | // it('should generate an empty tag once rendered', () => { 43 | // const Comp = styled.div`` 44 | // const vm = createApp(Comp).mount('body') 45 | // expectCSSMatches('.a { }') 46 | // }) 47 | 48 | // /* TODO: we should probably pretty-format the output so this test might have to change */ 49 | // it('should pass through all whitespace', () => { 50 | // const Comp = styled.div` \n ` 51 | // const vm = createApp(Comp).mount('body') 52 | // expectCSSMatches('.a { \n }', { ignoreWhitespace: false }) 53 | // }) 54 | 55 | // it('should inject only once for a styled component, no matter how often it\'s mounted', () => { 56 | // const Comp = styled.div`` 57 | // const vm = createApp(Comp).mount('body') 58 | // expectCSSMatches('.a { }') 59 | // }) 60 | 61 | // describe('innerRef', () => { 62 | // jsdom() 63 | 64 | // it('should handle styled-components correctly', () => { 65 | // const Comp = styled.div` 66 | // ${props => expect(props.innerRef).toExist()} 67 | // ` 68 | // const WrapperComp = Vue.extend({ 69 | // template: ' { this.testRef = comp }} />' 70 | // }) 71 | 72 | // const wrapper = createApp(WrapperComp).mount('body'); 73 | // expect(wrapper.$el.testRef).toExist() 74 | // expect(wrapper.$el.ref).toNotExist() 75 | // }) 76 | 77 | // it('should handle inherited components correctly', () => { 78 | // const StyledComp = styled.div`` 79 | 80 | // const WrappedStyledComp = Vue.extend({ 81 | // template: '' 82 | // }) 83 | 84 | // const ChildComp = styled(WrappedStyledComp)`` 85 | // const WrapperComp = Vue.extend({ 86 | // template: ' { this.testRef = comp }} />' 87 | // }) 88 | 89 | // const wrapper = createApp(WrapperComp).mount('body'); 90 | 91 | // expect(wrapper.node.testRef).toExist() 92 | // expect(wrapper.node.ref).toNotExist() 93 | // }) 94 | // }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/test/extending-components.test.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import expect from 'expect' 3 | 4 | import { resetStyled, expectCSSMatches } from './utils' 5 | 6 | let styled 7 | 8 | describe('extending components', () => { 9 | /** 10 | * Make sure the setup is the same for every test 11 | */ 12 | beforeEach(() => { 13 | styled = resetStyled() 14 | }) 15 | 16 | /* 17 | it('should generate a single class with no styles', () => { 18 | const Parent = styled.div`` 19 | const Child = styled(Parent)`` 20 | 21 | const p = createApp(Parent).mount('body') 22 | const c = createApp(Child).mount('body') 23 | 24 | expectCSSMatches('.a {}') 25 | }) 26 | */ 27 | 28 | it('should generate a single class if only parent has styles', () => { 29 | const Parent = styled.div`color: blue;` 30 | const Child = styled(Parent)`` 31 | 32 | const p = createApp(Parent).mount('body') 33 | const c = createApp(Child).mount('body') 34 | 35 | expectCSSMatches('.a {color: blue;}') 36 | }) 37 | 38 | it('should generate a single class if only child has styles', () => { 39 | const Parent = styled.div`color: blue;` 40 | const Child = styled(Parent)`` 41 | 42 | const p = createApp(Parent).mount('body') 43 | const c = createApp(Child).mount('body') 44 | 45 | expectCSSMatches('.a {color: blue;}') 46 | }) 47 | 48 | it('should generate a new class for the child with the added rules', () => { 49 | const Parent = styled.div`background-color: blue;` 50 | const Child = styled(Parent)`color: red;` 51 | 52 | const p = createApp(Parent).mount('body') 53 | const c = createApp(Child).mount('body') 54 | 55 | expectCSSMatches('.a {background-color: blue;} .b {color: red;}') 56 | }) 57 | 58 | it('should generate different classes for both parent and child', () => { 59 | const Parent = styled.div`color: blue;` 60 | const Child = styled(Parent)`color: red;` 61 | 62 | const p = createApp(Parent).mount('body') 63 | const c = createApp(Child).mount('body') 64 | 65 | expectCSSMatches('.a {color: blue;} .b {color: red;}') 66 | }) 67 | 68 | it('should keep nested rules to the child', () => { 69 | const Parent = styled.div` 70 | color: blue; 71 | > h1 { font-size: 4rem; } 72 | ` 73 | const Child = styled(Parent)`color: red;` 74 | 75 | const p = createApp(Parent).mount('body') 76 | const c = createApp(Child).mount('body') 77 | 78 | expectCSSMatches('.a {color: blue;}.a > h1 {font-size: 4rem;} .b {color: red;}') 79 | }) 80 | 81 | it('should keep default props from parent', () => { 82 | const parentProps = { 83 | color: { 84 | type: String, 85 | default: 'red' 86 | } 87 | } 88 | 89 | const Parent = styled('div', parentProps)` 90 | color: ${(props) => props.color}; 91 | ` 92 | 93 | const Child = styled(Parent)`background-color: green;` 94 | 95 | const p = createApp(Parent).mount('body') 96 | const c = createApp(Child).mount('body') 97 | 98 | expectCSSMatches(` 99 | .a {color: red;} 100 | .b {background-color: green;} 101 | `) 102 | }) 103 | 104 | it('should keep prop types from parent', () => { 105 | const parentProps = { 106 | color: { 107 | type: String 108 | } 109 | } 110 | 111 | const Parent = styled.div` 112 | color: ${(props) => props.color}; 113 | ` 114 | 115 | const Child = styled(Parent)`background-color: green;` 116 | 117 | const c = createApp(Child).mount('body') 118 | const p = createApp(Parent).mount('body') 119 | 120 | expect(c.$props).toEqual(p.$props) 121 | }) 122 | 123 | // it('should keep custom static member from parent', () => { 124 | // const Parent = styled.div`color: red;` 125 | 126 | // Parent.fetchData = () => 1 127 | 128 | // const Child = styled(Parent)`color: green;` 129 | 130 | // expect(Child.fetchData).toExist() 131 | // expect(Child.fetchData()).toEqual(1) 132 | // }) 133 | 134 | // it('should keep static member in triple inheritance', () => { 135 | // const GrandParent = styled.div`color: red;` 136 | // GrandParent.fetchData = () => 1 137 | 138 | // const Parent = styled(GrandParent)`color: red;` 139 | // const Child = styled(Parent)`color:red;` 140 | 141 | // expect(Child.fetchData).toExist() 142 | // expect(Child.fetchData()).toEqual(1) 143 | // }) 144 | }) 145 | -------------------------------------------------------------------------------- /src/test/styles.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import {createApp} from 'vue'; 3 | import { resetStyled, expectCSSMatches } from './utils' 4 | 5 | let styled 6 | 7 | describe('with styles', () => { 8 | /** 9 | * Make sure the setup is the same for every test 10 | */ 11 | beforeEach(() => { 12 | styled = resetStyled() 13 | }) 14 | 15 | it('should append a style', () => { 16 | const rule = 'color: blue;' 17 | const Comp = styled.div` 18 | ${rule} 19 | ` 20 | const vm = createApp(Comp).mount('body') 21 | expectCSSMatches('.a {color: blue;}') 22 | }) 23 | 24 | it('should append multiple styles', () => { 25 | const rule1 = 'color: blue;' 26 | const rule2 = 'background: red;' 27 | const Comp = styled.div` 28 | ${rule1} 29 | ${rule2} 30 | ` 31 | const vm = createApp(Comp).mount('body') 32 | expectCSSMatches('.a {color: blue;background: red;}') 33 | }) 34 | 35 | it('should handle inline style objects', () => { 36 | const rule1 = { 37 | backgroundColor: 'blue', 38 | } 39 | const Comp = styled.div` 40 | ${rule1} 41 | ` 42 | const vm = createApp(Comp).mount('body') 43 | expectCSSMatches('.a {background-color: blue;}') 44 | }) 45 | 46 | it('should handle inline style objects with media queries', () => { 47 | const rule1 = { 48 | backgroundColor: 'blue', 49 | '@media screen and (min-width: 250px)': { 50 | backgroundColor: 'red', 51 | }, 52 | } 53 | const Comp = styled.div` 54 | ${rule1} 55 | ` 56 | const vm = createApp(Comp).mount('body') 57 | expectCSSMatches('.a {background-color: blue;}@media screen and (min-width: 250px) {.a {background-color: red;}}') 58 | }) 59 | 60 | it('should handle inline style objects with pseudo selectors', () => { 61 | const rule1 = { 62 | backgroundColor: 'blue', 63 | '&:hover': { 64 | textDecoration: 'underline', 65 | }, 66 | } 67 | const Comp = styled.div` 68 | ${rule1} 69 | ` 70 | const vm = createApp(Comp).mount('body') 71 | expectCSSMatches('.a {background-color: blue;}.a:hover {-webkit-text-decoration: underline;text-decoration: underline;}') 72 | }) 73 | 74 | it('should handle inline style objects with pseudo selectors', () => { 75 | const rule1 = { 76 | backgroundColor: 'blue', 77 | '&:hover': { 78 | textDecoration: 'underline', 79 | }, 80 | } 81 | const Comp = styled.div` 82 | ${rule1} 83 | ` 84 | const vm = createApp(Comp).mount('body') 85 | expectCSSMatches('.a {background-color: blue;}.a:hover {-webkit-text-decoration: underline;text-decoration: underline;}') 86 | }) 87 | 88 | it('should handle inline style objects with nesting', () => { 89 | const rule1 = { 90 | backgroundColor: 'blue', 91 | '> h1': { 92 | color: 'white', 93 | }, 94 | } 95 | const Comp = styled.div` 96 | ${rule1} 97 | ` 98 | const vm = createApp(Comp).mount('body') 99 | expectCSSMatches('.a {background-color: blue;}.a > h1 {color: white;}') 100 | }) 101 | 102 | it('should handle inline style objects with contextual selectors', () => { 103 | const rule1 = { 104 | backgroundColor: 'blue', 105 | 'html.something &': { 106 | color: 'white', 107 | }, 108 | } 109 | const Comp = styled.div` 110 | ${rule1} 111 | ` 112 | const vm = createApp(Comp).mount('body') 113 | expectCSSMatches('.a {background-color: blue;}html.something .a {color: white;}') 114 | }) 115 | 116 | it('should inject styles of multiple components', () => { 117 | const firstRule = 'background: blue;' 118 | const secondRule = 'background: red;' 119 | const FirstComp = styled.div` 120 | ${firstRule} 121 | ` 122 | const SecondComp = styled.div` 123 | ${secondRule} 124 | ` 125 | 126 | const vm1 = createApp(FirstComp).mount('body') 127 | const vm2 = createApp(SecondComp).mount('body') 128 | 129 | expectCSSMatches('.a {background: blue;} .b {background: red;}') 130 | }) 131 | 132 | it('should inject styles of multiple components based on creation, not rendering order', () => { 133 | const firstRule = 'content: "first rule";' 134 | const secondRule = 'content: "second rule";' 135 | const FirstComp = styled.div` 136 | ${firstRule} 137 | ` 138 | const SecondComp = styled.div` 139 | ${secondRule} 140 | ` 141 | 142 | // Switch rendering order, shouldn't change injection order 143 | const vm2 = createApp(SecondComp).mount('body') 144 | const vm1 = createApp(FirstComp).mount('body') 145 | 146 | // Classes _do_ get generated in the order of rendering but that's ok 147 | expectCSSMatches(` 148 | .b {content: "first rule";} 149 | .a {content: "second rule";} 150 | `) 151 | }) 152 | 153 | it('should strip a JS-style (invalid) comment in the styles', () => { 154 | const comment = '// This is an invalid comment' 155 | const rule = 'color: blue;' 156 | const Comp = styled.div` 157 | ${comment} 158 | ${rule} 159 | ` 160 | const vm = createApp(Comp).mount('body') 161 | expectCSSMatches(` 162 | .a {color: blue;} 163 | `) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /src/vendor/glamor/sheet.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | high performance StyleSheet for css-in-js systems 4 | 5 | - uses multiple style tags behind the scenes for millions of rules 6 | - uses `insertRule` for appending in production for *much* faster performance 7 | - 'polyfills' on server side 8 | 9 | 10 | // usage 11 | 12 | import StyleSheet from 'glamor/lib/sheet' 13 | let styleSheet = new StyleSheet() 14 | 15 | styleSheet.inject() 16 | - 'injects' the stylesheet into the page (or into memory if on server) 17 | 18 | styleSheet.insert('#box { border: 1px solid red; }') 19 | - appends a css rule into the stylesheet 20 | 21 | styleSheet.flush() 22 | - empties the stylesheet of all its contents 23 | 24 | 25 | */ 26 | 27 | function last(arr) { 28 | return arr[arr.length -1] 29 | } 30 | 31 | function sheetForTag(tag) { 32 | for(let i = 0; i < document.styleSheets.length; i++) { 33 | if(document.styleSheets[i].ownerNode === tag) { 34 | return document.styleSheets[i] 35 | } 36 | } 37 | } 38 | 39 | const isDev = (x => (x === 'development') || !x)(process.env.NODE_ENV) 40 | const isTest = process.env.NODE_ENV === 'test' 41 | const isBrowser = typeof document !== 'undefined' && !isTest 42 | 43 | const oldIE = (() => { 44 | if(isBrowser) { 45 | let div = document.createElement('div') 46 | div.innerHTML = '' 47 | return div.getElementsByTagName('i').length === 1 48 | } 49 | })() 50 | 51 | function makeStyleTag() { 52 | let tag = document.createElement('style') 53 | tag.type = 'text/css' 54 | tag.appendChild(document.createTextNode('')); 55 | (document.head || document.getElementsByTagName('head')[0]).appendChild(tag) 56 | return tag 57 | } 58 | 59 | 60 | export class StyleSheet { 61 | constructor({ 62 | speedy = !isDev && !isTest, 63 | maxLength = (isBrowser && oldIE) ? 4000 : 65000 64 | } = {}) { 65 | this.isSpeedy = speedy // the big drawback here is that the css won't be editable in devtools 66 | this.sheet = undefined 67 | this.tags = [] 68 | this.maxLength = maxLength 69 | this.ctr = 0 70 | } 71 | inject() { 72 | if(this.injected) { 73 | throw new Error('already injected stylesheet!') 74 | } 75 | if(isBrowser) { 76 | // this section is just weird alchemy I found online off many sources 77 | this.tags[0] = makeStyleTag() 78 | // this weirdness brought to you by firefox 79 | this.sheet = sheetForTag(this.tags[0]) 80 | } 81 | else { 82 | // server side 'polyfill'. just enough behavior to be useful. 83 | this.sheet = { 84 | cssRules: [], 85 | insertRule: rule => { 86 | // enough 'spec compliance' to be able to extract the rules later 87 | // in other words, just the cssText field 88 | const serverRule = { cssText: rule } 89 | this.sheet.cssRules.push(serverRule) 90 | return {serverRule, appendRule: (newCss => serverRule.cssText += newCss)} 91 | } 92 | } 93 | } 94 | this.injected = true 95 | } 96 | speedy(bool) { 97 | if(this.ctr !== 0) { 98 | throw new Error(`cannot change speedy mode after inserting any rule to sheet. Either call speedy(${bool}) earlier in your app, or call flush() before speedy(${bool})`) 99 | } 100 | this.isSpeedy = !!bool 101 | } 102 | _insert(rule) { 103 | // this weirdness for perf, and chrome's weird bug 104 | // https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule 105 | try { 106 | this.sheet.insertRule(rule, this.sheet.cssRules.length) // todo - correct index here 107 | } 108 | catch(e) { 109 | if(isDev) { 110 | // might need beter dx for this 111 | console.warn('whoops, illegal rule inserted', rule) //eslint-disable-line no-console 112 | } 113 | } 114 | 115 | } 116 | insert(rule) { 117 | let insertedRule 118 | 119 | if(isBrowser) { 120 | // this is the ultrafast version, works across browsers 121 | if(this.isSpeedy && this.sheet.insertRule) { 122 | this._insert(rule) 123 | } 124 | else{ 125 | const textNode = document.createTextNode(rule) 126 | last(this.tags).appendChild(textNode) 127 | insertedRule = { textNode, appendRule: newCss => textNode.appendData(newCss)} 128 | 129 | if(!this.isSpeedy) { 130 | // sighhh 131 | this.sheet = sheetForTag(last(this.tags)) 132 | } 133 | } 134 | } 135 | else{ 136 | // server side is pretty simple 137 | insertedRule = this.sheet.insertRule(rule) 138 | } 139 | 140 | this.ctr++ 141 | if(isBrowser && this.ctr % this.maxLength === 0) { 142 | this.tags.push(makeStyleTag()) 143 | this.sheet = sheetForTag(last(this.tags)) 144 | } 145 | return insertedRule 146 | } 147 | flush() { 148 | if(isBrowser) { 149 | this.tags.forEach(tag => tag.parentNode.removeChild(tag)) 150 | this.tags = [] 151 | this.sheet = null 152 | this.ctr = 0 153 | // todo - look for remnants in document.styleSheets 154 | } 155 | else { 156 | // simpler on server 157 | this.sheet.cssRules = [] 158 | } 159 | this.injected = false 160 | } 161 | rules() { 162 | if(!isBrowser) { 163 | return this.sheet.cssRules 164 | } 165 | let arr = [] 166 | this.tags.forEach(tag => arr.splice(arr.length, 0, ...Array.from( 167 | sheetForTag(tag).cssRules 168 | ))) 169 | return arr 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Basic Example 6 | 7 | 8 |

Basic Example

9 |
10 | 11 | 12 | 13 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as CSS from 'csstype' 2 | import * as Vue from 'vue' 3 | 4 | type CSSProperties = CSS.Properties 5 | 6 | type CSSPseudos = { [K in CSS.Pseudos]?: CSSObject } 7 | 8 | interface CSSObject extends CSSProperties, CSSPseudos { 9 | [key: string]: CSSObject | string | number | undefined 10 | } 11 | 12 | type CSS = CSSProperties 13 | 14 | type Component = Vue.Component | Vue.DefineComponent 15 | 16 | type ThemeObject = { theme: { type: any; required: true } } 17 | 18 | type StyledHTMLElement = { 19 | [K in keyof IntrinsicElementAttributes]: ( 20 | str: TemplateStringsArray, 21 | ...placeholders: (( 22 | props: Vue.ExtractPropTypes 23 | ) => string | { toString: () => string })[] 24 | ) => Vue.DefineComponent 25 | } 26 | 27 | type StyledFunctionHTMLElement = { 28 | < 29 | Props = { [key: string]: Vue.Prop }, 30 | K extends keyof IntrinsicElementAttributes = any 31 | >( 32 | component: K, 33 | props?: Props 34 | ): ( 35 | str: TemplateStringsArray, 36 | ...placeholders: (( 37 | props: Vue.ExtractPropTypes 38 | ) => string | { toString: () => string })[] 39 | ) => Vue.DefineComponent< 40 | Vue.ExtractPropTypes & IntrinsicElementAttributes[K] 41 | > 42 | } 43 | 44 | type StyledVueComponent = { 45 | (component: T): ( 46 | str: TemplateStringsArray, 47 | ...placeholders: (( 48 | props: Vue.ExtractPropTypes 49 | ) => string | { toString: () => string })[] 50 | ) => Vue.DefineComponent & T 51 | } 52 | 53 | export type Styled = StyledHTMLElement & 54 | StyledFunctionHTMLElement & 55 | StyledVueComponent 56 | 57 | interface IntrinsicElementAttributes { 58 | a: Vue.AnchorHTMLAttributes 59 | abbr: Vue.HTMLAttributes 60 | address: Vue.HTMLAttributes 61 | area: Vue.AreaHTMLAttributes 62 | article: Vue.HTMLAttributes 63 | aside: Vue.HTMLAttributes 64 | audio: Vue.AudioHTMLAttributes 65 | b: Vue.HTMLAttributes 66 | base: Vue.BaseHTMLAttributes 67 | bdi: Vue.HTMLAttributes 68 | bdo: Vue.HTMLAttributes 69 | blockquote: Vue.BlockquoteHTMLAttributes 70 | body: Vue.HTMLAttributes 71 | br: Vue.HTMLAttributes 72 | button: Vue.ButtonHTMLAttributes 73 | canvas: Vue.CanvasHTMLAttributes 74 | caption: Vue.HTMLAttributes 75 | cite: Vue.HTMLAttributes 76 | code: Vue.HTMLAttributes 77 | col: Vue.ColHTMLAttributes 78 | colgroup: Vue.ColgroupHTMLAttributes 79 | data: Vue.DataHTMLAttributes 80 | datalist: Vue.HTMLAttributes 81 | dd: Vue.HTMLAttributes 82 | del: Vue.DelHTMLAttributes 83 | details: Vue.DetailsHTMLAttributes 84 | dfn: Vue.HTMLAttributes 85 | dialog: Vue.DialogHTMLAttributes 86 | div: Vue.HTMLAttributes 87 | dl: Vue.HTMLAttributes 88 | dt: Vue.HTMLAttributes 89 | em: Vue.HTMLAttributes 90 | embed: Vue.EmbedHTMLAttributes 91 | fieldset: Vue.FieldsetHTMLAttributes 92 | figcaption: Vue.HTMLAttributes 93 | figure: Vue.HTMLAttributes 94 | footer: Vue.HTMLAttributes 95 | form: Vue.FormHTMLAttributes 96 | h1: Vue.HTMLAttributes 97 | h2: Vue.HTMLAttributes 98 | h3: Vue.HTMLAttributes 99 | h4: Vue.HTMLAttributes 100 | h5: Vue.HTMLAttributes 101 | h6: Vue.HTMLAttributes 102 | head: Vue.HTMLAttributes 103 | header: Vue.HTMLAttributes 104 | hgroup: Vue.HTMLAttributes 105 | hr: Vue.HTMLAttributes 106 | html: Vue.HtmlHTMLAttributes 107 | i: Vue.HTMLAttributes 108 | iframe: Vue.IframeHTMLAttributes 109 | img: Vue.ImgHTMLAttributes 110 | input: Vue.InputHTMLAttributes 111 | ins: Vue.InsHTMLAttributes 112 | kbd: Vue.HTMLAttributes 113 | keygen: Vue.KeygenHTMLAttributes 114 | label: Vue.LabelHTMLAttributes 115 | legend: Vue.HTMLAttributes 116 | li: Vue.LiHTMLAttributes 117 | link: Vue.LinkHTMLAttributes 118 | main: Vue.HTMLAttributes 119 | map: Vue.MapHTMLAttributes 120 | mark: Vue.HTMLAttributes 121 | menu: Vue.MenuHTMLAttributes 122 | meta: Vue.MetaHTMLAttributes 123 | meter: Vue.MeterHTMLAttributes 124 | nav: Vue.HTMLAttributes 125 | noindex: Vue.HTMLAttributes 126 | noscript: Vue.HTMLAttributes 127 | object: Vue.ObjectHTMLAttributes 128 | ol: Vue.OlHTMLAttributes 129 | optgroup: Vue.OptgroupHTMLAttributes 130 | option: Vue.OptionHTMLAttributes 131 | output: Vue.OutputHTMLAttributes 132 | p: Vue.HTMLAttributes 133 | param: Vue.ParamHTMLAttributes 134 | picture: Vue.HTMLAttributes 135 | pre: Vue.HTMLAttributes 136 | progress: Vue.ProgressHTMLAttributes 137 | q: Vue.QuoteHTMLAttributes 138 | rp: Vue.HTMLAttributes 139 | rt: Vue.HTMLAttributes 140 | ruby: Vue.HTMLAttributes 141 | s: Vue.HTMLAttributes 142 | samp: Vue.HTMLAttributes 143 | script: Vue.ScriptHTMLAttributes 144 | section: Vue.HTMLAttributes 145 | select: Vue.SelectHTMLAttributes 146 | small: Vue.HTMLAttributes 147 | source: Vue.SourceHTMLAttributes 148 | span: Vue.HTMLAttributes 149 | strong: Vue.HTMLAttributes 150 | style: Vue.StyleHTMLAttributes 151 | sub: Vue.HTMLAttributes 152 | summary: Vue.HTMLAttributes 153 | sup: Vue.HTMLAttributes 154 | table: Vue.TableHTMLAttributes 155 | template: Vue.HTMLAttributes 156 | tbody: Vue.HTMLAttributes 157 | td: Vue.TdHTMLAttributes 158 | textarea: Vue.TextareaHTMLAttributes 159 | tfoot: Vue.HTMLAttributes 160 | th: Vue.ThHTMLAttributes 161 | thead: Vue.HTMLAttributes 162 | time: Vue.TimeHTMLAttributes 163 | title: Vue.HTMLAttributes 164 | tr: Vue.HTMLAttributes 165 | track: Vue.TrackHTMLAttributes 166 | u: Vue.HTMLAttributes 167 | ul: Vue.HTMLAttributes 168 | var: Vue.HTMLAttributes 169 | video: Vue.VideoHTMLAttributes 170 | wbr: Vue.HTMLAttributes 171 | webview: Vue.WebViewHTMLAttributes 172 | 173 | // SVG 174 | svg: Vue.SVGAttributes 175 | 176 | animate: Vue.SVGAttributes 177 | animateMotion: Vue.SVGAttributes 178 | animateTransform: Vue.SVGAttributes 179 | circle: Vue.SVGAttributes 180 | clipPath: Vue.SVGAttributes 181 | defs: Vue.SVGAttributes 182 | desc: Vue.SVGAttributes 183 | ellipse: Vue.SVGAttributes 184 | feBlend: Vue.SVGAttributes 185 | feColorMatrix: Vue.SVGAttributes 186 | feComponentTransfer: Vue.SVGAttributes 187 | feComposite: Vue.SVGAttributes 188 | feConvolveMatrix: Vue.SVGAttributes 189 | feDiffuseLighting: Vue.SVGAttributes 190 | feDisplacementMap: Vue.SVGAttributes 191 | feDistantLight: Vue.SVGAttributes 192 | feDropShadow: Vue.SVGAttributes 193 | feFlood: Vue.SVGAttributes 194 | feFuncA: Vue.SVGAttributes 195 | feFuncB: Vue.SVGAttributes 196 | feFuncG: Vue.SVGAttributes 197 | feFuncR: Vue.SVGAttributes 198 | feGaussianBlur: Vue.SVGAttributes 199 | feImage: Vue.SVGAttributes 200 | feMerge: Vue.SVGAttributes 201 | feMergeNode: Vue.SVGAttributes 202 | feMorphology: Vue.SVGAttributes 203 | feOffset: Vue.SVGAttributes 204 | fePointLight: Vue.SVGAttributes 205 | feSpecularLighting: Vue.SVGAttributes 206 | feSpotLight: Vue.SVGAttributes 207 | feTile: Vue.SVGAttributes 208 | feTurbulence: Vue.SVGAttributes 209 | filter: Vue.SVGAttributes 210 | foreignObject: Vue.SVGAttributes 211 | g: Vue.SVGAttributes 212 | image: Vue.SVGAttributes 213 | line: Vue.SVGAttributes 214 | linearGradient: Vue.SVGAttributes 215 | marker: Vue.SVGAttributes 216 | mask: Vue.SVGAttributes 217 | metadata: Vue.SVGAttributes 218 | mpath: Vue.SVGAttributes 219 | path: Vue.SVGAttributes 220 | pattern: Vue.SVGAttributes 221 | polygon: Vue.SVGAttributes 222 | polyline: Vue.SVGAttributes 223 | radialGradient: Vue.SVGAttributes 224 | rect: Vue.SVGAttributes 225 | stop: Vue.SVGAttributes 226 | switch: Vue.SVGAttributes 227 | symbol: Vue.SVGAttributes 228 | text: Vue.SVGAttributes 229 | textPath: Vue.SVGAttributes 230 | tspan: Vue.SVGAttributes 231 | use: Vue.SVGAttributes 232 | view: Vue.SVGAttributes 233 | } 234 | 235 | export const ThemeProvider: Vue.DefineComponent 236 | 237 | export const css: (input: TemplateStringsArray) => string 238 | 239 | export const styled: Styled 240 | export default styled 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This is a fork of [vue-styled-components](https://github.com/styled-components/vue-styled-components) 2 | 3 | It has been refactored to work with Vuejs 3. Changes are too large to merge them back into the upstream and this version is not backwards compatible. 4 | 5 | Also, we'll have to see how the update path will work for Vuejs 2.x dependencies. This is a very quick, as-is-port of vue-styled-components, meaning it will only provide the functionality we need, until vue-styled-components hopefully has found its own path to Vuejs 3. 6 | 7 | Please note that the package name has been changed to `vue3-styled-components` to avoid any name collisions. 8 | 9 | --- 10 | 11 | # vue-styled-components 12 | 13 | > Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅 14 | 15 | ## Support 16 | 17 | > This version is compatible with Vue 2.x 18 | 19 | ``` 20 | yarn add vue-styled-components 21 | ``` 22 | 23 | Utilising tagged template literals (a recent addition to JavaScript) and the power of CSS allows you to write actual CSS code to style your components. It also removes the mapping between components and styles – using components as a low-level styling construct could not be easier! 24 | 25 | *This is a (not fully-featured)fork from original styled-components made by [Glen Maddern](https://twitter.com/glenmaddern) and [Max Stoiber](https://twitter.com/mxstbr), supported by [Front End Center](https://frontend.center) and [Thinkmill](http://thinkmill.com.au/). Thank you for making this project possible!* 26 | 27 | ## Usage 28 | 29 | > Register first your component locally (see https://vuejs.org/v2/guide/components.html#Local-Registration) 30 | 31 | ``` 32 | new Vue({ 33 | // ... 34 | components { 35 | 'styled-title': StyledTitle 36 | }, 37 | template: ' Hello! ' 38 | } 39 | ``` 40 | 41 | ### Basic 42 | 43 | > Do not use built-in or reserved HTML elements as component id (title, button, input...). 44 | 45 | 46 | This creates two Vue components, `` and ``: 47 | 48 | ```JS 49 | import styled from 'vue-styled-components'; 50 | 51 | // Create a Vue component that renders an

which is 52 | // centered, palevioletred and sized at 1.5em 53 | const StyledTitle = styled.h1` 54 | font-size: 1.5em; 55 | text-align: center; 56 | color: palevioletred; 57 | `; 58 | 59 | // Create a Vue component that renders a
with 60 | // some padding and a papayawhip background 61 | const Wrapper = styled.section` 62 | padding: 4em; 63 | background: papayawhip; 64 | `; 65 | ``` 66 | 67 | You render them like so: 68 | 69 | ```JSX 70 | // Use them like any other Vue component – except they're styled! 71 | 72 | Hello World, this is my first styled component! 73 | 74 | ``` 75 | 76 | ### Passed props 77 | 78 | Styled components pass on all their props. This is a styled ``: 79 | 80 | ```JS 81 | import styled from 'vue-styled-components'; 82 | 83 | // Create an component that'll render an tag with some styles 84 | const StyledInput = styled.input` 85 | font-size: 1.25em; 86 | padding: 0.5em; 87 | margin: 0.5em; 88 | color: palevioletred; 89 | background: papayawhip; 90 | border: none; 91 | border-radius: 3px; 92 | 93 | &:hover { 94 | box-shadow: inset 1px 1px 2px rgba(0,0,0,0.1); 95 | } 96 | `; 97 | ``` 98 | You can just pass a `placeholder` prop into the `styled-component`. It will pass it on to the DOM node like any other Vue component: 99 | 100 | ```JSX 101 | // Render a styled input with a placeholder of "@liqueflies" 102 | 103 | ``` 104 | ### Adapting based on props 105 | 106 | This is a button component that has a `primary` state. By setting `primary` to `true` when rendering it we adjust the background and text color. 107 | 108 | ### Important 109 | 110 | > A prop is a custom attribute for passing information from parent components. A child component needs to explicitly declare the props it expects to receive using the props option, you must define your prop before, and of course, get benefits of validation! (see https://vuejs.org/v2/guide/components.html#Passing-Data-with-Props) 111 | 112 | ``` 113 | { 114 | props: { 115 | propA: String, 116 | propB: [String, Number] 117 | } 118 | } 119 | ``` 120 | 121 | ```JSX 122 | import styled from 'vue-styled-components'; 123 | 124 | const btnProps = { primary: Boolean }; 125 | 126 | const StyledButton = styled('button', btnProps)` 127 | font-size: 1em; 128 | margin: 1em; 129 | padding: 0.25em 1em; 130 | border: 2px solid palevioletred; 131 | border-radius: 3px; 132 | background: ${props => props.primary ? 'palevioletred' : 'white'}; 133 | color: ${props => props.primary ? 'white' : 'palevioletred'}; 134 | `; 135 | 136 | export default StyledButton; 137 | ``` 138 | 139 | ```JSX 140 | Normal 141 | Primary 142 | ``` 143 | 144 | ### Overriding component styles 145 | 146 | Taking the `StyledButton` component from above and removing the primary rules, this is what we're left with – just a normal button: 147 | 148 | ```JSX 149 | import styled from 'vue-styled-components'; 150 | 151 | const StyledButton = styled.button` 152 | background: white; 153 | color: palevioletred; 154 | font-size: 1em; 155 | margin: 1em; 156 | padding: 0.25em 1em; 157 | border: 2px solid palevioletred; 158 | border-radius: 3px; 159 | `; 160 | 161 | export default StyledButton; 162 | ``` 163 | 164 | ### Theming 165 | 166 | `vue-styled-components` has full theming support by exporting a `` wrapper component. This component provides a theme to all `Vue` components underneath itself via the context API. In the render tree all `vue-styled-components` will have access to the provided theme, even when they are multiple levels deep. 167 | 168 | Remember to register `ThemeProvider` locally. 169 | 170 | ```JSX 171 | import {ThemeProvider} from 'vue-styled-components' 172 | 173 | new Vue({ 174 | // ... 175 | components: { 176 | 'theme-provider': ThemeProvider 177 | }, 178 | // ... 179 | }); 180 | ``` 181 | 182 | Add your `ThemeProvider` component: 183 | 184 | ```vue 185 | 188 | 189 | // ... 190 | 191 | 192 | ``` 193 | 194 | And into your `Wrapper` component: 195 | 196 | ```JSX 197 | const Wrapper = styled.default.section` 198 | padding: 4em; 199 | background: ${props => props.theme.primary}; 200 | `; 201 | ``` 202 | 203 | ### Style component constructors as `router-link` 204 | 205 | You can style also Vue component constructors as `router-link` from `vue-router` and other components 206 | 207 | ```JSX 208 | import styled from 'vue-styled-components'; 209 | 210 | // unfortunately you can't import directly router-link, you have to retrieve contstructor 211 | const RouterLink = Vue.component('router-link') 212 | 213 | const StyledLink = styled(RouterLink)` 214 | color: palevioletred; 215 | font-size: 1em; 216 | text-decoration: none; 217 | `; 218 | 219 | export default StyledLink; 220 | ``` 221 | 222 | ```JSX 223 | Custom Router Link 224 | ``` 225 | 226 | Let's say someplace else you want to use your button component, but just in this one case you want the color and border color to be `tomato` instead of `palevioletred`. Now you _could_ pass in an interpolated function and change them based on some props, but that's quite a lot of effort for overriding the styles once. 227 | 228 | To do this in an easier way you can call `StyledComponent.extend` as a function and pass in the extended style. It overrides duplicate styles from the initial component and keeps the others around: 229 | 230 | ```JSX 231 | // Tomatobutton.js 232 | 233 | import StyledButton from './StyledButton'; 234 | 235 | const TomatoButton = StyledButton.extend` 236 | color: tomato; 237 | border-color: tomato; 238 | `; 239 | 240 | export default TomatoButton; 241 | ``` 242 | 243 | ### Polymorphic `as` prop 244 | If you want to keep all the styling you've applied to a component but just switch out what's being ultimately rendered (be it a different HTML tag or a different custom component), you can use the "as" prop to do this at runtime. Another powerful feature of the `as` prop is that it preserves styles if the lowest-wrapped component is a `StyledComponent`. 245 | 246 | **Example** 247 | In `Component.js` 248 | ```js 249 | // Renders a div element by default. 250 | const Component = styled('div', {})`` 251 | ``` 252 | Using the `as` prop in another template/component would be as shown below. 253 | ```vue 254 | 260 | ``` 261 | This sort of thing is very useful in use cases like a navigation bar where some of the items should be links and some just buttons, but all be styled the same way. 262 | 263 | ### withComponent 264 | Let's say you have a `button` and an `a` tag. You want them to share the exact same style. This is achievable with `.withComponent`. 265 | ```JSX 266 | const Button = styled.button` 267 | background: green; 268 | color: white; 269 | ` 270 | const Link = Button.withComponent('a') 271 | ``` 272 | 273 | ### injectGlobal 274 | 275 | A helper method to write global CSS. Does not return a component, adds the styles to the stylesheet directly. 276 | 277 | **We do not encourage the use of this. Use once per app at most, contained in a single file.** This is an escape hatch. Only use it for the rare `@font-face` definition or `body` styling. 278 | 279 | ```JS 280 | // global-styles.js 281 | 282 | import { injectGlobal } from 'vue-styled-components'; 283 | 284 | injectGlobal` 285 | @font-face { 286 | font-family: 'Operator Mono'; 287 | src: url('../fonts/Operator-Mono.ttf'); 288 | } 289 | 290 | body { 291 | margin: 0; 292 | } 293 | `; 294 | ``` 295 | 296 | ## Syntax highlighting 297 | 298 | The one thing you lose when writing CSS in template literals is syntax highlighting. We're working hard on making proper syntax highlighting happening in all editors. We currently have support for Atom, Visual Studio Code, and soon Sublime Text. 299 | 300 | ### Atom 301 | 302 | [**@gandm**](https://github.com/gandm), the creator of `language-babel`, has added support for `styled-components` in Atom! 303 | 304 | To get proper syntax highlighting, all you have to do is install and use the `language-babel` package for your JavaScript files! 305 | 306 | ### Sublime Text 307 | 308 | There is an [open PR](https://github.com/babel/babel-sublime/pull/289) by [@garetmckinley](https://github.com/garetmckinley) to add support for `styled-components` to `babel-sublime`! (if you want the PR to land, feel free to 👍 the initial comment to let the maintainers know there's a need for this!) 309 | 310 | As soon as that PR is merged and a new version released, all you'll have to do is install and use `babel-sublime` to highlight your JavaScript files! 311 | 312 | ### Visual Studio Code 313 | 314 | The [vscode-styled-components](https://github.com/styled-components/vscode-styled-components) extension provides syntax highlighting inside your Javascript files. You can install it as usual from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components). 315 | 316 | ### VIM / NeoVim 317 | The [`vim-styled-components`](https://github.com/fleischie/vim-styled-components) plugin gives you syntax highlighting inside your Javascript files. Install it with your usual plugin manager like [Plug](https://github.com/junegunn/vim-plug), [Vundle](https://github.com/VundleVim/Vundle.vim), [Pathogen](https://github.com/tpope/vim-pathogen), etc. 318 | 319 | Also if you're looking for an awesome javascript syntax package you can never go wrong with [YAJS.vim](https://github.com/othree/yajs.vim). 320 | 321 | ### Other Editors 322 | 323 | We could use your help to get syntax highlighting support to other editors! If you want to start working on syntax highlighting for your editor, open an issue to let us know. 324 | 325 | ## License 326 | 327 | Licensed under the MIT License, Copyright © 2017 Lorenzo Girardi. 328 | 329 | See [LICENSE](./LICENSE) for more information. 330 | --------------------------------------------------------------------------------