如何正确使用 vue js Web 组件内部的插槽并应用样式

Posted

技术标签:

【中文标题】如何正确使用 vue js Web 组件内部的插槽并应用样式【英文标题】:How to properly use slot inside of vue js web component and apply styles 【发布时间】:2019-08-05 18:54:45 【问题描述】:

我遇到了一个问题,即 Web 组件中的插槽实现未按预期运行。我对 Web 组件、自定义元素和插槽的理解是,插槽中呈现的元素应该从文档继承它们的样式,而不是 Shadow DOM,但是插槽中的元素实际上是被添加到 Shadow DOM因此忽略全局样式。我创建了以下示例来说明我遇到的问题。

共享用户界面

这是一个使用 cli (--target wc --name shared-ui ./src/components/*.vue) 编译成 web 组件的 Vue 应用程序

折叠组件.vue
<template>
    <div :class="[$style.collapsableComponent]">
        <div :class="[$style.collapsableHeader]" @click="onHeaderClick" :title="title">
            <span> title </span> 
        </div>
        <div :class="[$style.collapsableBody]" v-if="expanded">
            <slot name="body-content"></slot>
        </div>
    </div>
</template>

<script lang="ts">
    import  Vue, Component, Prop  from 'vue-property-decorator'

    @Component()
    export default class CollapsableComponent extends Vue 
        @Prop( default: "" )
        title!: string;

        @Prop(default: false)
        startExpanded!: boolean;

        private expanded: boolean = false;

        constructor() 
            super();
            this.expanded = this.startExpanded;
        

        get isVisible(): boolean 
            return this.expanded;
        

        onHeaderClick(): void 
            this.toggle();
        

        public toggle(expand?: boolean): void 
            if(expand === undefined) 
                this.expanded = !this.expanded;
            
            else 
                this.expanded = expand;
            
            this.$emit(this.expanded? 'expand' : 'collapse');
        

        public expand() 
            this.expanded = true;

        

        public collapse() 
            this.expanded = false;
        
    
</script>

<style module>
    :host 
        display: block;
    

    .collapsableComponent 
        background-color: white;
    

    .collapsableHeader 
        border: 1px solid grey;
        background: grey;
        height: 35px;
        color: black;
        border-radius: 15px 15px 0 0;
        text-align: left;
        font-weight: bold;
        line-height: 35px;
        font-size: 0.9rem;
        padding-left: 1em;
    

    .collapsableBody 
        border: 1px solid black;
        border-top: 0;
        border-radius: 0 0 10px 10px;
        padding: 1em;
    
</style>

shared-ui-consumer

这是一个 vue 应用程序,它使用标准脚本包含文件导入 shared-ui web 组件。

应用程序.vue
<template>
  <div id="app">
    <shared-ui title="Test">
      <span class="testClass" slot="body-content">
        Here is some text
      </span>
    </shared-ui>
  </div>
</template>

<script lang="ts">
import 'vue'
import  Component, Vue  from 'vue-property-decorator';

@Component( )
export default class App extends Vue 


</script>

<style lang="scss">
#app 
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;


.testClass
  color: red;

</style>
main.ts
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

// I needed to do this so the web component could reference Vue
(window as any).Vue = Vue;

new Vue(
  render: h => h(App),
).$mount('#app');

在此示例中,我希望容器内的内容具有红色文本,但是因为 Vue 将元素克隆到 Shadow DOM 中,所以 .testClass 样式被忽略并且文本以黑色填充呈现。

如何将 .testClass 应用于我的 Web 组件内的元素?

【问题讨论】:

我对 Web 组件、自定义元素和插槽的理解是,插槽中呈现的元素应该从文档而不是 Shadow DOM 继承其样式我认为你错了,无论是否开槽,内容都在 shadowDOM 中。如果您可以使用全局 CSS 为 SLOT 设置样式,那么 shadowDOM 的概念就消失了。 我不认为你是正确的。在jsfiddle built using standard web components 上查看以下示例。在示例中,如果您在开发工具中打开 DOM 资源管理器,您将看到插槽中的项目位于 shadow DOM 之外,并且被引用/链接到位于 shadow DOM 之外的元素。该插槽应该创建一个单独的 DOM 树并允许您将它们一起显示,请参阅here。 这是一张显示element living outside the shadow dom的图片。 您可以设置开槽 content 的样式,但不能设置全局 CSS 中的槽。这是一个操场/小提琴:jsfiddle.net/dannye/L8b0txgo 是的,这就是我遇到的问题。没有应用开槽内容的样式,因为它们实际上没有开槽而是呈现在阴影根中 【参考方案1】:

好的,所以我设法找到了一种解决方法,它使用本机插槽并将子组件正确呈现在 DOM 中的正确位置。

在挂载的事件中连接下一个刻度,用新的插槽替换插槽容器的 innerhtml。您可以花哨并为命名插槽做一些很酷的替换等等,但这应该足以说明解决方法。

共享用户界面

这是一个使用 cli (--target wc --name shared-ui ./src/components/*.vue) 编译成 web 组件的 Vue 应用程序

折叠组件.vue
<template>
    <div :class="[$style.collapsableComponent]">
        <div :class="[$style.collapsableHeader]" @click="onHeaderClick" :title="title">
            <span> title </span> 
        </div>
        <div ref="slotContainer" :class="[$style.collapsableBody]" v-if="expanded">
            <slot></slot>
        </div>
    </div>
</template>

<script lang="ts">
    import  Vue, Component, Prop  from 'vue-property-decorator'

    @Component()
    export default class CollapsableComponent extends Vue 
        @Prop( default: "" )
        title!: string;

        @Prop(default: false)
        startExpanded!: boolean;

        private expanded: boolean = false;

        constructor() 
            super();
            this.expanded = this.startExpanded;
        

        get isVisible(): boolean 
            return this.expanded;
        

        onHeaderClick(): void 
            this.toggle();
        
        //This is where the magic is wired up
        mounted(): void 
            this.$nextTick().then(this.fixSlot.bind(this));
        
        // This is where the magic happens
        fixSlot(): void 
            // remove all the innerHTML that vue has place where the slot should be
            this.$refs.slotContainer.innerHTML = '';
            // replace it with a new slot, if you are using named slot you can just add attributes to the slot
            this.$refs.slotContainer.append(document.createElement('slot'));
        

        public toggle(expand?: boolean): void 
            if(expand === undefined) 
                this.expanded = !this.expanded;
            
            else 
                this.expanded = expand;
            
            this.$emit(this.expanded? 'expand' : 'collapse');
        

        public expand() 
            this.expanded = true;

        

        public collapse() 
            this.expanded = false;
        
    
</script>

<style module>
    :host 
        display: block;
    

    .collapsableComponent 
        background-color: white;
    

    .collapsableHeader 
        border: 1px solid grey;
        background: grey;
        height: 35px;
        color: black;
        border-radius: 15px 15px 0 0;
        text-align: left;
        font-weight: bold;
        line-height: 35px;
        font-size: 0.9rem;
        padding-left: 1em;
    

    .collapsableBody 
        border: 1px solid black;
        border-top: 0;
        border-radius: 0 0 10px 10px;
        padding: 1em;
    
</style>

shared-ui-consumer

这是一个 vue 应用程序,它使用标准脚本包含文件导入 shared-ui Web 组件。

应用程序.vue
<template>
  <div id="app">
    <shared-ui title="Test">
      <span class="testClass" slot="body-content">
        Here is some text
      </span>
    </shared-ui>
  </div>
</template>

<script lang="ts">
import 'vue'
import  Component, Vue  from 'vue-property-decorator';

@Component( )
export default class App extends Vue 


</script>

<style lang="scss">
#app 
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;


.testClass
  color: red;

</style>
main.ts
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

// I needed to do this so the web component could reference Vue
(window as any).Vue = Vue;

new Vue(
  render: h => h(App),
).$mount('#app');

【讨论】:

以上是关于如何正确使用 vue js Web 组件内部的插槽并应用样式的主要内容,如果未能解决你的问题,请参考以下文章

学Vue就跟玩一样如何正确快速使用Vue中的插槽和配置代理

Vue组件高级 插槽的使用slot

Vue.js 3 - 将组件插入插槽

Vue.js(17)之 插槽

在有插槽问题的 Web 组件中使用事件委托(冒泡)

如何在 Vue.js 插槽中使用条件渲染?