Browse Source

Rendering

dynamic-styles
Giuseppe Gurgone 7 years ago
parent
commit
5f512328ba
  1. 32
      src/_utils.js
  2. 1
      src/babel.js
  3. 13
      src/render.js
  4. 37
      src/style.js
  5. 24
      test/__snapshots__/index.js.snap

32
src/_utils.js

@ -30,7 +30,6 @@ export const findStyles = path => {
return path.get('children').filter(isStyledJsx) return path.get('children').filter(isStyledJsx)
} }
// We only allow constants to be used in template literals.
// The following visitor ensures that MemberExpressions and Identifiers // The following visitor ensures that MemberExpressions and Identifiers
// are not in the scope of the current Method (render) or function (Component). // are not in the scope of the current Method (render) or function (Component).
export const validateExpressionVisitor = { export const validateExpressionVisitor = {
@ -69,9 +68,7 @@ export const validateExpressionVisitor = {
} }
} }
export const validateExpression = (expr, scope) => // Use `validateExpressionVisitor` to determine whether the `expr`ession has dynamic values.
expr.traverse(validateExpressionVisitor, scope)
export const isDynamic = (expr, scope) => { export const isDynamic = (expr, scope) => {
try { try {
expr.traverse(validateExpressionVisitor, scope) expr.traverse(validateExpressionVisitor, scope)
@ -121,13 +118,15 @@ export const getJSXStyleInfo = (expr, scope) => {
// p { color: %%styled-jsx-placeholder-${id}%%; } // p { color: %%styled-jsx-placeholder-${id}%%; }
const { quasis, expressions } = node const { quasis, expressions } = node
const dynamic = scope && isDynamic(expr, scope)
const css = quasis.reduce((css, quasi, index) => {
return `${css}${quasi.value.cooked}${quasis.length === index + 1
? ''
: `%%styled-jsx-placeholder-${index}%%`}`
}, '')
const hash = String(hashString(expr.getSource().slice(1, -1))) const hash = String(hashString(expr.getSource().slice(1, -1)))
const dynamic = scope ? isDynamic(expr, scope) : false
const css = quasis.reduce(
(css, quasi, index) =>
`${css}${quasi.value.cooked}${quasis.length === index + 1
? ''
: `%%styled-jsx-placeholder-${index}%%`}`,
''
)
return { return {
hash, hash,
@ -167,9 +166,9 @@ export const buildJsxId = (styles, externalJsxId) => {
return t.stringLiteral(hashes.static.join(' ')) return t.stringLiteral(hashes.static.join(' '))
} }
// _JSXStyle.get([ ['1234', [props.foo, bar, fn(props)]], ... ]) // _JSXStyle.dynamic([ ['1234', [props.foo, bar, fn(props)]], ... ])
const dynamic = t.callExpression( const dynamic = t.callExpression(
// Callee: _JSXStyle.get // Callee: _JSXStyle.dynamic
t.memberExpression(t.identifier(STYLE_COMPONENT), t.identifier('dynamic')), t.memberExpression(t.identifier(STYLE_COMPONENT), t.identifier('dynamic')),
// Arguments // Arguments
[t.arrayExpression(hashes.dynamic)] [t.arrayExpression(hashes.dynamic)]
@ -179,11 +178,14 @@ export const buildJsxId = (styles, externalJsxId) => {
return dynamic return dynamic
} }
// `1234 5678 ${_JSXStyle.get([ ['1234', [props.foo, bar, fn(props)]], ... ])}` // `1234 5678 ${_JSXStyle.dynamic([ ['1234', [props.foo, bar, fn(props)]], ... ])}`
return t.templateLiteral( return t.templateLiteral(
[ [
t.templateElement( t.templateElement(
{ raw: hashes.static.join(' ') + ' ', cooked: hashes.static }, {
raw: hashes.static.join(' ') + ' ',
cooked: hashes.static.join(' ') + ' '
},
false false
), ),
t.templateElement({ raw: '', cooked: '' }, true) t.templateElement({ raw: '', cooked: '' }, true)
@ -198,7 +200,7 @@ export const templateLiteralFromPreprocessedCss = (css, expressions) => {
const parts = css.split(/(?:%%styled-jsx-placeholder-(\d+)%%)/g) const parts = css.split(/(?:%%styled-jsx-placeholder-(\d+)%%)/g)
parts.forEach((part, index) => { parts.forEach((part, index) => {
if (index % 2 > 0) { if (index % 2 > 0) {
// This is necessary because after preprocessing declarations might have been alternate. // This is necessary because, after preprocessing, declarations might have been alterate.
// eg. properties are auto prefixed and therefore expressions need to match. // eg. properties are auto prefixed and therefore expressions need to match.
finalExpressions.push(expressions[part]) finalExpressions.push(expressions[part])
} else { } else {

1
src/babel.js

@ -30,6 +30,7 @@ import {
const getPrefix = (isDynamic, id) => const getPrefix = (isDynamic, id) =>
`[${MARKUP_ATTRIBUTE}~="${isDynamic ? '?' : id}"]` `[${MARKUP_ATTRIBUTE}~="${isDynamic ? '?' : id}"]`
const callExternalVisitor = (visitor, path, state) => { const callExternalVisitor = (visitor, path, state) => {
const { file } = state const { file } = state
const { opts } = file const { opts } = file

13
src/render.js

@ -1,5 +1,6 @@
const tags = new Map() const tags = new Map()
let prevStyles = new Map() let prevStyles = new Map()
const mountedInstancesCount = {}
export default (typeof window === 'undefined' ? renderOnServer : renderOnClient) export default (typeof window === 'undefined' ? renderOnServer : renderOnClient)
@ -25,11 +26,19 @@ function patch([added, removed]) {
fromServer.set(id, document.getElementById(`__jsx-style-${id}`)) fromServer.set(id, document.getElementById(`__jsx-style-${id}`))
} }
const tag = fromServer.get(id) || makeStyleTag(css) mountedInstancesCount[id] = (mountedInstancesCount[id] || 0) + 1
tags.set(id, tag) if (mountedInstancesCount[id] === 1) {
const tag = fromServer.get(id) || makeStyleTag(css)
tags.set(id, tag)
}
} }
for (const [id] of removed) { for (const [id] of removed) {
mountedInstancesCount[id] -= 1
if (mountedInstancesCount[id] > 0) {
continue
}
delete mountedInstancesCount[id]
const t = tags.get(id) const t = tags.get(id)
tags.delete(id) tags.delete(id)
t.parentNode.removeChild(t) t.parentNode.removeChild(t)

37
src/style.js

@ -1,9 +1,24 @@
import { Component } from 'react' import { Component } from 'react'
import hashString from 'string-hash'
import render from './render' import render from './render'
let components = [] let components = []
function hashArray(arr) {
return hashString(arr.join(','))
}
export default class extends Component { export default class extends Component {
static dynamic(arr) {
return arr
.map(tagInfo => {
const [styleId, expressions] = tagInfo
const hash = hashArray(expressions)
return `${styleId}-${hash}`
})
.join(' ')
}
componentWillMount() { componentWillMount() {
mount(this) mount(this)
} }
@ -14,7 +29,8 @@ export default class extends Component {
update({ update({
instance: this, instance: this,
styleId: nextProps.styleId, styleId: nextProps.styleId,
css: nextProps.css css: nextProps.css,
dynamic: nextProps.dynamic
}) })
} }
@ -30,13 +46,16 @@ export default class extends Component {
function stylesMap(updated) { function stylesMap(updated) {
const ret = new Map() const ret = new Map()
for (const c of components) { for (const c of components) {
if (updated && c === updated.instance) { // On `componentWillUpdate`
// On `componentWillUpdate` // we use `styleId` and `css` from updated component rather than reading `props`
// we use `styleId` and `css` from updated component rather than reading `props` // from the component since they haven't been updated yet.
// from the component since they haven't been updated yet. const props = updated && c === updated.instance ? updated : c.props
ret.set(updated.styleId, updated.css)
if (props.dynamic) {
const styleId = `${props.styleId}-${hashArray(props.dynamic)}`
ret.set(styleId, scopeCss(styleId, props.css))
} else { } else {
ret.set(c.props.styleId, c.props.css) ret.set(props.styleId, props.css)
} }
} }
return ret return ret
@ -66,3 +85,7 @@ function unmount(component) {
function update(updates) { function update(updates) {
render(stylesMap(updates)) render(stylesMap(updates))
} }
function scopeCss(id, css) {
return css.replace(/\[data-jsx~="\?"]/g, `[data-jsx~="${id}"]`)
}

24
test/__snapshots__/index.js.snap

@ -92,21 +92,21 @@ const animationName = 'my-cool-animation';
const obj = { display: 'block' }; const obj = { display: 'block' };
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
export default (({ display }) => <div data-jsx={\`2129566164 188072295 806016056 806016056 924167211 924167211 3469794077 945380644 3617592140 2369334310 3168033860 \${_JSXStyle.dynamic([['2799014000', [display ? 'block' : 'none']]])}\`}> export default (({ display }) => <div data-jsx={\`3864570373 188072295 964903971 964903971 2993630608 2993630608 2683050534 1376588607 3617592140 4197553757 3032198101 \${_JSXStyle.dynamic([['3588394506', [display ? 'block' : 'none']]])}\`}>
<p data-jsx={\`2129566164 188072295 806016056 806016056 924167211 924167211 3469794077 945380644 3617592140 2369334310 3168033860 \${_JSXStyle.dynamic([['2799014000', [display ? 'block' : 'none']]])}\`}>test</p> <p data-jsx={\`3864570373 188072295 964903971 964903971 2993630608 2993630608 2683050534 1376588607 3617592140 4197553757 3032198101 \${_JSXStyle.dynamic([['3588394506', [display ? 'block' : 'none']]])}\`}>test</p>
<_JSXStyle styleId={\\"2129566164\\"} css={\`p.\${color}[data-jsx~=\\"2129566164\\"]{color:\${otherColor};display:\${obj.display}}\`} /> <_JSXStyle styleId={\\"3864570373\\"} css={\`p.\${color}[data-jsx~=\\"3864570373\\"]{color:\${otherColor};display:\${obj.display}}\`} />
<_JSXStyle styleId={\\"188072295\\"} css={\\"p[data-jsx~=\\\\\\"188072295\\\\\\"]{color:red}\\"} /> <_JSXStyle styleId={\\"188072295\\"} css={\\"p[data-jsx~=\\\\\\"188072295\\\\\\"]{color:red}\\"} />
<_JSXStyle styleId={\\"806016056\\"} css={\`body{background:\${color}}\`} /> <_JSXStyle styleId={\\"964903971\\"} css={\`body{background:\${color}}\`} />
<_JSXStyle styleId={\\"806016056\\"} css={\`body{background:\${color}}\`} /> <_JSXStyle styleId={\\"964903971\\"} css={\`body{background:\${color}}\`} />
<_JSXStyle styleId={\\"924167211\\"} css={\`p[data-jsx~=\\"924167211\\"]{color:\${color}}\`} /> <_JSXStyle styleId={\\"2993630608\\"} css={\`p[data-jsx~=\\"2993630608\\"]{color:\${color}}\`} />
<_JSXStyle styleId={\\"924167211\\"} css={\`p[data-jsx~=\\"924167211\\"]{color:\${color}}\`} /> <_JSXStyle styleId={\\"2993630608\\"} css={\`p[data-jsx~=\\"2993630608\\"]{color:\${color}}\`} />
<_JSXStyle styleId={\\"3469794077\\"} css={\`p[data-jsx~=\\"3469794077\\"]{color:\${darken(color)}}\`} /> <_JSXStyle styleId={\\"2683050534\\"} css={\`p[data-jsx~=\\"2683050534\\"]{color:\${darken(color)}}\`} />
<_JSXStyle styleId={\\"945380644\\"} css={\`p[data-jsx~=\\"945380644\\"]{color:\${darken(color) + 2}}\`} /> <_JSXStyle styleId={\\"1376588607\\"} css={\`p[data-jsx~=\\"1376588607\\"]{color:\${darken(color) + 2}}\`} />
<_JSXStyle styleId={\\"3617592140\\"} css={\`@media (min-width:\${mediumScreen}){p[data-jsx~=\\"3617592140\\"]{color:green}p[data-jsx~=\\"3617592140\\"]{color:\${\`red\`}}}p[data-jsx~=\\"3617592140\\"]{color:red}\`} /> <_JSXStyle styleId={\\"3617592140\\"} css={\`@media (min-width:\${mediumScreen}){p[data-jsx~=\\"3617592140\\"]{color:green}p[data-jsx~=\\"3617592140\\"]{color:\${\`red\`}}}p[data-jsx~=\\"3617592140\\"]{color:red}\`} />
<_JSXStyle styleId={\\"2369334310\\"} css={\`p[data-jsx~=\\"2369334310\\"]{-webkit-animation-duration:\${animationDuration};animation-duration:\${animationDuration}}\`} /> <_JSXStyle styleId={\\"4197553757\\"} css={\`p[data-jsx~=\\"4197553757\\"]{-webkit-animation-duration:\${animationDuration};animation-duration:\${animationDuration}}\`} />
<_JSXStyle styleId={\\"3168033860\\"} css={\`p[data-jsx~=\\"3168033860\\"]{-webkit-animation:\${animationDuration} forwards \${animationName};animation:\${animationDuration} forwards \${animationName}}\`} /> <_JSXStyle styleId={\\"3032198101\\"} css={\`p[data-jsx~=\\"3032198101\\"]{-webkit-animation:\${animationDuration} forwards \${animationName};animation:\${animationDuration} forwards \${animationName}}\`} />
<_JSXStyle styleId={\\"2799014000\\"} css={\`span[data-jsx~=\\"?\\"]{display:\${display ? 'block' : 'none'}}\`} dynamic={[display ? 'block' : 'none']} /> <_JSXStyle styleId={\\"3588394506\\"} css={\`span[data-jsx~=\\"?\\"]{display:\${display ? 'block' : 'none'}}\`} dynamic={[display ? 'block' : 'none']} />
</div>);" </div>);"
`; `;

Loading…
Cancel
Save