user2058653
user2058653

Reputation: 733

Dynamic html elements in Vue.js

How is it possible to add elements dynamically to the content? Example below:

<template>
    {{{ message | hashTags }}}
</template>

<script>
    export default {
        ...

        filters: {
            hashTags: function(value) {
                // Replace hash tags with links
                return value.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>')
            }
        }
    }
</script>

Problem is that if I press the link no action will fire. Vue do not see new elements.

Upvotes: 24

Views: 59970

Answers (6)

Roy J
Roy J

Reputation: 43881

Update: Based on this answer, you can do a similar dynamic-template component in Vue 2. You can actually set up the component spec in the computed section and bind it using :is

var v = new Vue({
  el: '#vue',
  data: {
    message: 'hi #linky'
  },
  computed: {
    dynamicComponent: function() {
      return {
        template: `<div>${this.hashTags(this.message)}</div>`,
        methods: {
          someAction() {
            console.log("Action!");
          }
        }
      }
    }
  },
  methods: {
    hashTags: function(value) {
      // Replace hash tags with links
      return value.replace(/#(\S*)/g, '<a v-on:click="someAction">#$1</a>')
    }
  }
});

setTimeout(() => {
  v.message = 'another #thing';
}, 2000);
<script src="//unpkg.com/vue@latest/dist/vue.js"></script>
<div id="vue">
  <component :is="dynamicComponent" />
</div>

Vue bindings don't happen on interpolated HTML. You need something Vue sees as a template, like a partial. However, Vue only applies bindings to a partial once; you can't go back and change the template text and have it re-bind. So each time the template text changes, you have to create a new partial.

There is a <partial> tag/element you can put in your HTML, and it accepts a variable name, so the procedure is:

  • the template HTML changes
  • register new partial name for the new template HTML
  • update name variable so the new partial is rendered

It's a little bit horrible to register something new every time there's a change, so it would be preferable to use a component with a more structured template if possible, but if you really need completely dynamic HTML with bindings, it works.

The example below starts out with one message, link-ified as per your filter, and after two seconds, changes message.

You can just use message as the name of the partial for registering, but you need a computed that returns that name after doing the registering, otherwise it would try to render before the name was registered.

var v = new Vue({
  el: 'body',
  data: {
    message: 'hi #linky'
  },
  computed: {
    partialName: function() {
      Vue.partial(this.message, this.hashTags(this.message));
      return this.message;
    }
  },
  methods: {
    someAction: function() {
      console.log('Action!');
    },
    hashTags: function(value) {
      // Replace hash tags with links
      return value.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>')
    }
  }
});

setTimeout(() => {
  v.$set('message', 'another #thing');
}, 2000);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<partial :name="partialName"></partial>

Upvotes: 17

Richard
Richard

Reputation: 161

Since partial has been removed from VueJS 2 (https://v2.vuejs.org/v2/guide/migration.html#Vue-partial-removed)

A better way may be to create a component which processes its content and create appropriate DOM elements

The above component will replace hashtags by clickable links

<process-text>Hi #hashtag !</process-text>
Vue.component('process-text', {
    render: function (createElement) {
        var hashtagRegex = /(^|\W)(#[a-z\d][\w-]*)/ig
        var text = this.$slots.default[0].text
        var list = text.split(hashtagRegex)
        var children = []
        for (var i = 0; i < list.length; i++) {
            var element = list[i]
            if (element.match(hashtagRegex)) {
                children.push(createElement('a', {
                attrs: {
                    href: 'https://www.google.fr/search?q=' + element,
                    target: "_blank"
                    },
                domProps: {
                    innerHTML: element
                    }
                }))
            } else {
                children.push(element)
            }
        }
    }
    return createElement('p', {}, children)  // VueJS expects root element
})

Upvotes: 1

Pavel
Pavel

Reputation: 5876

In Vue.js 2 it's easier:

new Vue({
    ...,
    computed: {
        inner_html() {
            return ...; // any raw html
        },
    },
    template: `<div v-html='inner_html'></div>`,
});

Upvotes: 5

Pavel
Pavel

Reputation: 5876

Modified version of @RoyJ's answer, works in Vue.js v2.6.10

new Vue({
    ...,
    computed: {
        inner_html() {
            return ...; // any raw html
        },
    },
    directives: {
        dynamic: {
            bind(el, binding) {
                el.innerHTML = binding.value;
            },
            update(el, binding) {
                el.innerHTML = binding.value;
            },
        },
    },
    template: `<div v-dynamic='inner_html'></div>`,
});

Upvotes: 1

MadisonTrash
MadisonTrash

Reputation: 5614

The best solution I found which works fine with custom html is looks like this, it's like you kind of create new component each times the html property changes. No actually one did this, we just use computed property for creating new component.

That is how it looks:

new Vue({
  el: "#root",
  data: {
      value: '',
      name: 'root',
      htmlData: '<div><input @input="onInputProxy($event)" ' +
                            'v-model="value" ' + 
                            'v-for="i in 3" ' + 
                            ':ref="`customInput${i}`"></div>'
  },
  computed: {
    // our component is computed property which returns the dict
    htmlDataComponent () {
      return {
        template: this.htmlData, // we use htmlData as template text

        data() {
          return {
            name: 'component',
            value: ''
          }
        },
        created () {
          // value of "this" is formComponent
          console.log(this.name + ' created');
        },
        methods: {
          // proxy components method to parent method,
          // actually you done have to
          onInputProxy: this.onInput
        }
      }
    }
  },
  methods: {
    onInput ($event) {
      // while $event is proxied from dynamic formComponent
      // value of "this" is parent component
      console.log(this.name + ' onInput');

      // use refs to refer to real components value
      console.log(this.$refs.htmlDataComponent.value);
      console.log(this.$refs.htmlDataComponent.$refs.customInput1);
      console.log(this.$refs.htmlDataComponent.$refs.customInput2);
      console.log(this.$refs.htmlDataComponent.$refs.customInput3);
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js">
</script>

<div id="root">
  <component ref="htmlDataComponent" 
             v-if="htmlData"
             :is="htmlDataComponent"></component>
</div>

I did not check it for memory efficiency, but it looks like works just fine.

Upvotes: 2

Roy J
Roy J

Reputation: 43881

I just learned about $compile, and it seems to fit your need very nicely. A very simple directive using $compile avoids all the registrations.

Vue.directive('dynamic', function(newValue) {
    this.el.innerHTML = newValue;
    this.vm.$compile(this.el);
});

var v = new Vue({
  el: 'body',
  data: {
    message: 'hi #linky'
  },
  computed: {
    messageAsHtml: function() {
      return this.message.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>');
    }
  },
  methods: {
    someAction: function() {
      console.log('Action!');
    }
  }
});

setTimeout(() => {
  v.$set('message', 'another #thing');
}, 2000);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div v-dynamic="messageAsHtml"></div>

Upvotes: 13

Related Questions