Răsfoiți Sursa

feat: scoped CSS support for functional components

Evan You 8 ani în urmă
părinte
comite
050bb33f9b

+ 29 - 7
src/core/vdom/create-functional-component.js

@@ -8,6 +8,7 @@ import { installRenderHelpers } from '../instance/render-helpers/index'
 
 
 import {
 import {
   isDef,
   isDef,
+  isTrue,
   camelize,
   camelize,
   emptyObject,
   emptyObject,
   validateProp
   validateProp
@@ -28,14 +29,35 @@ function FunctionalRenderContext (
   this.listeners = data.on || emptyObject
   this.listeners = data.on || emptyObject
   this.injections = resolveInject(options.inject, parent)
   this.injections = resolveInject(options.inject, parent)
   this.slots = () => resolveSlots(children, parent)
   this.slots = () => resolveSlots(children, parent)
+
+  // ensure the createElement function in functional components
+  // gets a unique context - this is necessary for correct named slot check
+  const contextVm = Object.create(parent)
+  const isCompiled = isTrue(options._compiled)
+  const needNormalization = !isCompiled
+
   // support for compiled functional template
   // support for compiled functional template
-  if (options._compiled) {
+  if (isCompiled) {
+    // exposing constructor and $options for renderStatic() because it needs
+    // to cache the rendered trees on shared options
     this.constructor = Ctor
     this.constructor = Ctor
     this.$options = options
     this.$options = options
-    this._c = parent._c
+    // pre-resolve slots for renderSlot()
     this.$slots = this.slots()
     this.$slots = this.slots()
     this.$scopedSlots = data.scopedSlots || emptyObject
     this.$scopedSlots = data.scopedSlots || emptyObject
   }
   }
+
+  if (options._scopeId) {
+    this._c = (a, b, c, d) => {
+      const vnode: ?VNode = createElement(contextVm, a, b, c, d, needNormalization)
+      if (vnode) {
+        vnode.fnScopeId = options._scopeId
+      }
+      return vnode
+    }
+  } else {
+    this._c = (a, b, c, d) => createElement(contextVm, a, b, c, d, needNormalization)
+  }
 }
 }
 
 
 installRenderHelpers(FunctionalRenderContext.prototype)
 installRenderHelpers(FunctionalRenderContext.prototype)
@@ -58,10 +80,7 @@ export function createFunctionalComponent (
     if (isDef(data.attrs)) mergeProps(props, data.attrs)
     if (isDef(data.attrs)) mergeProps(props, data.attrs)
     if (isDef(data.props)) mergeProps(props, data.props)
     if (isDef(data.props)) mergeProps(props, data.props)
   }
   }
-  // ensure the createElement function in functional components
-  // gets a unique context - this is necessary for correct named slot check
-  const _contextVm = Object.create(contextVm)
-  const h = (a, b, c, d) => createElement(_contextVm, a, b, c, d, true)
+
   const renderContext = new FunctionalRenderContext(
   const renderContext = new FunctionalRenderContext(
     data,
     data,
     props,
     props,
@@ -69,7 +88,9 @@ export function createFunctionalComponent (
     contextVm,
     contextVm,
     Ctor
     Ctor
   )
   )
-  const vnode = options.render.call(null, h, renderContext)
+
+  const vnode = options.render.call(null, renderContext._c, renderContext)
+
   if (vnode instanceof VNode) {
   if (vnode instanceof VNode) {
     vnode.functionalContext = contextVm
     vnode.functionalContext = contextVm
     vnode.functionalOptions = options
     vnode.functionalOptions = options
@@ -77,6 +98,7 @@ export function createFunctionalComponent (
       (vnode.data || (vnode.data = {})).slot = data.slot
       (vnode.data || (vnode.data = {})).slot = data.slot
     }
     }
   }
   }
+
   return vnode
   return vnode
 }
 }
 
 

+ 10 - 5
src/core/vdom/patch.js

@@ -281,16 +281,21 @@ export function createPatchFunction (backend) {
   // of going through the normal attribute patching process.
   // of going through the normal attribute patching process.
   function setScope (vnode) {
   function setScope (vnode) {
     let i
     let i
-    let ancestor = vnode
-    while (ancestor) {
-      if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {
-        nodeOps.setAttribute(vnode.elm, i, '')
+    if (isDef(i = vnode.fnScopeId)) {
+      nodeOps.setAttribute(vnode.elm, i, '')
+    } else {
+      let ancestor = vnode
+      while (ancestor) {
+        if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {
+          nodeOps.setAttribute(vnode.elm, i, '')
+        }
+        ancestor = ancestor.parent
       }
       }
-      ancestor = ancestor.parent
     }
     }
     // for slot content they should also get the scopeId from the host instance.
     // for slot content they should also get the scopeId from the host instance.
     if (isDef(i = activeInstance) &&
     if (isDef(i = activeInstance) &&
       i !== vnode.context &&
       i !== vnode.context &&
+      i !== vnode.functionalContext &&
       isDef(i = i.$options._scopeId)
       isDef(i = i.$options._scopeId)
     ) {
     ) {
       nodeOps.setAttribute(vnode.elm, i, '')
       nodeOps.setAttribute(vnode.elm, i, '')

+ 1 - 0
src/core/vdom/vnode.js

@@ -23,6 +23,7 @@ export default class VNode {
   asyncMeta: Object | void;
   asyncMeta: Object | void;
   isAsyncPlaceholder: boolean;
   isAsyncPlaceholder: boolean;
   ssrContext: Object | void;
   ssrContext: Object | void;
+  fnScopeId: ?string;
 
 
   constructor (
   constructor (
     tag?: string,
     tag?: string,

+ 8 - 4
src/server/render.js

@@ -342,11 +342,15 @@ function renderStartingTag (node: VNode, context) {
   ) {
   ) {
     markup += ` ${(scopeId: any)}`
     markup += ` ${(scopeId: any)}`
   }
   }
-  while (isDef(node)) {
-    if (isDef(scopeId = node.context.$options._scopeId)) {
-      markup += ` ${scopeId}`
+  if (isDef(node.fnScopeId)) {
+    markup += ` ${node.fnScopeId}`
+  } else {
+    while (isDef(node)) {
+      if (isDef(scopeId = node.context.$options._scopeId)) {
+        markup += ` ${scopeId}`
+      }
+      node = node.parent
     }
     }
-    node = node.parent
   }
   }
   return markup + '>'
   return markup + '>'
 }
 }

+ 21 - 0
test/unit/features/options/_scopeId.spec.js

@@ -68,4 +68,25 @@ describe('Options _scopeId', () => {
       expect(child.$el.hasAttribute('data-2')).toBe(true)
       expect(child.$el.hasAttribute('data-2')).toBe(true)
     }).then(done)
     }).then(done)
   })
   })
+
+  it('should work on functional components', () => {
+    const child = {
+      functional: true,
+      _scopeId: 'child',
+      render (h) {
+        return h('div', { class: 'child' }, 'child')
+      }
+    }
+    const vm = new Vue({
+      _scopeId: 'parent',
+      components: { child },
+      template: '<div><child></child></div>'
+    }).$mount()
+
+    expect(vm.$el.hasAttribute('parent')).toBe(true)
+    const childEl = vm.$el.querySelector('.child')
+    expect(childEl.hasAttribute('child')).toBe(true)
+    // functional component with scopeId will not inherit parent scopeId
+    expect(childEl.hasAttribute('parent')).toBe(false)
+  })
 })
 })