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..ad11431fe75 100644 --- a/src/platforms/web/compiler/modules/style.js +++ b/src/platforms/web/compiler/modules/style.js @@ -1,10 +1,30 @@ /* @flow */ +import { parseText } from 'compiler/parser/text-parser' import { - getBindingAttr + getAndRemoveAttr, + 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) + } + const styleBinding = getBindingAttr(el, 'style', false /* getStatic */) if (styleBinding) { el.styleBinding = styleBinding @@ -12,12 +32,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..d09f56ae2dd --- /dev/null +++ b/src/platforms/web/util/style.js @@ -0,0 +1,66 @@ +/* @flow */ + +import { cached, extend, toObject } from 'shared/util' + +const parseStyleText = cached(function (cssText) { + const rs = {} + if (!cssText) { + return rs + } + const hasBackground = cssText.indexOf('background') >= 0 + // maybe with background-image: url(http://xxx) or base64 img + const listDelimiter = hasBackground ? /;(?![^(]*\))/g : ';' + const propertyDelimiter = hasBackground ? /:(.+)/ : ':' + cssText.split(listDelimiter).forEach(function (item) { + if (item) { + var tmp = item.split(propertyDelimiter) + tmp.length > 1 && (rs[tmp[0].trim()] = tmp[1].trim()) + } + }) + 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..0d63a780aaf 100644 --- a/test/unit/features/directives/style.spec.js +++ b/test/unit/features/directives/style.spec.js @@ -166,4 +166,110 @@ 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() + 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 => { + const vm = new Vue({ + template: '', + data: { + test: { color: 'red', fontSize: '12px' } + }, + components: { + child: { + template: '
', + data: () => ({ marginLeft: '16px' }) + } + } + }).$mount() + const style = vm.$el.style + const child = vm.$children[0] + 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(style.color).toBe('blue') + child.marginLeft = '30px' + }).then(() => { + expect(style.marginLeft).toBe('30px') + child.fontSize = '30px' + }).then(() => { + expect(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() + 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') + }) + + 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 style = vm.$el.style + const child = vm.$children[0].$children[0] + 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(style.marginLeft).toBe('60px') + child.nestedStyle = { + fontSize: '14px', + marginLeft: '40px' + } + }).then(() => { + expect(style.fontSize).toBe('12px') + expect(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 };