Parcourir la source

enable style merge behavior between parent-child components (fix #3997) (#4138)

* merge style between components

* update test case

* update style compiler

* add paren to style binding code

* update background property parsing

* introduce interpolation warning and refactor var to const
chengchao il y a 9 ans
Parent
commit
e960cd104e

+ 1 - 0
flow/compiler.js

@@ -106,6 +106,7 @@ declare type ASTElement = {
 
   staticClass?: string;
   classBinding?: string;
+  staticStyle?: string;
   styleBinding?: string;
   events?: ASTElementHandlers;
   nativeEvents?: ASTElementHandlers;

+ 1 - 0
flow/vnode.js

@@ -37,6 +37,7 @@ declare interface VNodeData {
   tag?: string;
   staticClass?: string;
   class?: any;
+  staticStyle?: string;
   style?: Array<Object> | Object;
   props?: { [key: string]: any };
   attrs?: { [key: string]: string };

+ 31 - 5
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 <div style="{{ val }}">, use <div :style="val">.'
+        )
+      }
+    }
+    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
 }

+ 17 - 22
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 {

+ 12 - 37
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)}`
   }
 }

+ 66 - 0
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
+}
+

+ 4 - 4
test/ssr/ssr-string.spec.js

@@ -102,7 +102,7 @@ describe('SSR: renderToString', () => {
       }
     }, result => {
       expect(result).toContain(
-        '<div server-rendered="true" style="font-size:14px;color:red;background-color:black"></div>'
+        '<div server-rendered="true" style="background-color:black;font-size:14px;color:red"></div>'
       )
       done()
     })
@@ -143,13 +143,13 @@ describe('SSR: renderToString', () => {
 
   it('nested custom component style', done => {
     renderVmWithOptions({
-      template: '<comp :style="style"></comp>',
+      template: '<comp style="color: blue" :style="style"></comp>',
       data: {
         style: 'color:red'
       },
       components: {
         comp: {
-          template: '<nested style="font-size:520rem"></nested>',
+          template: '<nested style="text-align: left;" :style="{fontSize:\'520rem\'}"></nested>',
           components: {
             nested: {
               template: '<div></div>'
@@ -159,7 +159,7 @@ describe('SSR: renderToString', () => {
       }
     }, result => {
       expect(result).toContain(
-        '<div server-rendered="true" style="color:red;font-size:520rem"></div>'
+        '<div server-rendered="true" style="text-align:left;font-size:520rem;color:red"></div>'
       )
       done()
     })

+ 106 - 0
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: '<div style="background: url(https://vuejs.org/images/logo.png);color: blue" :style="test"></div>',
+      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: '<child style="text-align: left;margin-right:20px" :style="test"></child>',
+      data: {
+        test: { color: 'red', fontSize: '12px' }
+      },
+      components: {
+        child: {
+          template: '<div style="margin-right:10px;" :style="{marginLeft: marginLeft}"></div>',
+          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: '<child :style="test"></child>',
+      data: {
+        test: { color: 'red', fontSize: '12px' }
+      },
+      components: {
+        child: {
+          template: '<div><nested ref="nested" style="color: blue;text-align:left"></nested></div>',
+          components: {
+            nested: {
+              template: '<div></div>'
+            }
+          }
+        }
+      }
+    }).$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: '<child :style="test"></child>',
+      data: {
+        test: { color: 'red', fontSize: '12px' }
+      },
+      components: {
+        child: {
+          template: '<nested style="color: blue;text-align:left"></nested>',
+          components: {
+            nested: {
+              template: '<div style="margin-left: 12px;" :style="nestedStyle"></div>',
+              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)
+  })
 })

+ 1 - 0
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 };