├── .babelrc.js
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── example
├── devServer.js
└── index.html
├── mocha-bootstrap.js
├── package-lock.json
├── package.json
├── rollup.config.js
└── src
├── constructors
├── css.js
├── injectGlobal.js
├── keyframes.js
├── styled.js
└── test
│ ├── css.test.js
│ ├── injectGlobal.test.js
│ └── styled.test.js
├── index.js
├── models
├── ComponentStyle.js
├── GlobalStyle.js
├── StyleSheet.js
├── StyledComponent.js
└── test
│ └── StyleSheet.test.js
├── providers
└── ThemeProvider.js
├── test
├── as.test.js
├── attrs.test.js
├── basic.test.js
├── component-features.test.js
├── css.test.js
├── extending-components.test.js
├── extending-styles.test.js
├── keyframes.test.js
├── props.test.js
├── styles.test.js
├── utils.js
└── withComponent.test.js
├── utils
├── domElements.js
├── flatten.js
├── generateAlphabeticName.js
├── hyphenateStyleName.js
├── interleave.js
├── isStyledComponent.js
├── isTag.js
├── isValidElementType.js
├── isVueComponent.js
├── normalizeProps.js
└── test
│ ├── flatten.test.js
│ ├── generateAlphabeticName.test.js
│ └── interleave.test.js
└── vendor
├── README.md
└── glamor
└── sheet.js
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/test/utils.js
2 | *.test.js
3 | example/
4 | lib/
5 | src/vendor
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-styled-components
2 |
3 | > Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅
4 |
5 | ## Support
6 |
7 | > This version is compatible with Vue 2.x
8 |
9 | ```
10 | yarn add vue-styled-components
11 | ```
12 |
13 | 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!
14 |
15 | *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!*
16 |
17 | ## Usage
18 |
19 | > Register first your component locally (see https://vuejs.org/v2/guide/components.html#Local-Registration)
20 |
21 | ```
22 | new Vue({
23 | // ...
24 | components {
25 | 'styled-title': StyledTitle
26 | },
27 | template: ' Hello! '
28 | }
29 | ```
30 |
31 | ### Basic
32 |
33 | > Do not use built-in or reserved HTML elements as component id (title, button, input...).
34 |
35 |
36 | This creates two Vue components, `` and ``:
37 |
38 | ```JS
39 | import styled from 'vue-styled-components';
40 |
41 | // Create a Vue component that renders an which is
42 | // centered, palevioletred and sized at 1.5em
43 | const StyledTitle = styled.h1`
44 | font-size: 1.5em;
45 | text-align: center;
46 | color: palevioletred;
47 | `;
48 |
49 | // Create a Vue component that renders a with
50 | // some padding and a papayawhip background
51 | const Wrapper = styled.section`
52 | padding: 4em;
53 | background: papayawhip;
54 | `;
55 | ```
56 |
57 | You render them like so:
58 |
59 | ```JSX
60 | // Use them like any other Vue component – except they're styled!
61 |
62 | Hello World, this is my first styled component!
63 |
64 | ```
65 |
66 | ### Passed props
67 |
68 | Styled components pass on all their props. This is a styled ``:
69 |
70 | ```JS
71 | import styled from 'vue-styled-components';
72 |
73 | // Create an component that'll render an tag with some styles
74 | const StyledInput = styled.input`
75 | font-size: 1.25em;
76 | padding: 0.5em;
77 | margin: 0.5em;
78 | color: palevioletred;
79 | background: papayawhip;
80 | border: none;
81 | border-radius: 3px;
82 |
83 | &:hover {
84 | box-shadow: inset 1px 1px 2px rgba(0,0,0,0.1);
85 | }
86 | `;
87 | ```
88 | 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:
89 |
90 | ```JSX
91 | // Render a styled input with a placeholder of "@liqueflies"
92 |
93 | ```
94 | ### Adapting based on props
95 |
96 | 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.
97 |
98 | ### Important
99 |
100 | > 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)
101 |
102 | ```
103 | {
104 | props: {
105 | propA: String,
106 | propB: [String, Number]
107 | }
108 | }
109 | ```
110 |
111 | ```JSX
112 | import styled from 'vue-styled-components';
113 |
114 | const btnProps = { primary: Boolean };
115 |
116 | const StyledButton = styled('button', btnProps)`
117 | font-size: 1em;
118 | margin: 1em;
119 | padding: 0.25em 1em;
120 | border: 2px solid palevioletred;
121 | border-radius: 3px;
122 | background: ${props => props.primary ? 'palevioletred' : 'white'};
123 | color: ${props => props.primary ? 'white' : 'palevioletred'};
124 | `;
125 |
126 | export default StyledButton;
127 | ```
128 |
129 | ```JSX
130 | Normal
131 | Primary
132 | ```
133 |
134 | ### Overriding component styles
135 |
136 | Taking the `StyledButton` component from above and removing the primary rules, this is what we're left with – just a normal button:
137 |
138 | ```JSX
139 | import styled from 'vue-styled-components';
140 |
141 | const StyledButton = styled.button`
142 | background: white;
143 | color: palevioletred;
144 | font-size: 1em;
145 | margin: 1em;
146 | padding: 0.25em 1em;
147 | border: 2px solid palevioletred;
148 | border-radius: 3px;
149 | `;
150 |
151 | export default StyledButton;
152 | ```
153 |
154 | ### Theming
155 |
156 | `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.
157 |
158 | Remember to register `ThemeProvider` locally.
159 |
160 | ```JSX
161 | import {ThemeProvider} from 'vue-styled-components'
162 |
163 | new Vue({
164 | // ...
165 | components: {
166 | 'theme-provider': ThemeProvider
167 | },
168 | // ...
169 | });
170 | ```
171 |
172 | Add your `ThemeProvider` component:
173 |
174 | ```vue
175 |
178 |
179 | // ...
180 |
181 |
182 | ```
183 |
184 | And into your `Wrapper` component:
185 |
186 | ```JSX
187 | const Wrapper = styled.default.section`
188 | padding: 4em;
189 | background: ${props => props.theme.primary};
190 | `;
191 | ```
192 |
193 | ### Style component constructors as `router-link`
194 |
195 | You can style also Vue component constructors as `router-link` from `vue-router` and other components
196 |
197 | ```JSX
198 | import styled from 'vue-styled-components';
199 |
200 | // unfortunately you can't import directly router-link, you have to retrieve contstructor
201 | const RouterLink = Vue.component('router-link')
202 |
203 | const StyledLink = styled(RouterLink)`
204 | color: palevioletred;
205 | font-size: 1em;
206 | text-decoration: none;
207 | `;
208 |
209 | export default StyledLink;
210 | ```
211 |
212 | ```JSX
213 | Custom Router Link
214 | ```
215 |
216 | 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.
217 |
218 | 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:
219 |
220 | ```JSX
221 | // Tomatobutton.js
222 |
223 | import StyledButton from './StyledButton';
224 |
225 | const TomatoButton = StyledButton.extend`
226 | color: tomato;
227 | border-color: tomato;
228 | `;
229 |
230 | export default TomatoButton;
231 | ```
232 |
233 | ### Polymorphic `as` prop
234 | 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`.
235 |
236 | **Example**
237 | In `Component.js`
238 | ```js
239 | // Renders a div element by default.
240 | const Component = styled('div', {})``
241 | ```
242 | Using the `as` prop in another template/component would be as shown below.
243 | ```vue
244 |
245 |
246 |
247 | Button
248 |
249 |
250 | ```
251 | 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.
252 |
253 | ### withComponent
254 | 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`.
255 | ```JSX
256 | const Button = styled.button`
257 | background: green;
258 | color: white;
259 | `
260 | const Link = Button.withComponent('a')
261 | ```
262 |
263 | ### injectGlobal
264 |
265 | A helper method to write global CSS. Does not return a component, adds the styles to the stylesheet directly.
266 |
267 | **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.
268 |
269 | ```JS
270 | // global-styles.js
271 |
272 | import { injectGlobal } from 'vue-styled-components';
273 |
274 | injectGlobal`
275 | @font-face {
276 | font-family: 'Operator Mono';
277 | src: url('../fonts/Operator-Mono.ttf');
278 | }
279 |
280 | body {
281 | margin: 0;
282 | }
283 | `;
284 | ```
285 |
286 | ## Syntax highlighting
287 |
288 | 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.
289 |
290 | ### Atom
291 |
292 | [**@gandm**](https://github.com/gandm), the creator of `language-babel`, has added support for `styled-components` in Atom!
293 |
294 | To get proper syntax highlighting, all you have to do is install and use the `language-babel` package for your JavaScript files!
295 |
296 | ### Sublime Text
297 |
298 | 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!)
299 |
300 | 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!
301 |
302 | ### Visual Studio Code
303 |
304 | 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).
305 |
306 | ### VIM / NeoVim
307 | 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.
308 |
309 | 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).
310 |
311 | ### Other Editors
312 |
313 | 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.
314 |
315 | ## License
316 |
317 | Licensed under the MIT License, Copyright © 2017 Lorenzo Girardi.
318 |
319 | See [LICENSE](./LICENSE) for more information.
320 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Basic Example
6 |
7 |
8 | Basic Example
9 |
10 |
11 |
12 |
13 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-styled-components",
3 | "version": "1.6.0",
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/styled-components/vue-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/styled-components/vue-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": "^2.6.11"
82 | },
83 | "lint-staged": {
84 | "*.js": [
85 | "eslint --fix",
86 | "git add"
87 | ]
88 | },
89 | "pre-commit": "lint-staged"
90 | }
91 |
--------------------------------------------------------------------------------
/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 prodOutput = [
22 | { exports, file: 'dist/vue-styled-components.min.js', format: 'umd', name: moduleName }
23 | ]
24 |
25 | const devOutput = [
26 | { exports, file: 'dist/vue-styled-components.js', format: 'umd', name: moduleName },
27 | { exports, file: 'dist/vue-styled-components.es.js', format: 'es', name: moduleName }
28 | ]
29 |
30 | const output = prod ? prodOutput : devOutput
31 |
32 | const plugins = [
33 | commonjs(),
34 | babel({
35 | babelrc: true
36 | }),
37 | // Unlike Webpack and Browserify, Rollup doesn't automatically shim Node
38 | // builtins like `process`. This ad-hoc plugin creates a 'virtual module'
39 | // which includes a shim containing just the parts the bundle needs.
40 | {
41 | resolveId (importee) {
42 | if (importee === processShim) return importee
43 | return null
44 | },
45 | load (id) {
46 | if (id === processShim) return 'export default { argv: [], env: {} }'
47 | return null
48 | }
49 | },
50 | builtins(),
51 | nodeResolve({
52 | mainFields: ['module', 'main', 'jsnext', 'browser']
53 | }),
54 | replace({
55 | 'process.env.NODE_ENV': JSON.stringify(prod ? 'production' : 'development')
56 | }),
57 | inject({
58 | process: processShim
59 | }),
60 | json()
61 | ]
62 |
63 | if (prod) plugins.push(terser(), visualizer({ filename: './bundle-stats.html' }))
64 |
65 | export default {
66 | input: 'src/index.js',
67 | output,
68 | plugins
69 | }
70 |
--------------------------------------------------------------------------------
/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/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/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 = {}, options = {}) => {
7 | if (!isValidElementType(tagName)) {
8 | throw new Error(tagName + ' is not allowed for styled tag type.')
9 | }
10 |
11 | const templateFunction = (cssRules, ...interpolations) => (
12 | createStyledComponent(tagName, css(cssRules, ...interpolations), props, options)
13 | )
14 |
15 | templateFunction.attrs = attrs => styled(tagName, props, {
16 | ...options,
17 | attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean)
18 | })
19 |
20 | return templateFunction
21 | }
22 |
23 | domElements.forEach((domElement) => {
24 | styled[domElement] = styled(domElement)
25 | })
26 |
27 | return styled
28 | }
29 |
--------------------------------------------------------------------------------
/src/constructors/test/css.test.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/styled-components/vue-styled-components/6f296bf57f50893d578e892c1898ddb14e1d16ad/src/constructors/test/css.test.js
--------------------------------------------------------------------------------
/src/constructors/test/injectGlobal.test.js:
--------------------------------------------------------------------------------
1 | import Vue 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 = new Vue(Comp).$mount();
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/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/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 |
7 | import _styledComponent from './models/StyledComponent'
8 | import _componentStyle from './models/ComponentStyle'
9 | import _styled from './constructors/styled'
10 |
11 | const styled = _styled(
12 | _styledComponent(_componentStyle(generateAlphabeticName))
13 | )
14 |
15 | export default styled
16 |
17 | export { css, injectGlobal, keyframes, ThemeProvider }
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/models/StyledComponent.js:
--------------------------------------------------------------------------------
1 | import css from '../constructors/css'
2 | import normalizeProps from '../utils/normalizeProps'
3 | import isVueComponent from '../utils/isVueComponent'
4 |
5 | export default (ComponentStyle) => {
6 | const createStyledComponent = (target, rules, props, options) => {
7 | const {
8 | attrs = []
9 | } = options
10 | const componentStyle = new ComponentStyle(rules)
11 |
12 | // handle array-declaration props
13 | const currentProps = normalizeProps(props)
14 | const prevProps = normalizeProps(target.props)
15 |
16 | const StyledComponent = {
17 | inject: {
18 | $theme: {
19 | default: function () {
20 | return () => ({ })
21 | }
22 | }
23 | },
24 | props: {
25 | as: [String, Object],
26 | value: null,
27 | ...currentProps,
28 | ...prevProps
29 | },
30 | data () {
31 | return {
32 | localValue: this.value
33 | }
34 | },
35 | render (createElement) {
36 | const children = []
37 | for (const slot in this.$slots) {
38 | if (slot === 'default') {
39 | children.push(this.$slots[slot])
40 | } else {
41 | children.push(createElement('template', { slot }, this.$slots[slot]))
42 | }
43 | }
44 |
45 | return createElement(
46 | // Check if target is StyledComponent to preserve inner component styles for composition
47 | isVueComponent(target) ? target : this.$props.as || target,
48 | {
49 | class: [this.generatedClassName],
50 | props: this.$props,
51 | domProps: {
52 | ...this.attrs,
53 | value: this.localValue
54 | },
55 | on: {
56 | ...this.$listeners,
57 | input: event => {
58 | if (event && event.target) {
59 | this.localValue = event.target.value
60 | }
61 | }
62 | },
63 | scopedSlots: this.$scopedSlots
64 | },
65 | children
66 | )
67 | },
68 | methods: {
69 | generateAndInjectStyles (componentProps) {
70 | return componentStyle.generateAndInjectStyles(componentProps)
71 | }
72 | },
73 | computed: {
74 | generatedClassName () {
75 | const { context, attrs } = this
76 | const componentProps = { ...context, ...attrs }
77 | return this.generateAndInjectStyles(componentProps)
78 | },
79 | theme () {
80 | return this.$theme()
81 | },
82 | context () {
83 | return {
84 | theme: this.theme,
85 | ...this.$props
86 | }
87 | },
88 | attrs () {
89 | const resolvedAttrs = {}
90 | const { context } = this
91 |
92 | attrs.forEach((attrDef) => {
93 | let resolvedAttrDef = attrDef
94 |
95 | if (typeof resolvedAttrDef === 'function') {
96 | resolvedAttrDef = resolvedAttrDef(context)
97 | }
98 |
99 | for (const key in resolvedAttrDef) {
100 | context[key] = resolvedAttrs[key] = resolvedAttrDef[key]
101 | }
102 | })
103 |
104 | return resolvedAttrs
105 | }
106 | },
107 | watch: {
108 | value (newValue) {
109 | this.localValue = newValue
110 | },
111 | localValue () {
112 | this.$emit('input', this.localValue)
113 | }
114 | },
115 | extend (cssRules, ...interpolations) {
116 | const extendedRules = css(cssRules, ...interpolations)
117 | return createStyledComponent(target, rules.concat(extendedRules), props, options)
118 | },
119 | withComponent (newTarget) {
120 | return createStyledComponent(newTarget, rules, props, options)
121 | }
122 | }
123 |
124 | return StyledComponent
125 | }
126 |
127 | return createStyledComponent
128 | }
129 |
--------------------------------------------------------------------------------
/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/providers/ThemeProvider.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'ThemeProvider',
3 | props: {
4 | theme: Object
5 | },
6 | provide () {
7 | return {
8 | $theme: () => this.theme
9 | }
10 | },
11 | render: function (createElement) {
12 | return createElement('div', {}, this.$slots.default)
13 | }
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/src/test/as.test.js:
--------------------------------------------------------------------------------
1 | import Vue 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 = new Vue({
17 | render: (h) => h(Base, {
18 | props: {
19 | as: 'button'
20 | }
21 | })
22 | }).$mount()
23 | expect(b.$el.tagName.toLowerCase()).toEqual('button')
24 | })
25 |
26 |
27 | it('should append base class to new components composing lower level styled components', () => {
28 | const Base = styled.div`
29 | color: blue;
30 | `
31 | const Composed = styled(Base, {
32 | bg: String,
33 | })`
34 | background: ${props => props.bg};
35 | `
36 |
37 | const b = new Vue(Base).$mount()
38 | const c = new Vue({
39 | render: (h) => h(Composed, {
40 | props: {
41 | bg: 'yellow',
42 | as: 'dialog'
43 | }
44 | })
45 | }).$mount()
46 |
47 | expect(c.$el.tagName.toLowerCase()).toEqual('dialog')
48 | expect(c.$el._prevClass.includes(b.$el._prevClass)).toBeTruthy()
49 | expectCSSMatches('.a{color: blue;} .b{background:yellow;}')
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/src/test/attrs.test.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import expect from 'expect'
3 |
4 | import { resetStyled, expectCSSMatches } from './utils'
5 |
6 | let styled
7 |
8 | describe('"attrs" feature', () => {
9 | beforeEach(() => {
10 | styled = resetStyled()
11 | })
12 |
13 | it('should add html attributes to an element', () => {
14 | const Component = styled('img', {}).attrs({ src: 'image.jpg' })`
15 | width: 50;
16 | `
17 | const vm = new Vue(Component).$mount()
18 | expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg' })
19 | })
20 |
21 | it('should add several html attributes to an element', () => {
22 | const Component = styled('img', {}).attrs({ src: 'image.jpg', alt: 'Test image' })`
23 | width: 50;
24 | `
25 | const vm = new Vue(Component).$mount()
26 | expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image' })
27 | })
28 |
29 | it('should work as expected with empty attributes object provided', () => {
30 | const Component = styled('img', {}).attrs({})`
31 | width: 50;
32 | `
33 | const vm = new Vue(Component).$mount()
34 | expectCSSMatches('.a {width: 50;}')
35 | })
36 |
37 | it('should work as expected with null attributes object provided', () => {
38 | const Component = styled('img', {}).attrs(null)`
39 | width: 50;
40 | `
41 | const vm = new Vue(Component).$mount()
42 | expectCSSMatches('.a {width: 50;}')
43 | expect(vm._vnode.data.domProps).toEqual({})
44 | })
45 |
46 | it('should work as expected without attributes provided', () => {
47 | const Component = styled('img')`
48 | width: 50;
49 | `
50 | const vm = new Vue(Component).$mount()
51 | expectCSSMatches('.a {width: 50;}')
52 | expect(vm._vnode.data.domProps).toEqual({})
53 | })
54 |
55 | it('should work with a function as a parameter of of the method', () => {
56 | const Component = styled('img', {}).attrs(() => ({
57 | src: 'image.jpg',
58 | alt: 'Test image',
59 | height: '50'
60 | }))`
61 | width: 50;
62 | `
63 | const vm = new Vue(Component).$mount()
64 | expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: '50' })
65 | })
66 |
67 | it('should work with multiple attrs method call', () => {
68 | const Component = styled('img', {})
69 | .attrs(() => ({
70 | src: 'image.jpg',
71 | alt: 'Test image'
72 | }))
73 | .attrs({
74 | height: '50'
75 | })`
76 | width: 50;
77 | `
78 | const vm = new Vue(Component).$mount()
79 | expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: '50' })
80 | })
81 |
82 | it('should access to all previous attribute properties', () => {
83 | const Component = styled('img', {})
84 | .attrs(() => ({
85 | src: 'image',
86 | alt: 'My test image'
87 | }))
88 | .attrs((props) => ({
89 | src: props.src + '.jpg',
90 | height: 5 * 10
91 | }))`
92 | width: 50;
93 | `
94 | const vm = new Vue(Component).$mount()
95 | expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'My test image', height: 50 })
96 | })
97 |
98 | it('should override attribute properties', () => {
99 | const Component = styled('img', {})
100 | .attrs(() => ({
101 | src: 'image.jpg',
102 | alt: 'Test image',
103 | height: '20'
104 | }))
105 | .attrs({
106 | height: '50'
107 | })`
108 | width: 50;
109 | `
110 | const vm = new Vue(Component).$mount()
111 | expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: '50' })
112 | })
113 |
114 | it('should access to component props', () => {
115 | const Component = styled('img', { propsHeight: Number })
116 | .attrs((props) => ({
117 | src: 'image.jpg',
118 | alt: 'Test image',
119 | height: props.propsHeight * 2
120 | }))`
121 | width: 50;
122 | `
123 |
124 | const vm = new Vue({
125 | render: function (h) {
126 | return h(Component, {
127 | props: {
128 | propsHeight: 20
129 | },
130 | })
131 | }
132 | }).$mount()
133 |
134 | expect(vm.$children[0]._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: 40 })
135 | })
136 |
137 | it('attributes should be reactive', () => {
138 | const Component = styled('img', { propsHeight: Number })
139 | .attrs((props) => ({
140 | src: 'image.jpg',
141 | alt: 'Test image',
142 | height: props.propsHeight * 2
143 | }))`
144 | width: 50;
145 | `
146 |
147 | const vm = new Vue({
148 | render: function (h) {
149 | const self = this
150 | return h(Component, {
151 | props: {
152 | propsHeight: self.dataHeight
153 | },
154 | })
155 | },
156 | data: () => ({
157 | dataHeight: 20
158 | })
159 | }).$mount()
160 |
161 | expect(vm.$children[0]._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: 40 })
162 |
163 | vm.dataHeight = 90
164 | setTimeout(() => { // $nextTick
165 | expect(vm.$children[0]._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: 180 })
166 | }, 0)
167 | })
168 | })
169 |
--------------------------------------------------------------------------------
/src/test/basic.test.js:
--------------------------------------------------------------------------------
1 | import Vue 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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(WrapperComp).$mount();
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 = new Vue(WrapperComp).$mount();
90 |
91 | // expect(wrapper.node.testRef).toExist()
92 | // expect(wrapper.node.ref).toNotExist()
93 | // })
94 | // })
95 | })
96 |
--------------------------------------------------------------------------------
/src/test/component-features.test.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/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 = new Vue({
25 | components: { StyledComp },
26 | template: `ActualContent`
27 | }).$mount()
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 = new Vue({
38 | components: { StyledComp },
39 | template: `
40 |
41 | ActualContent
42 | `
43 | }).$mount()
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 = new Vue({
54 | components: { StyledComp },
55 | template: `
56 |
57 | {{ p }}
58 | `
59 | }).$mount()
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 = new Vue({
70 | components: { StyledComp },
71 | template: `
72 |
73 | {{ p }}
74 | `
75 | }).$mount()
76 | expect(vm.$el.innerHTML).toEqual('ActualContent')
77 | })
78 |
79 | })
80 |
--------------------------------------------------------------------------------
/src/test/css.test.js:
--------------------------------------------------------------------------------
1 | import Vue 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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
58 | expectCSSMatches('.a {--custom-prop: some-val;}')
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/src/test/extending-components.test.js:
--------------------------------------------------------------------------------
1 | import Vue 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 = new Vue(Parent).$mount()
22 | const c = new Vue(Child).$mount()
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 = new Vue(Parent).$mount()
33 | const c = new Vue(Child).$mount()
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 = new Vue(Parent).$mount()
43 | const c = new Vue(Child).$mount()
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 = new Vue(Parent).$mount()
53 | const c = new Vue(Child).$mount()
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 = new Vue(Parent).$mount()
63 | const c = new Vue(Child).$mount()
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 = new Vue(Parent).$mount()
76 | const c = new Vue(Child).$mount()
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 = new Vue(Parent).$mount()
96 | const c = new Vue(Child).$mount()
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 = new Vue(Child).$mount()
118 | const p = new Vue(Parent).$mount()
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/extending-styles.test.js:
--------------------------------------------------------------------------------
1 | import Vue 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 = new Vue(Base).$mount()
21 | const e = new Vue(Extended).$mount()
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/test/props.test.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import { resetStyled, expectCSSMatches } from './utils'
4 | import ThemeProvider from "../providers/ThemeProvider"
5 |
6 | let styled
7 |
8 | describe('props', () => {
9 | beforeEach(() => {
10 | styled = resetStyled()
11 | })
12 |
13 | it('should execute interpolations and fall back', () => {
14 | const compProps = { fg: String }
15 | const Comp = styled('div', compProps)`
16 | color: ${props => props.fg || 'black'};
17 | `
18 | const vm = new Vue(Comp).$mount()
19 | expectCSSMatches('.a {color: black;}')
20 | })
21 |
22 | it('should execute interpolations and inject props', () => {
23 | const compProps = { fg: String }
24 | const Comp = styled('div', compProps)`
25 | color: ${props => props.fg || 'black'};
26 | `
27 | const Ctor = Vue.extend(Comp)
28 | const vm = new Ctor({
29 | propsData: {
30 | fg: 'red'
31 | }
32 | }).$mount()
33 | expectCSSMatches('.a {color: red;}')
34 | })
35 |
36 | it('should add any injected theme to the component', () => {
37 | const theme = {
38 | blue: "blue",
39 | }
40 |
41 | const Comp = styled.div`
42 | color: ${props => props.theme.blue};
43 | `
44 | const Themed = {
45 | render: function(createElement) {
46 | return createElement(
47 | ThemeProvider,
48 | {
49 | props: {
50 | theme,
51 | },
52 | },
53 | [
54 | createElement(Comp)
55 | ]
56 | )
57 | }
58 | }
59 |
60 | const vm = new Vue(Themed).$mount()
61 | expectCSSMatches('.a {color: blue;}')
62 | })
63 | })
64 |
--------------------------------------------------------------------------------
/src/test/styles.test.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(Comp).$mount()
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 = new Vue(FirstComp).$mount()
127 | const vm2 = new Vue(SecondComp).$mount()
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 = new Vue(SecondComp).$mount()
144 | const vm1 = new Vue(FirstComp).$mount()
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 = new Vue(Comp).$mount()
161 | expectCSSMatches(`
162 | .a {color: blue;}
163 | `)
164 | })
165 | })
166 |
--------------------------------------------------------------------------------
/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/test/withComponent.test.js:
--------------------------------------------------------------------------------
1 | import Vue 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 = new Vue(OldTarget).$mount()
18 | const n = new Vue(NewTarget).$mount()
19 |
20 | assert(o._vnode.tag === 'div');
21 | assert(n._vnode.tag === 'a');
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/isStyledComponent.js:
--------------------------------------------------------------------------------
1 | export default function isStyledComponent (target) {
2 | return target &&
3 | target.methods &&
4 | typeof target.methods.generateAndInjectStyles === 'function'
5 | }
6 |
--------------------------------------------------------------------------------
/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/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/isVueComponent.js:
--------------------------------------------------------------------------------
1 | export default function isVueComponent (target) {
2 | return target &&
3 | (
4 | typeof target.render === 'function' ||
5 | typeof target.template === 'string'
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------