vuejs/vue-web-component-wrapper

How to load a font with Vue Web Component Wrapper

serbemas opened this issue ยท 15 comments

I'm working in a new project using Vue CLI 3 and I'm having an issue when I try to load a font in a web component.

I have prepared this basic example.

<template>
  <div>
    <p>
      For a guide and recipes on how to configure / customize this project
    </p>
  </div>
</template>

<script>
export default {
  name: 'Test',
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
p {
  @import url('https://fonts.googleapis.com/css?family=Montserrat&display=swap');

  font-family: 'Montserrat';
  color: #42b983;
  font-size: 28px;
}
</style>

And this is the code generated running npm serve:

image

As you can see in the image above, it generates an <style> tag with this content:

<style type="text/css">@import url(https://fonts.googleapis.com/css?family=Montserrat&display=swap);</style>

How can I solve this?

If You want to use pure CSS use @font-face, otherwise same as in regular vue components https://cli.vuejs.org/guide/css.html#pre-processors e.g. <style lang="scss">

I have changed the example and I have added a .less file with font face definitions.

<template>
  <div>
    <p>
      For a guide and recipes on how to configure / customize this project
    </p>
  </div>
</template>

<script>
export default {
  name: 'Test',
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="less">
  @import "~@/assets/less/base/fonts.less";

p {
  font-family: 'Montserrat';
  color: #42b983;
  font-size: 28px;
}
</style>

And this is the file fonts.less


@font-face {
  font-family: 'Montserrat';
  src: url("./../../fonts/Montserrat-ExtraLight.otf") format("opentype");
  font-style: normal;
}

The fonts aren't loaded by browser:

image

I have read that font-face is not working with shadow-root. Thanks!!

Take a look on what is generated, if path is correct etc.

I think that everything is generated correctly and the paths are corrects too, because in the shadow-root is added a <style> with the file content.

image

In the dist folder are the fonts too.

So it's working correctly, right? Did You try using it? Maybe Chrome won't download it untill used?

But why is not being used? I can't understand it. The style is loaded inside shadow-root however it seems that the font-face rule is not being processed.

I think load a font inside a web component should be very common.. so I don't think that I'm the first who has this problem.

Can be this a bug of the wrapper or is related to the web components maybe?

PS: If I load the styles in doing something like it works correctly.

created() {
    const linkNode = document.createElement('link');
    linkNode.type = 'text/css';
    linkNode.rel = 'stylesheet';
    linkNode.href = '//fonts.googleapis.com/css?family=Montserrat&display=swap';
    document.head.appendChild(linkNode);
  }

I mean - did you style some div/p etc. with font-family you've imported? If not why Chrome should download this font if it's not used?
This is just guess.

Yes off course.. the code is in the first comment. The styles are:

<style lang="less">
  @import "~@/assets/less/base/fonts.less";

p {
  font-family: 'Montserrat';
  color: #42b983;
  font-size: 28px;
}

and the template:

<template>
  <div>
    <p>
      For a guide and recipes on how to configure / customize this project
    </p>
  </div>
</template>

Please prepare GitHub or CodeSandbox repo. Regards!

Finally I have solved it creating a component called for example 'FontsLoader.vue' that loads the fonts style

<style scoped lang="less">
  @import "../../assets/less/base/fonts.less";
</style>

and then when its mounted() loop over the shadowRoot child nodes to find the node with @font-face declarations and append a new child in the head with the node content.

same issue~

Any solution for this?

n4ks commented

My fonts load fine in the dev version, but they don't work in build.

seyfer commented

@serbemas could you please share your solution code?

this is what I did following your spec

<template>
  <!-- This component doesn't need to render anything -->
</template>

<script setup lang="ts">
import { onMounted } from 'vue';

onMounted(() => {
  // Target the my-chatbot custom element
  const myChatbotElem = document.querySelector('my-chatbot');

  if (myChatbotElem && oskChatbotElem.shadowRoot) {
    const fontFaceNodes: NodeListOf<ChildNode> = myChatbotElem.shadowRoot.querySelectorAll('style');

    // because browser cannot read fonts from shadowDom, we need to inject them globally
    fontFaceNodes.forEach((node) => {
      if (node.textContent && node.textContent.includes('@font-face')) {
        const newStyleNode = document.createElement('style');
        newStyleNode.textContent = node.textContent;
        document.head.appendChild(newStyleNode);
      }
    });
  }
});
</script>

<style scoped lang="scss">
@import "@/assets/font/font-europa";
</style>

given that @font-face definitions are in SCSS file.
I also made sure fonts are inlined, not referenced by URL, as web-component build will have just one umd.js file in my case.
in Vue config for webpack 5 (for webpack 4 use url-loader)

chainWebpack: (config) => {
    // Clear the existing rules for fonts
    config.module.rules.delete('fonts');

    config.module
      .rule('fonts')
      .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
      .type('asset/inline')
      .parser({
        dataUrlCondition: {
          maxSize: 1000000, // Inline fonts smaller than 1MB
        },
      });
  },

and after doing that, I was able to use fonts in web component.

.my-elem-ui {
    font-family: "Europa", sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

Important note! I did include @font-face definitions in the component styles as well.
So they need to be present in the component shadowDom AND in the global page document to work.

I wish @vitejs/vite-plugin-vue could extract @font-face nodes and inject a javascript to add them in the head of the document.

Or if somebody can write a custom compileStyle: (options: SFCStyleCompileOptions): SFCStyleCompileResults function/configuration to achieve that.