├── 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 | ActualContent
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 | {{ p }}
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 | {{ p }}
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 |
255 |
256 |
257 | Button
258 |
259 |
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 |
--------------------------------------------------------------------------------