Reputation: 724
Given a string from a user input, I am attempting to render this string with particular substrings wrapped in a component. In my particular case the substring being matched is a date that matches a regex pattern and the component that is supposed to wrap it is a chip from Vuetify.
Above is a screenshot of what I have achieved so far. The input of the textarea
is rendered below it with certain substrings wrapped in the chip component from Vuetify. The above was achieved by replacing the substring matched by a regex pattern with the HTML to render the component and giving this string to a v-html
directive for rendering. Below is some code showing how this was done.
<div style="line-height:40px;" v-html="messageOutput"></div>
let finalStr = ''
let str = 'Your a/c no. XXXXXXXXXX85 is credited on 15-11-17.'
let dateReg = /((?=\d{4})\d{4}|(?=[a-zA-Z]{3})[a-zA-Z]{3}|\d{2})((?=\/)\/|-)((?=[0-9]{2})[0-9]{2}|(?=[0-9]{1,2})[0-9]{1,2}|[a-zA-Z]{3})((?=\/)\/|-)((?=[0-9]{4})[0-9]{4}|(?=[0-9]{2})[0-9]{2}|[a-zA-Z]{3})/
const date = dateReg.exec(str)
finalStr = str.replace(date[0], `
<div class="md-chip md-chip-clickable">
<div class="md-chip-icon primary"><i class="material-icons">date_range</i></div>
${date[0]}
</div>
`)
The problem is using custom components as opposed to plain HTML does not give the expected output. The styling is not rendered and the component does not react to events.
How can I dynamically wrap a substring with a component in Vue.js?
Upvotes: 3
Views: 4952
Reputation: 9691
The problem of custom components not working as expected stems from the attempt of including them inside a v-html
directive. Due to the value of the v-html
directive being inserted as plain HTML, by setting an element's innerHTML
, data and events are not reactively bound.
Note that you cannot use
v-html
to compose template partials, because Vue is not a string-based templating engine. Instead, components are preferred as the fundamental unit for UI reuse and composition.
– Vue guide on interpolating raw HTML
[
v-html
updates] the element’sinnerHTML
. Note that the contents are inserted as plain HTML - they will not be compiled as Vue templates. If you find yourself trying to compose templates usingv-html
, try to rethink the solution by using components instead.
– Vue API documentation on the v-html
directive
Components are the fundamental units for UI reuse and composition. We now must build a component that is capable of identifying particular substrings and wrapping a component around them. Vue's components/templates and directives by themselves would not be able to handle this task – it just isn't possible. However Vue does provide a way of building components at a lower level through render functions.
With a render function we can accept a string as a prop, tokenize it and build out a view with matching substrings wrapped in a component. Below is a naive implementation of such a solution:
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/[email protected]/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/[email protected]/dist/vue.min.js"></script>
</body>
</html>
There is a SmartRenderer
component which accepts a string
through a prop. Within the render function we:
foo
) and wrapping it in a component (in our naive implementation the component is a Chip
, which just makes the foo
bold) otherwise leave the token as is.createElement
as the children of a div
element to be created.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
takes an HTML tag name, component options (or a function) as its first argument and in our case the second argument takes a child or children to render. You can read more about createElement
in the docs.
The solution as posted has some unsolved problems such as:
\n
).\s\s\s
).It is also naive in the way it checks if a token needs to be wrapped and how it wraps it – it's just an if
statement with the wrapping component hard-coded in. You could implement a prop called rules
which is an array of objects specifying a rule to test and a component to wrap the token in if the test passes. However this solution should be enough to get you started.
Upvotes: 6