Home > Software design >  Class Attribute Overwritten in Renderless Vue Component
Class Attribute Overwritten in Renderless Vue Component

Time:10-08

I've built a renderless base icon Vue component that just outputs whatever is in its slot but with some additional attributes added to the slotted component. One of the attributes I set is class="icon" which allows me to have my styles for all icons in one place.

Icon.vue

<script>
import Vue from "vue";

export default {
  name: "u-icon",
  render(createElement) {
    const svg = this.$slots.default[0];
    const attrs = svg.data.attrs;

    attrs.xmlns = "http://www.w3.org/2000/svg";
    attrs.class = "icon";
    attrs["aria-hidden"] = "true";
    attrs.focusable = "false";
    attrs.role = "img";

    return this.$slots.default;
  },
};
</script>

<style>
.icon {
  // ...
}
</style>

Close-Icon.vue

I can then more easily create many different icon components such as this close icon component.

<template>
  <u-icon>
    <svg viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
      <line x1="4" y1="4" x2="20" y2="20"></line>
      <line x1="4" y1="20" x2="20" y2="4"></line>
    </svg>
  </u-icon>
</template>

<script>
import icon from "~/icon.vue";
export default {
  name: "close-icon",
  components: {
    "u-icon": icon,
  },
};
</script>

Actual Outcome

The problem is when I use the close-icon component and set a class like so:

<close-icon class="foo"/>

The rendered output is not combining the two classes "icon foo".

<svg class="foo" ...>
  ...
</svg>

Expected Outcome

How can I ensure that the class is not overwritten but appended to so that the output is:

<svg class="icon foo" ...>
  ...
</svg>

If possible, any changes I make should be made to Icon.vue and I'd like to avoid making changes to Close-Icon.vue since there are a large number of other icons.

CodePudding user response:

class and style bindings are not part of the $attrs - source

I'm not sure why your code even work without the class binding on close-icon but if you want to add/modify the classes of existing VNode (coming from default slot), you should use svg.data.staticClass instead of svg.data.attrs.class

Example below even handles the case in which static class is also placed directly on the svg element inside close-icon - renders <svg class="foo bar icon" ...

Vue.component("u-icon", {  
  render(createElement) {
    const svg = this.$slots.default[0];
    const attrs = svg.data.attrs;
    
    attrs.xmlns = "http://www.w3.org/2000/svg";
    attrs["aria-hidden"] = "true";
    attrs.focusable = "false";
    attrs.role = "img";
    
    svg.data.staticClass = (svg.data.staticClass || "")   " icon";

    return this.$slots.default;
  },
});

Vue.component("close-icon", {
  template:`
  <u-icon>
    <svg viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" class="bar">
      <line x1="4" y1="4" x2="20" y2="20"></line>
      <line x1="4" y1="20" x2="20" y2="4"></line>
    </svg>
  </u-icon>
  `
})

new Vue({
  el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<div id="app">
  <close-icon class="foo"></close-icon>
</div>

  • Related