DEV Community

hadakadenkyu
hadakadenkyu

Posted on • Edited on

Importing Style Tag of SFC to Other SFC in Vue.js

This article is in response to issue where scoped style is not applied when extending components.

// Comp.vue
<script>
export default {
  data: () => ({
    text: 'Hello'
  })
};
</script>
<template>
  <p class="text">
    {{ text }}
  </p>
</template>
<style scoped>
.text {
  background-color: yellow;
  padding: 10px;
  font-size: 1.3rem;
}
</style>
Enter fullscreen mode Exit fullscreen mode
// ExtendedComp.vue
<script>
import Comp from './Comp.vue';
export default {
  extends: Comp,
  data: () => ({
    text: 'Hello extended'
  })
};
</script>
Enter fullscreen mode Exit fullscreen mode

Scoped Style Won't Apply

I found that by writing load in the plugins section of vite.config.js, you can modify the SFC before building. I thought if I could bring the style tag of the extended component similarly, it would work, and it did.

Scoped Style Works

Specifically, I defined the following plugin in vite.config.js.

// vite.config.js
import { fileURLToPath, URL } from 'node:url'
import path from 'node:path'
import { readFileSync } from 'fs'
import { parseDocument } from 'htmlparser2'
import { findAll, getElementsByTagName, appendChild, removeElement } from 'domutils'
import { render } from 'dom-serializer';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
    {
      async load(id) {
        if (id.endsWith('.vue')) {
          const source = readFileSync(id).toString();
          // It doesn't matter what parses the SFC, but let's use an HTML parser used in vue here.
          const dom = parseDocument(source);
          const stls = findAll(elem => elem.tagName === 'style' && elem.attribs?.src?.endsWith('.vue'), dom.children);
          if (stls.length === 0) {
            return source;
          }
          for (const stl of stls) {
            const src = stl.attribs.src;
            // If the style tag that references the .vue is left,
            // a compile error will occur due to duplicate style tags.
            // so remove it here.
            removeElement(stl);
            const absPath = path.resolve(path.dirname(id), src);
            // `resolve.alias` can be resolved with `this.resolve`,
            // but relative paths are not resolved, so I do it like this.
            const resolved = (await this.resolve(absPath)) || (await this.resolve(src));
            const source = readFileSync(resolved.id).toString();
            const vueDom = parseDocument(source);
            const vueStls = getElementsByTagName('style', vueDom.children);
            for (const vueStl of vueStls) {
              appendChild(dom, vueStl);
            }
          }
          return render(dom);
        }
      }
    },
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Then specify the vue file with the style you want to refer to from the extended component.

// ExtendedComp.vue
<script>
import Comp from './Comp.vue';
export default {
  extends: Comp,
  data: () => ({
    text: 'Hello extended'
  })
};
</script>
<style src="./Comp.vue"></style>
Enter fullscreen mode Exit fullscreen mode

Note: You can refer to it even without extending.

Top comments (1)

Collapse
 
grantorin profile image
Andrii Mostovenko

Wouldn't it be easier to move the style to a separate file and connect it by import in both components?