From 050bb33f9b02589357c037623ea8cbf8ff13555b Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 11 Oct 2017 10:38:46 -0400 Subject: [PATCH] feat: scoped CSS support for functional components --- src/core/vdom/create-functional-component.js | 36 ++++++++++++++++---- src/core/vdom/patch.js | 15 +++++--- src/core/vdom/vnode.js | 1 + src/server/render.js | 12 ++++--- test/unit/features/options/_scopeId.spec.js | 21 ++++++++++++ 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/core/vdom/create-functional-component.js b/src/core/vdom/create-functional-component.js index 74c36ea7b90..b3d1c967c3d 100644 --- a/src/core/vdom/create-functional-component.js +++ b/src/core/vdom/create-functional-component.js @@ -8,6 +8,7 @@ import { installRenderHelpers } from '../instance/render-helpers/index' import { isDef, + isTrue, camelize, emptyObject, validateProp @@ -28,14 +29,35 @@ function FunctionalRenderContext ( this.listeners = data.on || emptyObject this.injections = resolveInject(options.inject, 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 - 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.$options = options - this._c = parent._c + // pre-resolve slots for renderSlot() this.$slots = this.slots() 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) @@ -58,10 +80,7 @@ export function createFunctionalComponent ( if (isDef(data.attrs)) mergeProps(props, data.attrs) 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( data, props, @@ -69,7 +88,9 @@ export function createFunctionalComponent ( contextVm, Ctor ) - const vnode = options.render.call(null, h, renderContext) + + const vnode = options.render.call(null, renderContext._c, renderContext) + if (vnode instanceof VNode) { vnode.functionalContext = contextVm vnode.functionalOptions = options @@ -77,6 +98,7 @@ export function createFunctionalComponent ( (vnode.data || (vnode.data = {})).slot = data.slot } } + return vnode } diff --git a/src/core/vdom/patch.js b/src/core/vdom/patch.js index 65764b6360f..7358b7430de 100644 --- a/src/core/vdom/patch.js +++ b/src/core/vdom/patch.js @@ -281,16 +281,21 @@ export function createPatchFunction (backend) { // of going through the normal attribute patching process. function setScope (vnode) { 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. if (isDef(i = activeInstance) && i !== vnode.context && + i !== vnode.functionalContext && isDef(i = i.$options._scopeId) ) { nodeOps.setAttribute(vnode.elm, i, '') diff --git a/src/core/vdom/vnode.js b/src/core/vdom/vnode.js index b58664cdc77..3b19a9b4d27 100644 --- a/src/core/vdom/vnode.js +++ b/src/core/vdom/vnode.js @@ -23,6 +23,7 @@ export default class VNode { asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; + fnScopeId: ?string; constructor ( tag?: string, diff --git a/src/server/render.js b/src/server/render.js index 8be73cf3f6d..0ca49f7c2ba 100644 --- a/src/server/render.js +++ b/src/server/render.js @@ -342,11 +342,15 @@ function renderStartingTag (node: VNode, context) { ) { 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 + '>' } diff --git a/test/unit/features/options/_scopeId.spec.js b/test/unit/features/options/_scopeId.spec.js index a01327a541c..be13b9dc22c 100644 --- a/test/unit/features/options/_scopeId.spec.js +++ b/test/unit/features/options/_scopeId.spec.js @@ -68,4 +68,25 @@ describe('Options _scopeId', () => { expect(child.$el.hasAttribute('data-2')).toBe(true) }).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: '
' + }).$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) + }) })