如何在 vue js web 组件中正确使用插槽并应用样式

How to properly use slot inside of vue js web component and apply styles

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

共享-ui

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

CollapseComponent.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>

共享-ui-消费者

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

App.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 应用于我的网络组件内的元素?

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

在 mounted 事件中连接下一个勾号以用新插槽替换插槽容器的 innerHtml。您可以花点心思,为命名插槽等做一些很酷的替换,但这应该足以说明解决方法。

共享-ui

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

CollapseComponent.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>

共享-ui-消费者

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

App.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');