How to render a Vue VNode to a String

3.1k views Asked by At

I'm trying to use a CSS mask in my Vue component. I need to complete the implementation of the toSvg function below. This will render the Vue VNode coming from this.$slots.default to an SVG string.

<script>
export default {
  computed: {
    maskImage() {
      const svg = this.toSvg(this.$slots.default);
      const encodedSvg = btoa(svg);
      return `url('data:image/svg+xml;base64,${encodedSvg}')`;
    },
  },
  methods: {
    toSvg(vnode) {
      // TODO: How can I convert the VNode to a string like the one below?
      // In React, I could use const svg = ReactDOMServer.renderToStaticMarkup(vnode);
      return `<svg viewBox="0 0 260 68" xmlns="http://www.w3.org/2000/svg">
          <rect x="80" y="32" width="160" height="12" rx="2" />
          <rect width="180" height="20" rx="2" />
          <rect x="80" y="52" width="95" height="12" rx="2" />
          <rect y="26" width="68" height="42" rx="2" />
      </svg>`;
    },
  },
  render(createElement) {
    return createElement("div", {
      attrs: {
        class: "skeleton",
        style: `-webkit-mask-image: ${this.maskImage}; mask-image: ${this.maskImage};`,
      },
    });
  },
};
</script>

<style lang="scss">
.skeleton {
  animation: skeleton-animation 2s infinite linear;
  background: linear-gradient(to right, hsl(30, 1, 99) 0%, hsl(30, 2, 95) 30%, hsl(30, 2, 95) 70%, hsl(30, 1, 99) 100%) 0 0 / 200% 100% hsl(30, 2, 95);
  overflow: hidden;
  position: relative;

  width: 200px;
  height: 100px;

  @keyframes skeleton-animation {
    100% {
      background-position: -200% 0;
    }
  }
}
</style>

Usage example:

<u-skeleton>
  <svg viewBox="0 0 260 68" xmlns="http://www.w3.org/2000/svg">
    <rect x="80" y="32" width="160" height="12" rx="2" />
    <rect width="180" height="20" rx="2" />
    <rect x="80" y="52" width="95" height="12" rx="2" />
    <rect y="26" width="68" height="42" rx="2" />
  </svg>
</u-skeleton>

Shows as:

enter image description here

1

There are 1 answers

0
Sphinx On BEST ANSWER

Uses Vue.extend to construct one SVG component constructor, inside render function of the constructor, it renders slots.default.

Next step is create its instance, then mount() and get the compiled html.

Vue.component('v-test', {
  computed: {
    maskImage() {
      let vnodes = this.$slots.default
      let SVGConstructor = Vue.extend({
        render: function (h, context) {
          return h('svg', {
            attrs: {
              xmlns: 'http://www.w3.org/2000/svg'
            }
          }, vnodes)
        }
      })
      let instance = new SVGConstructor()
      instance.$mount()
      const encodedSvg = btoa(instance.$el.outerHTML);
      return `url('data:image/svg+xml;base64,${encodedSvg}')`;
    }
  },
  render(createElement) {
    return createElement("div", {
      attrs: {
        class: "skeleton",
        style: `-webkit-mask-image: ${this.maskImage}; mask-image: ${this.maskImage};`,
      },
    })
  },
})

new Vue({
  el: '#app'
})
.skeleton {
  animation: skeleton-animation 2s infinite linear;
  background: linear-gradient(to right, #fcfcfc 0%, #f3f2f2 30%, #f3f2f2 70%, #fcfcfc 100%) 0 0 / 200% 100% #f3f2f2;
  overflow: hidden;
  position: relative;
  width: 200px;
  height: 100px;
}
@keyframes skeleton-animation {
  100% {
    background-position: -200% 0;
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <v-test>
    <svg viewBox="0 0 260 68" xmlns="http://www.w3.org/2000/svg">
      <rect x="80" y="32" width="160" height="12" rx="2" />
      <rect width="180" height="20" rx="2" />
      <rect x="80" y="52" width="95" height="12" rx="2" />
      <rect y="26" width="68" height="42" rx="2" />
    </svg>
  </v-test>
  <hr>
  <v-test>
    <svg viewBox="0 0 260 68" x="0" y="0" xmlns="http://www.w3.org/2000/svg">
      <rect x="80" y="32" width="160" height="12" rx="2" />
      <rect width="180" height="20" rx="2" />
      <rect x="80" y="52" width="95" height="12" rx="2" />
      <rect y="26" width="68" height="42" rx="2" />
    </svg>
    <svg viewBox="0 0 260 68" x="20" y="-20" xmlns="http://www.w3.org/2000/svg">
      <rect x="80" y="32" width="160" height="12" rx="2" />
      <rect width="180" height="20" rx="2" />
      <rect x="80" y="52" width="95" height="12" rx="2" />
      <rect y="26" width="68" height="42" rx="2" />
    </svg>
  </v-test>
</div>