Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I modify Vue.js VNodes?

I want to assign some attributes and classes to the children VNode through data object. That just works. But during my Vue.js investigation, I have not seen such pattern in use, that's why I don't think it's good idea to modify children VNode's.

But that approach sometimes comes in handy – for example I want to assign to all the buttons in default slot the aria-label attribute.

See example below, using default stateful components:

Vue.component('child', {
  template: '<div>My role is {{ $attrs.role }}</div>',
})

Vue.component('parent', {
  render(h) {
    const {
      default: defaultSlot
    } = this.$slots

    if (defaultSlot) {
      defaultSlot.forEach((child, index) => {
        if (!child.data) child.data = {}
        if (!child.data.attrs) child.data.attrs = {}

        const {
          data
        } = child

        data.attrs.role = 'button'
        data.class = 'bar'
        data.style = `color: #` + index + index + index
      })
    }

    return h(
      'div', {
        class: 'parent',
      },
      defaultSlot,
    )
  },
})

new Vue({
  el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <parent>
    <child></child>
    <child></child>
    <child></child>
    <child></child>
    <child></child>
  </parent>
</div>

And here is examples using stateless functional components:

Vue.component('child', {
  functional: true,
  render(h, {
    children
  }) {
    return h('div', {
      class: 'bar'
    }, children)
  },
})

Vue.component('parent', {
  functional: true,
  render(h, {
    scopedSlots
  }) {
    const defaultScopedSlot = scopedSlots.default({
      foo: 'bar'
    })

    if (defaultScopedSlot) {
      defaultScopedSlot.forEach((child, index) => {
        child.data = {
          style: `color: #` + index + index + index
        }

        child.data.attrs = {
          role: 'whatever'
        }
      })
    }
    return h(
      'div', {
        class: 'parent',
      },
      defaultScopedSlot,
    )
  },
})

new Vue({
  el: '#app',
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
  <parent>
    <template v-slot:default="{ foo }">
      <child>{{ foo }}</child>
      <child>{{ foo }}</child>
      <child>{{ foo }}</child>
    </template>
  </parent>
</div>

I am waiting for the following answers:

  1. Yes, you can use it, there are no potential problems with this approach.

  2. Yes, but these problem(s) can happen.

  3. No, there are a lot of problem(s).

UPDATE:

That another good approach I have found it's to wrap child VNode into the another created VNode with appropriate data object, like this:

const wrappedChildren = children.map(child => {
  return h("div", { class: "foo" }, [child]);
});

Using this approach I have no fear modifying children VNode's.

Thank you in advance.

like image 862
Andrew Vasilchuk Avatar asked Aug 28 '19 19:08

Andrew Vasilchuk


People also ask

Can we modify props in Vue?

Note: Props are read-only, which means the child component cannot modify them because the data is owned by the parent component and is only passed down to the child component to read it.

Should you use JSX with Vue?

js. Love it or hate it, JSX is a popular extension to JavaScript that allows XML tokens in your scripts. If you want to create templates in your script files and you find Vue's render() function to be difficult to work with, JSX may be just what you need.

Does VueJS have virtual DOM?

Render Functions. Vue templates are compiled into virtual DOM render functions.

Is VueJS better than react?

Vue. js combined the top-level features of React and Angular, but its main feature is the perfect user experience. Also, it leveraged the capacity of the virtual DOM and optimized the code structure.


1 Answers

There are potential problems with doing this. Used very sparingly it can be a useful technique and personally I would be happy to use it if no simple alternative were available. However, you're in undocumented territory and if something goes wrong you'll likely have to debug by stepping through Vue internals. It is not for the faint-hearted.

First, some of examples of something similar being used by others.

  1. Patching key:
    https://medium.com/dailyjs/patching-the-vue-js-virtual-dom-the-need-the-explanation-and-the-solution-ba18e4ae385b
  2. An example where Vuetify patches a VNode from a mixin:
    https://github.com/vuetifyjs/vuetify/blob/5329514763e7fab11994c4303aa601346e17104c/packages/vuetify/src/components/VImg/VImg.ts#L219
  3. An example where Vuetify patches a VNode from a scoped slot: https://github.com/vuetifyjs/vuetify/blob/7f7391d76dc44f7f7d64f30ad7e0e429c85597c8/packages/vuetify/src/components/VItemGroup/VItem.ts#L58

I think only the third example is really comparable to the patching in this question. A key feature there is that it uses a scoped slot rather than a normal slot, so the VNodes are created within the same render function.

It gets more complicated with normal slots. The problem is that the VNodes for the slot are created in the parent's render function. If the child's render function runs multiple times it'll just keep getting passed the same VNodes for the slot. Modifying those VNodes won't necessarily do what you'd expect as the diffing algorithm just sees the same VNodes and doesn't perform any DOM updates.

Here's an example to illustrate:

const MyRenderComponent = {
  data () {
    return {
      blueChildren: true
    }
  },

  render (h) {
    // Add a button before the slot children
    const children = [h('button', {
      on: {
        click: () => {
          this.blueChildren = !this.blueChildren
        }
      }
    }, 'Blue children: ' + this.blueChildren)]

    const slotContent = this.$slots.default

    for (const child of slotContent) {
      if (child.data && child.data.class) {
        // Add/remove the CSS class 'blue'
        child.data.class.blue = this.blueChildren
        
        // Log it out to confirm this really is happening
        console.log(child.data.class)
      }
      
      children.push(child)
    }

    return h('div', null, children)
  }
}

new Vue({
  el: '#app',

  components: {
    MyRenderComponent
  },

  data () {
    return {
      count: 0
    }
  }
})
.red {
  border: 1px solid red;
  margin: 10px;
  padding: 5px;
}

.blue {
  background: #009;
  color: white;
}
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<div id="app">
  <my-render-component>
    <div :class="{red: true}">This is a slot content</div>
  </my-render-component>
  <button @click="count++">
    Update outer: {{ count }}
  </button>
</div>

There are two buttons. The first button toggles a data property called blueChildren. It's used to decide whether or not to add a CSS class to the children. Changing the value of blueChildren will successfully trigger a re-render of the child component and the VNode does get updated, but the DOM is unchanged.

The other button forces the outer component to re-render. That regenerates the VNodes in the slot. These then get passed to the child and the DOM will get updated.

Vue is making some assumptions about what can and can't cause a VNode to change and optimising accordingly. In Vue 3 this is only going to get worse (by which I mean better) because there are a lot more of these optimisations coming along. There's a very interesting presentation Evan You gave about Vue 3 that covers the kinds of optimisations that are coming and they all fall into this category of Vue assuming that certain things can't change.

There are ways to fix this example. When the component is performing an update the VNode will contain a reference to the DOM node, so it can be updated directly. It's not great, but it can be done.

My own feeling is that you're only really safe if the patching you're doing is fixed, such that updates aren't a problem. Adding some attributes or CSS classes should work, so long as you don't want to change them later.

There is another class of problems to overcome. Tweaking VNodes can be really fiddly. The examples in the question allude to it. What if data is missing? What if attrs is missing?

In the scoped slots example in the question the child component has class="bar" on its <div>. That gets blown away in the parent. Perhaps that's intentional, perhaps not, but trying to merge together all the different objects is quite tricky. For example, class could be a string, object or array. The Vuetify example uses _b, which is an alias for Vue's internal bindObjectProps, to avoid having to cover all the different cases itself.

Along with the different formats are the different node types. Nodes don't necessarily represent components or elements. There are also text nodes and comments, where comment nodes are a consequence of v-if rather than actual comments in the template.

Handling all the different edge cases correctly is pretty difficult. Then again, it may be that none of these edge cases cause any real problems for the use cases you actually have in mind.

As a final note, all of the above only applies to modifying a VNode. Wrapping VNodes from a slot or inserting other children between them in a render function is perfectly normal.

like image 171
skirtle Avatar answered Oct 27 '22 21:10

skirtle