return n0
}"
`;
+
+exports[`v-html > work with component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, setBlockHtml as _setBlockHtml, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+ const _component_Comp = _resolveComponent("Comp")
+ const n0 = _createComponentWithFallback(_component_Comp, null, null, true)
+ _renderEffect(() => _setBlockHtml(n0, _ctx.foo))
+ return n0
+}"
+`;
+
+exports[`v-html > work with dynamic component 1`] = `
+"import { createDynamicComponent as _createDynamicComponent, setBlockHtml as _setBlockHtml, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+ const n0 = _createDynamicComponent(() => (_ctx.Comp), null, null, true)
+ _renderEffect(() => _setBlockHtml(n0, _ctx.foo))
+ return n0
+}"
+`;
return n0
}"
`;
+
+exports[`v-text > work with component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, toDisplayString as _toDisplayString, setBlockText as _setBlockText, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+ const _component_Comp = _resolveComponent("Comp")
+ const n0 = _createComponentWithFallback(_component_Comp, null, null, true)
+ _renderEffect(() => _setBlockText(n0, _toDisplayString(_ctx.foo)))
+ return n0
+}"
+`;
+
+exports[`v-text > work with dynamic component 1`] = `
+"import { createDynamicComponent as _createDynamicComponent, toDisplayString as _toDisplayString, setBlockText as _setBlockText, renderEffect as _renderEffect } from 'vue';
+
+export function render(_ctx) {
+ const n0 = _createDynamicComponent(() => (_ctx.Comp), null, null, true)
+ _renderEffect(() => _setBlockText(n0, _toDisplayString(_ctx.foo)))
+ return n0
+}"
+`;
expect(code).matchSnapshot()
})
+ test('work with dynamic component', () => {
+ const { code } = compileWithVHtml(`<component :is="Comp" v-html="foo"/>`)
+ expect(code).matchSnapshot()
+ expect(code).contains('setBlockHtml(n0, _ctx.foo))')
+ })
+
+ test('work with component', () => {
+ const { code } = compileWithVHtml(`<Comp v-html="foo"/>`)
+ expect(code).matchSnapshot()
+ expect(code).contains('setBlockHtml(n0, _ctx.foo))')
+ })
+
test('should raise error and ignore children when v-html is present', () => {
const onError = vi.fn()
const { code, ir, helpers } = compileWithVHtml(
expect(code).matchSnapshot()
})
+ test('work with dynamic component', () => {
+ const { code } = compileWithVText(`<component :is="Comp" v-text="foo"/>`)
+ expect(code).matchSnapshot()
+ expect(code).contains('setBlockText(n0, _toDisplayString(_ctx.foo))')
+ })
+
+ test('work with component', () => {
+ const { code } = compileWithVText(`<Comp v-text="foo"/>`)
+ expect(code).matchSnapshot()
+ expect(code).contains('setBlockText(n0, _toDisplayString(_ctx.foo))')
+ })
+
test('should raise error and ignore children when v-text is present', () => {
const onError = vi.fn()
const { code, ir } = compileWithVText(`<div v-text="test">hello</div>`, {
context: CodegenContext,
): CodeFragment[] {
const { helper } = context
- const { value, element } = oper
+
+ const { value, element, isComponent } = oper
return [
NEWLINE,
- ...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)),
+ ...genCall(
+ // use setBlockHtml for component
+ isComponent ? helper('setBlockHtml') : helper('setHtml'),
+ `n${element}`,
+ genExpression(value, context),
+ ),
]
}
context: CodegenContext,
): CodeFragment[] {
const { helper } = context
- const { element, values, generated, jsx } = oper
+ const { element, values, generated, jsx, isComponent } = oper
const texts = combineValues(values, context, jsx)
return [
NEWLINE,
- ...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts),
+ ...genCall(
+ // use setBlockText for component
+ isComponent ? helper('setBlockText') : helper('setText'),
+ `${generated && !isComponent ? 'x' : 'n'}${element}`,
+ texts,
+ ),
]
}
values: SimpleExpressionNode[]
generated?: boolean // whether this is a generated empty text node by `processTextLikeContainer`
jsx?: boolean
+ isComponent?: boolean
}
export type KeyOverride = [find: string, replacement: string]
type: IRNodeTypes.SET_HTML
element: number
value: SimpleExpressionNode
+ isComponent?: boolean
}
export interface SetTemplateRefIRNode extends BaseIRNode {
type: IRNodeTypes.SET_HTML,
element: context.reference(),
value: exp,
+ isComponent: node.tagType === 1,
})
}
-import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
+import {
+ DOMErrorCodes,
+ ElementTypes,
+ createDOMCompilerError,
+} from '@vue/compiler-dom'
import { IRNodeTypes } from '../ir'
import { EMPTY_EXPRESSION } from './utils'
import type { DirectiveTransform } from '../transform'
context.childrenTemplate = [String(literal)]
} else {
context.childrenTemplate = [' ']
- context.registerOperation({
- type: IRNodeTypes.GET_TEXT_CHILD,
- parent: context.reference(),
- })
+ const isComponent = node.tagType === ElementTypes.COMPONENT
+ if (!isComponent) {
+ context.registerOperation({
+ type: IRNodeTypes.GET_TEXT_CHILD,
+ parent: context.reference(),
+ })
+ }
context.registerEffect([exp], {
type: IRNodeTypes.SET_TEXT,
element: context.reference(),
values: [exp],
generated: true,
+ isComponent,
})
}
}
-import { NOOP } from '@vue/shared'
+import { NOOP, toDisplayString } from '@vue/shared'
import {
setDynamicProp as _setDynamicProp,
setAttr,
+ setBlockHtml,
+ setBlockText,
setClass,
setDynamicProps,
setElementText,
setValue,
} from '../../src/dom/prop'
import { setStyle } from '../../src/dom/prop'
-import { VaporComponentInstance } from '../../src/component'
+import { VaporComponentInstance, createComponent } from '../../src/component'
import { ref, setCurrentInstance } from '@vue/runtime-dom'
+import { makeRender } from '../_utils'
+import {
+ createDynamicComponent,
+ defineVaporComponent,
+ renderEffect,
+ template,
+} from '../../src'
let removeComponentInstance = NOOP
beforeEach(() => {
removeComponentInstance()
})
+const define = makeRender()
+
describe('patchProp', () => {
describe('setClass', () => {
test('should set class', () => {
expect(el.innerHTML).toBe('<p>bar</p>')
})
})
+
+ describe('setBlockText', () => {
+ test('with dynamic component', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return template('<div>child</div>', true)()
+ },
+ })
+ const value = ref('foo')
+ const { html } = define({
+ setup() {
+ const n1 = createDynamicComponent(() => Comp, null, null, true)
+ renderEffect(() => setBlockText(n1, toDisplayString(value)))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<div>foo</div><!--dynamic-component-->')
+ })
+
+ test('with dynamic component with fallback', async () => {
+ const value = ref('foo')
+ const { html } = define({
+ setup() {
+ const n1 = createDynamicComponent(() => 'button', null, null, true)
+ renderEffect(() => setBlockText(n1, toDisplayString(value)))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<button>foo</button><!--dynamic-component-->')
+ })
+
+ test('with component', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return template('<div>child</div>', true)()
+ },
+ })
+ const value = ref('foo')
+ const { html } = define({
+ setup() {
+ const n1 = createComponent(Comp, null, null, true)
+ renderEffect(() => setBlockText(n1, toDisplayString(value)))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<div>foo</div>')
+ })
+
+ test('with component renders multiple roots nodes', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return [
+ template('<div>child</div>')(),
+ template('<div>child</div>')(),
+ ]
+ },
+ })
+ const value = ref('foo')
+ const { html } = define({
+ setup() {
+ const n1 = createComponent(Comp, null, null, true)
+ renderEffect(() => setBlockText(n1, toDisplayString(value)))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<div>child</div><div>child</div>')
+ expect('Extraneous non-props attributes (textContent)').toHaveBeenWarned()
+ })
+
+ test('with component renders text node', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return template('child')()
+ },
+ })
+ const value = ref('foo')
+ const { html } = define({
+ setup() {
+ const n1 = createComponent(Comp, null, null, true)
+ renderEffect(() => setBlockText(n1, toDisplayString(value)))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('child')
+ expect('Extraneous non-props attributes (textContent)').toHaveBeenWarned()
+ })
+ })
+
+ describe('setBlockHtml', () => {
+ test('with dynamic component', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return template('<div>child</div>', true)()
+ },
+ })
+ const value = ref('<p>foo</p>')
+ const { html } = define({
+ setup() {
+ const n1 = createDynamicComponent(() => Comp, null, null, true)
+ renderEffect(() => setBlockHtml(n1, value.value))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<div><p>foo</p></div><!--dynamic-component-->')
+ })
+
+ test('with dynamic component with fallback', async () => {
+ const value = ref('<p>foo</p>')
+ const { html } = define({
+ setup() {
+ const n1 = createDynamicComponent(() => 'button', null, null, true)
+ renderEffect(() => setBlockHtml(n1, value.value))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<button><p>foo</p></button><!--dynamic-component-->')
+ })
+
+ test('with component', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return template('<div>child</div>', true)()
+ },
+ })
+ const value = ref('<p>foo</p>')
+ const { html } = define({
+ setup() {
+ const n1 = createComponent(Comp, null, null, true)
+ renderEffect(() => setBlockHtml(n1, value.value))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<div><p>foo</p></div>')
+ })
+
+ test('with component renders multiple roots', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return [
+ template('<div>child</div>')(),
+ template('<div>child</div>')(),
+ ]
+ },
+ })
+ const value = ref('<p>foo</p>')
+ const { html } = define({
+ setup() {
+ const n1 = createComponent(Comp, null, null, true)
+ renderEffect(() => setBlockHtml(n1, value.value))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('<div>child</div><div>child</div>')
+ expect('Extraneous non-props attributes (innerHTML)').toHaveBeenWarned()
+ })
+
+ test('with component renders text node', async () => {
+ const Comp = defineVaporComponent({
+ setup() {
+ return template('child')()
+ },
+ })
+ const value = ref('<p>foo</p>')
+ const { html } = define({
+ setup() {
+ const n1 = createComponent(Comp, null, null, true)
+ renderEffect(() => setBlockHtml(n1, value.value))
+ return n1
+ },
+ }).render()
+
+ expect(html()).toBe('child')
+ expect('Extraneous non-props attributes (innerHTML)').toHaveBeenWarned()
+ })
+ })
})
import {
type NormalizedStyle,
canSetValueDirectly,
+ isArray,
isOn,
isString,
normalizeClass,
import {
type VaporComponentInstance,
isApplyingFallthroughProps,
+ isVaporComponent,
} from '../component'
+import type { Block } from '../block'
type TargetElement = Element & {
$root?: true
}
}
+export function setBlockText(
+ block: Block & { $txt?: string },
+ value: unknown,
+): void {
+ value = value == null ? '' : value
+ if (block.$txt !== value) {
+ setTextToBlock(block, (block.$txt = value as string))
+ }
+}
+
+/**
+ * dev only
+ */
+function warnCannotSetProp(prop: string): void {
+ warn(
+ `Extraneous non-props attributes (` +
+ `${prop}) ` +
+ `were passed to component but could not be automatically inherited ` +
+ `because component renders text or multiple root nodes.`,
+ )
+}
+
+function setTextToBlock(block: Block, value: any): void {
+ if (block instanceof Node) {
+ if (block instanceof Element) {
+ block.textContent = value
+ } else if (__DEV__) {
+ warnCannotSetProp('textContent')
+ }
+ } else if (isVaporComponent(block)) {
+ setTextToBlock(block.block, value)
+ } else if (isArray(block)) {
+ if (__DEV__) {
+ warnCannotSetProp('textContent')
+ }
+ } else {
+ setTextToBlock(block.nodes, value)
+ }
+}
+
export function setHtml(el: TargetElement, value: any): void {
value = value == null ? '' : unsafeToTrustedHTML(value)
if (el.$html !== value) {
}
}
+export function setBlockHtml(
+ block: Block & { $html?: string },
+ value: any,
+): void {
+ value = value == null ? '' : value
+ if (block.$html !== value) {
+ setHtmlToBlock(block, (block.$html = value))
+ }
+}
+
+function setHtmlToBlock(block: Block, value: any): void {
+ if (block instanceof Node) {
+ if (block instanceof Element) {
+ block.innerHTML = value
+ } else if (__DEV__) {
+ warnCannotSetProp('innerHTML')
+ }
+ } else if (isVaporComponent(block)) {
+ setHtmlToBlock(block.block, value)
+ } else if (isArray(block)) {
+ if (__DEV__) {
+ warnCannotSetProp('innerHTML')
+ }
+ } else {
+ setHtmlToBlock(block.nodes, value)
+ }
+}
+
export function setDynamicProps(el: any, args: any[]): void {
const props = args.length > 1 ? mergeProps(...args) : args[0]
const cacheKey = `$dprops${isApplyingFallthroughProps ? '$' : ''}`
export { createTextNode, child, nthChild, next } from './dom/node'
export {
setText,
+ setBlockText,
setHtml,
+ setBlockHtml,
setClass,
setStyle,
setAttr,
setProp,
setDOMProp,
setDynamicProps,
+ setElementText,
} from './dom/prop'
export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
export { createIf } from './apiCreateIf'