UltraInstinct
UltraInstinct

Reputation: 44444

Vue doesn't update DOM upon 'indirectly' changing the value of an expression

TL;DR

I am trying to dynamically build a UI from JSON. The JSON represents a vue.js app with application state (variables) & UI building logic conditional on those variables.

The JSON object of "type": "switch" (see the fiddle linked below), directs the vue.js app to display one of many "cases": {"case1": {..}, "case2": {..}} depending on the value of a state variable "variable": "key" /*translates to vueApp.key */.

Changing one of the variables (update_status) leads to DOM update initially. Changing it again after mounting the app does not affect the DOM, sadly. I'm pretty sure I am doing something stupid or missing something subtle.


Slightly longer version:

(If you're still reading this, please look at the fiddle at this point. None of the below will make sense without it. Thanks!)

Vue.js Template (with app.variables.update_status = "available")

<script type="text/x-template" id="template-switch">
  <div>
      <!-- Debug statements -->
      Switch cases: {{data.cases}}<br>
      Variables: {{$root.variables}}


      <div v-for="(value, key) in data.cases">
          <div v-bind:class="$root.variables[data.variable]"
               v-if="key == $root.variables[data.variable]">
              <all-components v-bind:data="value"></all-components>
          </div>
      </div>
  </div>
</script>

Input JSON (bound as data in the above template):

{
    // Switch on value of app.variables.update_status
    "type": "switch",
    "variable": "update_status",   // Refers to app.variables.update_status
                                   // Used in <script id="template-switch">
    "cases": {
        // if app.variables.update_status == "checking" (Initial value)
        "checking": {
          "type": "paragraph",
          "text": "Checking for updates"
        },
        // if app.variables.update_status == "available" (Changed below)
        "available": {
            "type": "paragraph",
            "text": "Updates available."
        }
    }
}

My question:

Assuming app is the Vue.js app, I'd expect setting app.variables.update_status = "available" should lead to DOM change. But it doesn't as described in TL;DR section. I'm hoping to understand why.

What I have tried:

Try it out!

Here's the JS Fiddle (heavily downsized, and commented for easier understanding :))

What to try:

Once the fiddle runs, open the browser console and try executing the following statements:

Vue.js version: 2.5.16


Update

Also, I just found out that if I pass data object as:

new Vue({.., data: { .. , variables: {update_status: "temp"}}})

– it works!

I don’t understand this, primarily because variables field is set up to have a deep watcher. I’d assume that when it would have a its fields updated (such as variables.update_status = "new-value";), the observer would eventually trigger the DOM update. But for some reason this doesn’t happen.

I’m really hoping I’m doing something stupid, and that this isn’t this a bug.

Link to the new Fiddle that shows this behaviour: https://jsfiddle.net/g0z3xcyk/

Upvotes: 8

Views: 13006

Answers (2)

Sphinx
Sphinx

Reputation: 10729

Some issues with your code:

  1. Check Reactivity In depth as @LuisOrduz commented & answered, Vue cannot detect property addition or deletion. so two solutions: decalare it first (as your second fiddle did), or uses Vue.set or vm.$set to add one property.

  2. Use vm.$mount(selector) instead of using JQuery to append vm.$el; check vm.$mount

  3. It's better to use vm.$data to access data property instead of vm[key]; check vm.$data

Below is one demo:

function registerComponents() {
    Vue.component('all-components', {
      template: '#template-all-components',
      props: ['data']
    });
    Vue.component('weave-switch', {
      template: '#template-switch',
      props: ['data'],
      methods: {
        toggleStatus: function () {
          this.$root.$data.variables.update_status += ' @'
        }
      }
    });
    Vue.component('paragraph', {
      template: '#template-paragraph',
      props: ['data']
    });
}

function GenericCard(selector, options) {
    var data = Object.assign({}, options.data, {variables: {}});
    var watch = {};
    Object.keys(data).forEach(function(key) {
        watch[key] = {handler: function(val) {
        }, deep: true};
    });
    var app = new Vue({
        template: options.template,
        data: function () { // uses function instead
            return data
        },
        watch: watch
    });
    
    DEBUG = app;

    return {
        load: function(data) {
            Object.keys(data).forEach(function(key) {
                app.$data[key] = data[key];
            });
            
            //app.$data.variables.update_status = "checking"; // orginal method
            
                        app.$set(app.$data.variables, 'update_status', 'checking') // new solution
            app.$mount(selector);
            
            //var dom = app.$el;
            //$(selector).append(dom); // uses vm.$mount(selector) instead
            
            DEBUG.$set(DEBUG.$data.variables, 'update_status', 'available')  // new solution
            //DEBUG.$data.variables.update_status = 'available1'  // or new solution
            //DEBUG.variables.update_status = "available";  // orginal method
        },
        DEBUG: DEBUG
    };
}

registerComponents();

card = GenericCard('#app', {
  template: "#template-card",
  data: {
    ui: {}
  }
});
card.load({
        ui: {
      // Switch on value of app.variables.update_status
      "type": "switch",
      "variable": "update_status",   // Refers to app.variables.update_status
                                     // Used in <script id="template-switch">
            "cases": {
        // if app.variables.update_status == "checking" (Initial value)
        "checking": {
          "type": "paragraph",
          "text": "Checking for updates"
        },
        // if app.variables.update_status == "available" (Changed below)
        "available": {
          "type": "paragraph",
          "text": "Updates available."
        }
      }
    }
  });
Vue.config.productionTip = false

function toggleStatus() {
  card.DEBUG.$data.variables.update_status += ' #'
}
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<script type="text/x-template" id="template-all-components">
  <div v-if="data.type == 'paragraph'">
      <paragraph v-bind:data="data.text"></paragraph>
  </div>
  <div v-else-if="data.type == 'switch'">
      <weave-switch v-bind:data="data"></weave-switch>
  </div>
</script>
<script type="text/x-template" id="template-switch">
  <div>
        <!-- Debug statements -->
      Switch cases: {{data.cases}}<br>
      Variables: {{$root.variables}}
      <button @click="toggleStatus()">Toggle</button>
      
      <div v-for="(value, key) in data.cases">
          <div v-bind:class="$root.$data.variables[data.variable]"
               v-if="key == $root.$data.variables[data.variable]">
              <all-components v-bind:data="value"></all-components>
          </div>
      </div>
  </div>
</script>
<script type="text/x-template" id="template-paragraph">
  <p>{{data}}</p>
</script>

<script type="text/x-template" id="template-card">
  <all-components v-bind:data="ui"></all-components>
</script>


<div id="app">
  
</div>

<button onclick="toggleStatus()">Toggle2</button>

Upvotes: 2

Luis Orduz
Luis Orduz

Reputation: 2887

The reason it won't update in your first fiddle is because Vue doesn't detect property addition or deletion, and you're not passing the update_status property when you instance vue, the docs explain it further.

In your second fiddle you're setting update_status when you instance vue and that's why changes, in that case, are detected.

Another option, as mentioned in the docs, is using Vue.set or recreating the object entirely by assigning it again with Object.assign

Upvotes: 5

Related Questions