DEV Community

hadakadenkyu
hadakadenkyu

Posted 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 { JSDOM } from 'jsdom'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
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.
          // Since no need for window or body, I use a fragment.
          const frag = JSDOM.fragment(source)
          const stls = frag.querySelectorAll(`style[src$=".vue"]`)
          return [...stls].reduce(async (acc, stl) => {
            const src = stl.getAttribute('src')
            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 frag = JSDOM.fragment(source)
            const stls = frag.querySelectorAll(`style`)
            // If the style tag that references the .vue is left,
            // a compile error will occur.
            const regex = new RegExp(`\\s+src=(['"])${src}\\1`)
            // It's easier to manipulate the DOM and output innerHTML,
            // but when parsing as HTML, self-closing tags aren't resolved well,
            // and when parsing as XML, the content of style and script is escaped.
            // So I chose to add the styles to the original string.
            return [...stls].reduce(
              (acc, stl) => acc + stl.outerHTML, acc.replace(regex, '')
            )
          }, source)
        }
      },
    },
  ],
  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 (0)