4
votes

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
Instantiating the new section as a new Vue seems the right approach, but it seems like the bindings wouldn't work, because they're in the other Vue. However, possibly in a scoped slot they would.Roy J
is there a way to mount a components manually and then add them to the Vue instance ?Ahmed Musallam
I really don't know. It's very un-Vue to have something else modify the DOM and then tell Vue to take over the changes. It would be better if your server just served different data and the front end could do the DOM updates.Roy J
I agree completely, this will only happen on the authoring interface. For end-users, this will never happen. I think creating a new Vue instance with only the updated DOM should suffice for now.Ahmed Musallam
Might be helpful: css-tricks.com/…Liam

2 Answers

5
votes

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
votes

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/