Skip to content

Commit

Permalink
feat: scoped CSS support for functional components
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Oct 11, 2017
1 parent ea0d227 commit 050bb33
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 16 deletions.
36 changes: 29 additions & 7 deletions src/core/vdom/create-functional-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { installRenderHelpers } from '../instance/render-helpers/index'

import {
isDef,
isTrue,
camelize,
emptyObject,
validateProp
Expand All @@ -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)
Expand All @@ -58,25 +80,25 @@ 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,
children,
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
if (data.slot) {
(vnode.data || (vnode.data = {})).slot = data.slot
}
}

return vnode
}

Expand Down
15 changes: 10 additions & 5 deletions src/core/vdom/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '')
Expand Down
1 change: 1 addition & 0 deletions src/core/vdom/vnode.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default class VNode {
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnScopeId: ?string;

constructor (
tag?: string,
Expand Down
12 changes: 8 additions & 4 deletions src/server/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '>'
}
Expand Down
21 changes: 21 additions & 0 deletions test/unit/features/options/_scopeId.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<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)
})
})

0 comments on commit 050bb33

Please sign in to comment.