3

What am I trying to do

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.

What I have done

Expected view

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>
`)

What is not working

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?

4
  • "I give a string with date in it." where does string come from? How are you finding it? When is the JS method running – does it run on a specific user event, or is it called from a component method? Would you be able to share the code for the entire component? Commented Nov 22, 2017 at 10:54
  • @wing as you can see in the picture. its a modal box with text area. User will enter the message. and when user clicks on the load button, i invoke the method. and it will show the output as in the picture. Commented Nov 22, 2017 at 10:56
  • If I understand it correctly, you are trying to render a string submitted by the user with HTML wrapped around pieces of information captured by RegEx patterns? Commented Nov 22, 2017 at 11:01
  • @wing yes exactly Commented Nov 22, 2017 at 11:17

1 Answer 1

6

Problem

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’s innerHTML. 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 using v-html, try to rethink the solution by using components instead.

Vue API documentation on the v-html directive

Solution

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:

  1. Tokenize the string by splitting it by whitespace characters.
  2. Build up an array of elements to be rendered by iterating through each token and
  3. Checking if the token matches a rule (in our naive implementation seeing if the string matches 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.
  4. Accumulating the result of each iteration in an array.
  5. The array of Step 3 is then passed to 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:

  • Handling a variety of whitespace characters, such as newlines (\n).
  • Handling multiple occurrences of whitespace characters, such as (\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.

Further reading

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.