skip to Main Content

I have a use case (below) where I need to mount (if thats the correct term) a Vue.js component template that was inserted into the DOM via jQuery, I can setup a Mutation Observer or react to certain events that are triggered when the mutation happens.

I am using Vue.js v2

Here is a simple example I put together to illustrate the point:

live jsFiddle https://jsfiddle.net/w7q7b1bh/2/

The HTML below contains inlined-templates for two components

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<div id="app">
  <!-- The use of inline-template is required for my solution to work -->
  <simple-counter inline-template>
    <button v-bind:style="style" v-on:click="add">clicks: {{ counter }}</button>
  </simple-counter>
  <simple-counter inline-template>
    <button v-on:click="counter += 1">{{ counter }}</button>
  </simple-counter>
</div>

<button id="mutate">Mutate</button>

The js:

// simple counter component
Vue.component('simple-counter', {
  data: function() {
    return {
      counter: 0,
      style: {
        color: 'red',
        width: '200px'
      }
    }
  },
  methods: {
    add: function() {
      this.counter = this.counter + 1;
      this.style.color = this.style.color == 'red' ? 'green' : 'red';
    }
  }
})

// create the Vue instance
var initV = () => new Vue({
  el: '#app'
});

// expose the instance for later use
window.v = initV();

// click handler that will add a new `simple-counter` template to the Vue.el scope
$('#mutate').click(function(){
    $('#app').append(`  <div is="simple-counter" inline-template>
    <button v-bind:style="style" v-on:click="add">click to add: <span class="inactive" v-bind:class="{ active: true }">{{ counter }}</span></button></div>`)
    // do something after the template is incerted
    window.v.$destroy()
    window.v = initV(); // does not work

})

As mentioned in the code, destroying the re-instantiating the Vue instance does not work, I understand why, the templates for the components are changed on first Vue instantiation to their final HTML, when you try and instantiate a second time, templates are not there, components are not mounted

I’d like to be able to find the newly added components after mutation and mount only those, is that possible? and how?

UPDATE:
I was able to find a way to do it via instantiating a new Vue instance with el set to the specific mutated part of the DOM as opposed to the whole #app tree:

$('#mutate').click(function(){
    var appended = 
            $(`
       <div is="simple-counter" inline-template>
         <button v-bind:style="style" v-on:click="add">
           click to add: {{ counter }}
         </button>
       </div>`
      ).appendTo($('#app'));

    var newV = new Vue({el: appended[0]});
});

Seems to work, but also looks ugly and I am not sure what other implications this might have..

Use Case:

I am working on a way to write Vue.js components for a CMS called Adobe Experience Manager (AEM).

I write my components using inlined-template which gives me the advantage of SEO as well as server-side rendering using another templating language called HTL.

The way AEM authoring works is that, when a component is edited (via a dialog), that specific component is re-rendered on the server-side then injected back to the DOM to replace the old component, all done via Ajax and jQuery (no browser refresh).

Here is an example

AEM component template:
<button>${properties.buttonTitle}</button>

Here is what an author might do:

  1. author visits the authoring page
  2. opens the button component dialog to edit
  3. changes the buttonTitle to “new button title”
  4. Saves

upon saving, an ajax is sent, the component HTML is re-rendered on the server and returned is the new HTML. That HTML now replaces the old HTML via jQuery (mutates the DOM)

This is fine for static components, but if this was a Vue.js component, how do I dynamically mount it while keeping other components mounted.

An easy solution to this is to refresh the page… but that is just bad experience… There has to be a better way.

2

Answers


  1. Chosen as BEST ANSWER

    Thanks to @liam I was able to find an appropriate solution to my problem

    After mutating the DOM with the HTML template, keep a reference to that template's parent element

    for example:

    var $template = $('<div is="simple-counter" inline-template> ..rest of template here.. <div>').appendTo('#app') // app is the Vue instance el or a child of it
    

    Now you can create a new instance of your component and add $template to it as the el property

    if my component was:

    var simpleCounterComponent = Vue.component('simple-counter', {
      data: function() {
        return {
          counter: 0,
          style: {
            color: 'red',
            width: '200px'
          }
        }
      },
      methods: {
        add: function() {
          this.counter = this.counter + 1;
          this.style.color = this.style.color == 'red' ? 'green' : 'red';
        }
      }
    })
    

    I can do:

    var instance = new simpleCounterComponent({
      el: $template.get(0) // getting an HTML element not a jQuery object
    });
    

    And this way, that newly added template has become a Vue component

    Take a look at this fiddle for working example based on the question:

    https://jsfiddle.net/947ojvnw/11/


  2. One way to instantiate Vue components in runtime-generated HTML is:

    var ComponentClass = Vue.extend({
        template: '...',
    });
    var instance = new ComponentClass({
        propsData: { name: value },
    });
    instance.$mount('#uid'); // HTML contains <... id="uid">
    ...
    instance.$destroy(); // if HTML containing id="uid" is dropped
    

    More here (I am not affiliated with this site)
    https://css-tricks.com/creating-vue-js-component-instances-programmatically/

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search