问题
自定义组件无法按预期工作的问题源于尝试将它们包含在v-html 指令中。由于v-html 指令的值被插入为纯HTML,通过设置元素的innerHTML,数据和事件不会被响应绑定。
请注意,您不能使用v-html 来编写模板部分,因为 Vue 不是基于字符串的模板引擎。相反,组件更适合作为 UI 重用和组合的基本单元。
——Vue guide on interpolating raw HTML
[v-html更新]元素的innerHTML。 请注意,内容是以纯 HTML 形式插入的 - 它们不会被编译为 Vue 模板。 如果您发现自己尝试使用 v-html 编写模板,请尝试改用组件来重新考虑解决方案。
——Vue API documentation on the v-html directive
解决方案
组件是 UI 重用和组合的基本单元。我们现在必须构建一个能够识别特定子字符串并围绕它们包装组件的组件。 Vue 的组件/模板和指令本身无法处理这个任务——这是不可能的。但是 Vue 确实通过render functions 提供了一种在较低级别构建组件的方法。
使用渲染函数,我们可以接受一个字符串作为道具,对其进行标记并构建一个视图,其中包含包装在组件中的匹配子字符串。以下是此类解决方案的简单实现:
const Chip = {
template: `
<div class="chip">
<slot></slot>
</div>
`,
};
const SmartRenderer = {
props: [
'string',
],
render(createElement) {
const TOKEN_DELIMITER_REGEX = /(\s+)/;
const tokens = this.string.split(TOKEN_DELIMITER_REGEX);
const children = tokens.reduce((acc, token) => {
if (token === 'foo') return [...acc, createElement(Chip, token)];
return [...acc, token];
}, []);
return createElement('div', children);
},
};
const SmartInput = {
components: {
SmartRenderer
},
data: () => ({
value: '',
}),
template: `
<div class="smart-input">
<textarea
class="input"
v-model="value"
>
</textarea>
<SmartRenderer :string="value" />
</div>
`,
};
new Vue({
el: '#root',
components: {
SmartInput,
},
template: `
<SmartInput />
`,
data: () => ({}),
});
.chip {
display: inline-block;
font-weight: bold;
}
.smart-input .input {
font: inherit;
resize: vertical;
}
.smart-input .output {
overflow-wrap: break-word;
word-break: break-all;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="https://unpkg.com/milligram@1.3.0/dist/milligram.css">
</head>
<body>
<p>Start typing below. At some point include <strong>foo</strong>, separated from other words with at least one whitespace character.</p>
<div id="root"></div>
<script src="https://unpkg.com/vue@2.5.8/dist/vue.min.js"></script>
</body>
</html>
有一个SmartRenderer 组件通过一个道具接受string。在渲染函数中我们:
- 通过用空格字符分割字符串来标记字符串。
- 通过遍历每个标记和
- 检查令牌是否匹配规则(在我们的幼稚实现中查看字符串是否匹配
foo)并将其包装在组件中(在我们幼稚的实现中,组件是Chip,这只是使foo 粗体)否则保持令牌不变。
- 将每次迭代的结果累积到一个数组中。
- 然后将 Step 3 的数组作为要创建的
div 元素的子元素传递给 createElement。
render(createElement) {
const TOKEN_DELIMITER_REGEX = /(\s+)/;
const tokens = this.string.split(TOKEN_DELIMITER_REGEX);
const children = tokens.reduce((acc, token) => {
if (token === 'foo') return [...acc, createElement(Chip, token)];
return [...acc, token];
}, []);
return createElement('div', children);
},
createElement 将 HTML 标记名称、组件选项(或函数)作为其第一个参数,在我们的例子中,第二个参数需要一个或多个子级来呈现。您可以在docs 中阅读有关createElement 的更多信息。
发布的解决方案有一些未解决的问题,例如:
- 处理各种空白字符,例如换行符 (
\n)。
- 处理多次出现的空白字符,例如 (
\s\s\s)。
它检查令牌是否需要包装以及如何包装它的方式也很幼稚 - 它只是一个 if 语句,其中硬编码了包装组件。您可以实现一个名为 rules 的道具这是一个对象数组,指定要测试的规则和一个组件,如果测试通过,则将令牌包装在其中。但是,此解决方案应该足以让您入门。
进一步阅读