From 83f61be4ca9627733eae48362e5f6559cd7a39f6 Mon Sep 17 00:00:00 2001 From: defcc Date: Sun, 6 Nov 2016 21:50:38 +0800 Subject: [PATCH 1/6] merge style between components --- flow/compiler.js | 1 + flow/vnode.js | 1 + src/platforms/web/compiler/modules/style.js | 18 +++- src/platforms/web/runtime/modules/style.js | 39 ++++---- src/platforms/web/server/modules/style.js | 49 +++------- src/platforms/web/util/style.js | 59 +++++++++++ test/ssr/ssr-string.spec.js | 8 +- test/unit/features/directives/style.spec.js | 102 ++++++++++++++++++++ types/vnode.d.ts | 1 + 9 files changed, 212 insertions(+), 66 deletions(-) create mode 100644 src/platforms/web/util/style.js diff --git a/flow/compiler.js b/flow/compiler.js index a99052d8d06..6a3552db8a0 100644 --- a/flow/compiler.js +++ b/flow/compiler.js @@ -106,6 +106,7 @@ declare type ASTElement = { staticClass?: string; classBinding?: string; + staticStyle?: string; styleBinding?: string; events?: ASTElementHandlers; nativeEvents?: ASTElementHandlers; diff --git a/flow/vnode.js b/flow/vnode.js index c58efa2f9be..00e357df95a 100644 --- a/flow/vnode.js +++ b/flow/vnode.js @@ -37,6 +37,7 @@ declare interface VNodeData { tag?: string; staticClass?: string; class?: any; + staticStyle?: string; style?: Array | Object; props?: { [key: string]: any }; attrs?: { [key: string]: string }; diff --git a/src/platforms/web/compiler/modules/style.js b/src/platforms/web/compiler/modules/style.js index af647922b53..6a8a0d1dc50 100644 --- a/src/platforms/web/compiler/modules/style.js +++ b/src/platforms/web/compiler/modules/style.js @@ -1,10 +1,16 @@ /* @flow */ import { + getAndRemoveAttr, getBindingAttr } from 'compiler/helpers' function transformNode (el: ASTElement) { + const staticStyle = getAndRemoveAttr(el, 'style') + if (staticStyle) { + el.staticStyle = staticStyle + } + const styleBinding = getBindingAttr(el, 'style', false /* getStatic */) if (styleBinding) { el.styleBinding = styleBinding @@ -12,12 +18,18 @@ function transformNode (el: ASTElement) { } function genData (el: ASTElement): string { - return el.styleBinding - ? `style:(${el.styleBinding}),` - : '' + let data = '' + if (el.staticStyle) { + data += 'staticStyle:"' + (el.staticStyle) + '",' + } + if (el.styleBinding) { + data += 'style:(' + (el.styleBinding) + '),' + } + return data } export default { + staticKeys: ['staticStyle'], transformNode, genData } diff --git a/src/platforms/web/runtime/modules/style.js b/src/platforms/web/runtime/modules/style.js index 563f6ee3bb0..08b72453c34 100644 --- a/src/platforms/web/runtime/modules/style.js +++ b/src/platforms/web/runtime/modules/style.js @@ -1,6 +1,7 @@ /* @flow */ -import { cached, extend, camelize, toObject } from 'shared/util' +import { cached, camelize, extend, looseEqual } from 'shared/util' +import { normalizeBindingStyle, getStyle } from 'web/util/style' const cssVarRE = /^--/ const setProp = (el, name, val) => { @@ -31,45 +32,39 @@ const normalize = cached(function (prop) { }) function updateStyle (oldVnode: VNodeWithData, vnode: VNodeWithData) { - if ((!oldVnode.data || !oldVnode.data.style) && !vnode.data.style) { + const data = vnode.data + const oldData = oldVnode.data + + if (!data.staticStyle && !data.style && + !oldData.staticStyle && !oldData.style) { return } + let cur, name const el: any = vnode.elm const oldStyle: any = oldVnode.data.style || {} - let style: any = vnode.data.style || {} - - // handle string - if (typeof style === 'string') { - el.style.cssText = style - return - } - - const needClone = style.__ob__ + const style: Object = normalizeBindingStyle(vnode.data.style || {}) + vnode.data.style = extend({}, style) - // handle array syntax - if (Array.isArray(style)) { - style = vnode.data.style = toObject(style) - } + const newStyle: Object = getStyle(vnode, true) - // clone the style for future updates, - // in case the user mutates the style object in-place. - if (needClone) { - style = vnode.data.style = extend({}, style) + if (looseEqual(el._prevStyle, newStyle)) { + return } for (name in oldStyle) { - if (style[name] == null) { + if (newStyle[name] == null) { setProp(el, name, '') } } - for (name in style) { - cur = style[name] + for (name in newStyle) { + cur = newStyle[name] if (cur !== oldStyle[name]) { // ie9 setting to null has no effect, must use empty string setProp(el, name, cur == null ? '' : cur) } } + el._prevStyle = newStyle } export default { diff --git a/src/platforms/web/server/modules/style.js b/src/platforms/web/server/modules/style.js index a98dec68be7..69fccd10556 100644 --- a/src/platforms/web/server/modules/style.js +++ b/src/platforms/web/server/modules/style.js @@ -1,44 +1,19 @@ /* @flow */ +import { hyphenate } from 'shared/util' +import { getStyle } from 'web/util/style' -import { hyphenate, toObject } from 'shared/util' - -function concatStyleString (former: string, latter: string) { - if (former === '' || latter === '' || former.charAt(former.length - 1) === ';') { - return former + latter - } - return former + ';' + latter -} - -function generateStyleText (node) { - const staticStyle = node.data.attrs && node.data.attrs.style - let styles = node.data.style - const parentStyle = node.parent ? generateStyleText(node.parent) : '' - - if (!styles && !staticStyle) { - return parentStyle - } - - let dynamicStyle = '' - if (styles) { - if (typeof styles === 'string') { - dynamicStyle += styles - } else { - if (Array.isArray(styles)) { - styles = toObject(styles) - } - for (const key in styles) { - dynamicStyle += `${hyphenate(key)}:${styles[key]};` - } - } +function genStyleText (vnode: VNode): string { + let styleText = '' + const style = getStyle(vnode, false) + for (const key in style) { + styleText += `${hyphenate(key)}:${style[key]};` } - - dynamicStyle = concatStyleString(parentStyle, dynamicStyle) - return concatStyleString(dynamicStyle, staticStyle || '') + return styleText.slice(0, -1) } -export default function renderStyle (node: VNodeWithData): ?string { - const res = generateStyleText(node) - if (res) { - return ` style=${JSON.stringify(res)}` +export default function renderStyle (vnode: VNodeWithData): ?string { + const styleText = genStyleText(vnode) + if (styleText) { + return ` style=${JSON.stringify(styleText)}` } } diff --git a/src/platforms/web/util/style.js b/src/platforms/web/util/style.js new file mode 100644 index 00000000000..a3671a23bf0 --- /dev/null +++ b/src/platforms/web/util/style.js @@ -0,0 +1,59 @@ +/* @flow */ + +import { cached, extend, toObject } from 'shared/util' + +const parseStyleText = cached(function (cssText) { + var rs = {} + cssText && cssText.split(/\s*;\s*/).forEach(function (item) { + if (item) { + var styleObj = item.split(/\s*:\s*/) + rs[styleObj[0]] = styleObj[1] + } + }) + return rs +}) + +function normalizeStyleData (styleData: Object): Object { + const style = normalizeBindingStyle(styleData.style) + const staticStyle = parseStyleText(styleData.staticStyle) + return extend(extend({}, staticStyle), style) +} + +export function normalizeBindingStyle (bindingStyle: any): Object { + if (Array.isArray(bindingStyle)) { + return toObject(bindingStyle) + } + + if (typeof bindingStyle === 'string') { + return parseStyleText(bindingStyle) + } + return bindingStyle +} + +/** + * parent component style should be after child's + * so that parent component's style could override it + */ +export function getStyle (vnode: VNode, checkChild: boolean): Object { + let data = vnode.data + let parentNode = vnode + let childNode = vnode + + data = normalizeStyleData(data) + + if (checkChild) { + while (childNode.child) { + childNode = childNode.child._vnode + if (childNode.data) { + data = extend(normalizeStyleData(childNode.data), data) + } + } + } + while ((parentNode = parentNode.parent)) { + if (parentNode.data) { + data = extend(data, normalizeStyleData(parentNode.data)) + } + } + return data +} + diff --git a/test/ssr/ssr-string.spec.js b/test/ssr/ssr-string.spec.js index fbd1d8f55b9..e399fe738ae 100644 --- a/test/ssr/ssr-string.spec.js +++ b/test/ssr/ssr-string.spec.js @@ -66,7 +66,7 @@ describe('SSR: renderToString', () => { } }, result => { expect(result).toContain( - '
' + '
' ) done() }) @@ -107,13 +107,13 @@ describe('SSR: renderToString', () => { it('nested custom component style', done => { renderVmWithOptions({ - template: '', + template: '', data: { style: 'color:red' }, components: { comp: { - template: '', + template: '', components: { nested: { template: '
' @@ -123,7 +123,7 @@ describe('SSR: renderToString', () => { } }, result => { expect(result).toContain( - '
' + '
' ) done() }) diff --git a/test/unit/features/directives/style.spec.js b/test/unit/features/directives/style.spec.js index 96acb21d343..ff6bb37ebe0 100644 --- a/test/unit/features/directives/style.spec.js +++ b/test/unit/features/directives/style.spec.js @@ -166,4 +166,106 @@ describe('Directive v-bind:style', () => { }).then(done) }) } + + it('should merge static style with binding style', () => { + const vm = new Vue({ + template: '
', + data: { + test: { color: 'red', fontSize: '12px' } + } + }).$mount() + expect(vm.$el.style.cssText.replace(/\s/g, '')).toBe('text-align:left;color:red;font-size:12px;') + expect(vm.$el.style.getPropertyValue('color')).toBe('red') + expect(vm.$el.style.getPropertyValue('font-size')).toBe('12px') + }) + + it('should merge between parent and child', done => { + const vm = new Vue({ + template: '', + data: { + test: { color: 'red', fontSize: '12px' } + }, + components: { + child: { + template: '
', + data: () => ({ marginLeft: '16px' }) + } + } + }).$mount() + const child = vm.$children[0] + expect(vm.$el.style.cssText.replace(/\s/g, '')).toBe('margin-right:20px;margin-left:16px;text-align:left;color:red;font-size:12px;') + expect(vm.$el.style.color).toBe('red') + expect(vm.$el.style.marginRight).toBe('20px') + vm.test.color = 'blue' + waitForUpdate(() => { + expect(vm.$el.style.color).toBe('blue') + child.marginLeft = '30px' + }).then(() => { + expect(vm.$el.style.marginLeft).toBe('30px') + child.fontSize = '30px' + }).then(() => { + expect(vm.$el.style.fontSize).toBe('12px') + }).then(done) + }) + + it('should not pass to child root element', () => { + const vm = new Vue({ + template: '', + data: { + test: { color: 'red', fontSize: '12px' } + }, + components: { + child: { + template: '
', + components: { + nested: { + template: '
' + } + } + } + } + }).$mount() + expect(vm.$el.style.color).toBe('red') + expect(vm.$el.style.textAlign).toBe('') + expect(vm.$el.style.fontSize).toBe('12px') + }) + + it('should merge between nested components', (done) => { + const vm = new Vue({ + template: '', + data: { + test: { color: 'red', fontSize: '12px' } + }, + components: { + child: { + template: '', + components: { + nested: { + template: '
', + data: () => ({ nestedStyle: { marginLeft: '30px' }}) + } + } + } + } + }).$mount() + const child = vm.$children[0].$children[0] + expect(vm.$el.style.color).toBe('red') + expect(vm.$el.style.marginLeft).toBe('30px') + expect(vm.$el.style.textAlign).toBe('left') + expect(vm.$el.style.fontSize).toBe('12px') + vm.test.color = 'yellow' + waitForUpdate(() => { + child.nestedStyle.marginLeft = '60px' + }).then(() => { + console.log(vm.$el.style.cssText) + expect(vm.$el.style.marginLeft).toBe('60px') + child.nestedStyle = { + fontSize: '14px', + marginLeft: '40px' + } + }).then(() => { + expect(vm.$el.style.fontSize).toBe('12px') + expect(vm.$el.style.marginLeft).toBe('40px') + }).then(done) + }) }) diff --git a/types/vnode.d.ts b/types/vnode.d.ts index 959252e8cbd..ba2ca73a408 100644 --- a/types/vnode.d.ts +++ b/types/vnode.d.ts @@ -38,6 +38,7 @@ export interface VNodeData { tag?: string; staticClass?: string; class?: any; + staticStyle?: string; style?: Object[] | Object; props?: { [key: string]: any }; attrs?: { [key: string]: any }; From 1984bc5bdd85bfe70e5019805758e9c158a79d5d Mon Sep 17 00:00:00 2001 From: defcc Date: Sun, 6 Nov 2016 22:00:18 +0800 Subject: [PATCH 2/6] update test case --- test/unit/features/directives/style.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/features/directives/style.spec.js b/test/unit/features/directives/style.spec.js index ff6bb37ebe0..c2afb2fd017 100644 --- a/test/unit/features/directives/style.spec.js +++ b/test/unit/features/directives/style.spec.js @@ -216,7 +216,7 @@ describe('Directive v-bind:style', () => { }, components: { child: { - template: '
', + template: '
', components: { nested: { template: '
' @@ -228,6 +228,7 @@ describe('Directive v-bind:style', () => { expect(vm.$el.style.color).toBe('red') expect(vm.$el.style.textAlign).toBe('') expect(vm.$el.style.fontSize).toBe('12px') + expect(vm.$children[0].$refs.nested.$el.style.color).toBe('blue') }) it('should merge between nested components', (done) => { @@ -257,7 +258,6 @@ describe('Directive v-bind:style', () => { waitForUpdate(() => { child.nestedStyle.marginLeft = '60px' }).then(() => { - console.log(vm.$el.style.cssText) expect(vm.$el.style.marginLeft).toBe('60px') child.nestedStyle = { fontSize: '14px', From fbcfb2c160c792a4c78d659b5dc73125d210d462 Mon Sep 17 00:00:00 2001 From: defcc Date: Sun, 6 Nov 2016 22:35:58 +0800 Subject: [PATCH 3/6] update style compiler --- src/platforms/web/compiler/modules/style.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platforms/web/compiler/modules/style.js b/src/platforms/web/compiler/modules/style.js index 6a8a0d1dc50..6f104db2369 100644 --- a/src/platforms/web/compiler/modules/style.js +++ b/src/platforms/web/compiler/modules/style.js @@ -8,7 +8,7 @@ import { function transformNode (el: ASTElement) { const staticStyle = getAndRemoveAttr(el, 'style') if (staticStyle) { - el.staticStyle = staticStyle + el.staticStyle = JSON.stringify(staticStyle) } const styleBinding = getBindingAttr(el, 'style', false /* getStatic */) @@ -20,10 +20,10 @@ function transformNode (el: ASTElement) { function genData (el: ASTElement): string { let data = '' if (el.staticStyle) { - data += 'staticStyle:"' + (el.staticStyle) + '",' + data += `staticStyle:${el.staticStyle},` } if (el.styleBinding) { - data += 'style:(' + (el.styleBinding) + '),' + data += `style:${el.styleBinding},` } return data } From f47f2c8f4cd563cfb2573398b6c47d18e38d4337 Mon Sep 17 00:00:00 2001 From: defcc Date: Sun, 6 Nov 2016 22:41:42 +0800 Subject: [PATCH 4/6] add paren to style binding code --- src/platforms/web/compiler/modules/style.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platforms/web/compiler/modules/style.js b/src/platforms/web/compiler/modules/style.js index 6f104db2369..8c6956a14d1 100644 --- a/src/platforms/web/compiler/modules/style.js +++ b/src/platforms/web/compiler/modules/style.js @@ -23,7 +23,7 @@ function genData (el: ASTElement): string { data += `staticStyle:${el.staticStyle},` } if (el.styleBinding) { - data += `style:${el.styleBinding},` + data += `style:(${el.styleBinding}),` } return data } From 95e7b04a747f2487bc572862b50c99ec9960b74b Mon Sep 17 00:00:00 2001 From: defcc Date: Mon, 7 Nov 2016 21:05:06 +0800 Subject: [PATCH 5/6] update background property parsing --- src/platforms/web/util/style.js | 13 ++++-- test/unit/features/directives/style.spec.js | 44 +++++++++++---------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/platforms/web/util/style.js b/src/platforms/web/util/style.js index a3671a23bf0..e97f6065f7e 100644 --- a/src/platforms/web/util/style.js +++ b/src/platforms/web/util/style.js @@ -4,10 +4,17 @@ import { cached, extend, toObject } from 'shared/util' const parseStyleText = cached(function (cssText) { var rs = {} - cssText && cssText.split(/\s*;\s*/).forEach(function (item) { + if (!cssText) { + return rs + } + var hasBackground = cssText.indexOf('background') >= 0 + // maybe with background-image: url(http://xxx) or base64 img + var listDelimiter = hasBackground ? /;(?![^(]*\))/g : ';' + var propertyDelimiter = hasBackground ? /:(.+)/ : ':' + cssText.split(listDelimiter).forEach(function (item) { if (item) { - var styleObj = item.split(/\s*:\s*/) - rs[styleObj[0]] = styleObj[1] + var tmp = item.split(propertyDelimiter) + tmp.length > 1 && (rs[tmp[0].trim()] = tmp[1].trim()) } }) return rs diff --git a/test/unit/features/directives/style.spec.js b/test/unit/features/directives/style.spec.js index c2afb2fd017..0d63a780aaf 100644 --- a/test/unit/features/directives/style.spec.js +++ b/test/unit/features/directives/style.spec.js @@ -169,14 +169,15 @@ describe('Directive v-bind:style', () => { it('should merge static style with binding style', () => { const vm = new Vue({ - template: '
', + template: '
', data: { test: { color: 'red', fontSize: '12px' } } }).$mount() - expect(vm.$el.style.cssText.replace(/\s/g, '')).toBe('text-align:left;color:red;font-size:12px;') - expect(vm.$el.style.getPropertyValue('color')).toBe('red') - expect(vm.$el.style.getPropertyValue('font-size')).toBe('12px') + const style = vm.$el.style + expect(style.getPropertyValue('background-image')).toMatch('https://vuejs.org/images/logo.png') + expect(style.getPropertyValue('color')).toBe('red') + expect(style.getPropertyValue('font-size')).toBe('12px') }) it('should merge between parent and child', done => { @@ -192,19 +193,20 @@ describe('Directive v-bind:style', () => { } } }).$mount() + const style = vm.$el.style const child = vm.$children[0] - expect(vm.$el.style.cssText.replace(/\s/g, '')).toBe('margin-right:20px;margin-left:16px;text-align:left;color:red;font-size:12px;') - expect(vm.$el.style.color).toBe('red') - expect(vm.$el.style.marginRight).toBe('20px') + expect(style.cssText.replace(/\s/g, '')).toBe('margin-right:20px;margin-left:16px;text-align:left;color:red;font-size:12px;') + expect(style.color).toBe('red') + expect(style.marginRight).toBe('20px') vm.test.color = 'blue' waitForUpdate(() => { - expect(vm.$el.style.color).toBe('blue') + expect(style.color).toBe('blue') child.marginLeft = '30px' }).then(() => { - expect(vm.$el.style.marginLeft).toBe('30px') + expect(style.marginLeft).toBe('30px') child.fontSize = '30px' }).then(() => { - expect(vm.$el.style.fontSize).toBe('12px') + expect(style.fontSize).toBe('12px') }).then(done) }) @@ -225,9 +227,10 @@ describe('Directive v-bind:style', () => { } } }).$mount() - expect(vm.$el.style.color).toBe('red') - expect(vm.$el.style.textAlign).toBe('') - expect(vm.$el.style.fontSize).toBe('12px') + const style = vm.$el.style + expect(style.color).toBe('red') + expect(style.textAlign).toBe('') + expect(style.fontSize).toBe('12px') expect(vm.$children[0].$refs.nested.$el.style.color).toBe('blue') }) @@ -249,23 +252,24 @@ describe('Directive v-bind:style', () => { } } }).$mount() + const style = vm.$el.style const child = vm.$children[0].$children[0] - expect(vm.$el.style.color).toBe('red') - expect(vm.$el.style.marginLeft).toBe('30px') - expect(vm.$el.style.textAlign).toBe('left') - expect(vm.$el.style.fontSize).toBe('12px') + expect(style.color).toBe('red') + expect(style.marginLeft).toBe('30px') + expect(style.textAlign).toBe('left') + expect(style.fontSize).toBe('12px') vm.test.color = 'yellow' waitForUpdate(() => { child.nestedStyle.marginLeft = '60px' }).then(() => { - expect(vm.$el.style.marginLeft).toBe('60px') + expect(style.marginLeft).toBe('60px') child.nestedStyle = { fontSize: '14px', marginLeft: '40px' } }).then(() => { - expect(vm.$el.style.fontSize).toBe('12px') - expect(vm.$el.style.marginLeft).toBe('40px') + expect(style.fontSize).toBe('12px') + expect(style.marginLeft).toBe('40px') }).then(done) }) }) From 393405bceb7419cd07d6773097a7058c0a1165df Mon Sep 17 00:00:00 2001 From: defcc Date: Mon, 7 Nov 2016 21:47:11 +0800 Subject: [PATCH 6/6] introduce interpolation warning and refactor var to const --- src/platforms/web/compiler/modules/style.js | 18 ++++++++++++++++-- src/platforms/web/util/style.js | 8 ++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/platforms/web/compiler/modules/style.js b/src/platforms/web/compiler/modules/style.js index 8c6956a14d1..ad11431fe75 100644 --- a/src/platforms/web/compiler/modules/style.js +++ b/src/platforms/web/compiler/modules/style.js @@ -1,13 +1,27 @@ /* @flow */ +import { parseText } from 'compiler/parser/text-parser' import { getAndRemoveAttr, - getBindingAttr + getBindingAttr, + baseWarn } from 'compiler/helpers' -function transformNode (el: ASTElement) { +function transformNode (el: ASTElement, options: CompilerOptions) { + const warn = options.warn || baseWarn const staticStyle = getAndRemoveAttr(el, 'style') if (staticStyle) { + if (process.env.NODE_ENV !== 'production') { + const expression = parseText(staticStyle, options.delimiters) + if (expression) { + warn( + `style="${staticStyle}": ` + + 'Interpolation inside attributes has been removed. ' + + 'Use v-bind or the colon shorthand instead. For example, ' + + 'instead of
, use
.' + ) + } + } el.staticStyle = JSON.stringify(staticStyle) } diff --git a/src/platforms/web/util/style.js b/src/platforms/web/util/style.js index e97f6065f7e..d09f56ae2dd 100644 --- a/src/platforms/web/util/style.js +++ b/src/platforms/web/util/style.js @@ -3,14 +3,14 @@ import { cached, extend, toObject } from 'shared/util' const parseStyleText = cached(function (cssText) { - var rs = {} + const rs = {} if (!cssText) { return rs } - var hasBackground = cssText.indexOf('background') >= 0 + const hasBackground = cssText.indexOf('background') >= 0 // maybe with background-image: url(http://xxx) or base64 img - var listDelimiter = hasBackground ? /;(?![^(]*\))/g : ';' - var propertyDelimiter = hasBackground ? /:(.+)/ : ':' + const listDelimiter = hasBackground ? /;(?![^(]*\))/g : ';' + const propertyDelimiter = hasBackground ? /:(.+)/ : ':' cssText.split(listDelimiter).forEach(function (item) { if (item) { var tmp = item.split(propertyDelimiter)