#vue.js #web-component #vue-cli-3
#vue.js #веб-компонент #vue-cli-3
Вопрос:
Я столкнулся с проблемой, из-за которой реализация слотов в вебкомпоненте функционирует не так, как ожидалось. Мое понимание веб-компонентов, пользовательских элементов и слотов заключается в том, что элементы, отображаемые в слоте, должны наследовать свой стиль от документа, а не Shadow DOM однако элемент в слоте фактически добавляется в Shadow DOM и, следовательно, игнорирует глобальные стили. Я создал следующий пример, чтобы проиллюстрировать проблему, с которой я столкнулся.
общий пользовательский интерфейс
Это приложение Vue, которое компилируется для веб-компонентов с использованием cli ( --target wc --name shared-ui ./src/components/*.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>
общий-пользовательский интерфейс-потребитель
Это приложение vue, которое импортирует веб-компонент общего пользовательского интерфейса, используя стандартный файл, включающий скрипт.
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 к элементу внутри моего веб-компонента?
Комментарии:
1. Мое понимание веб-компонентов, пользовательских элементов и слотов заключается в том, что элементы, отображаемые в слоте, должны наследовать свой стиль от документа, а не от Shadow DOM Я думаю, что вы ошибаетесь здесь, со слотом или нет, контент находится в ShadowDOM. Если бы вы могли стилизовать слоты с помощью глобального CSS, концепция ShadowDOM исчезла.
2. Я не думаю, что вы правы. Взгляните на следующий пример на jsfiddle, созданный с использованием стандартных веб-компонентов . В примере, если вы откроете DOM Explorer в инструментах разработки, вы увидите, что элемент в слоте находится за пределами shadow dom и ссылается на элемент, который находится за пределами shadow DOM. Предполагается, что слот создаст отдельное дерево DOM и позволит вам отображать их вместе, смотрите здесь .
3. Вот изображение, показывающее элемент, находящийся вне shadow dom .
4. Вы можете стилизовать содержимое интервала, но не слот из глобального CSS. Вот игровая площадка / скрипка: jsfiddle.net/dannye/L8b0txgo
5. Да, это проблема, с которой я сталкиваюсь. Стили для выделенного содержимого не применяются, потому что они на самом деле не выделяются, а отображаются внутри теневого корня
Ответ №1:
Хорошо, итак, мне удалось найти обходной путь для этого, который использует собственные слоты и правильно отображает дочерние компоненты в нужном месте в DOM.
В смонтированном событии установите следующую галочку, чтобы заменить innerHTML вашего контейнера slot на новый слот. Вы можете проявить фантазию и сделать несколько классных замен для именованных слотов и еще много чего, но этого должно быть достаточно для иллюстрации обходного пути.
общий пользовательский интерфейс
Это приложение Vue, которое компилируется для веб-компонентов с использованием cli ( --target wc --name shared-ui ./src/components/*.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>
общий-пользовательский интерфейс-потребитель
Это приложение vue, которое импортирует веб-компонент общего пользовательского интерфейса, используя стандартный файл, включающий скрипт.
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');