Можно ли автоматически или программно размещать вложенные веб-компоненты или элементы определенного типа без указания на них атрибута slot?
Рассмотрим такую структуру:
<parent-element>
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
<p>Content</p>
</parent-element>
С <parent-element> имеющим Shadow DOM следующим образом:
<div id = "child-elements">
<slot name = "child-elements">
<child-element>Default child</child-element>
</slot>
</div>
<div id = "content">
<slot></slot>
</div>
Ожидаемый результат:
<parent-element>
<#shadow-root>
<div id = "child-elements">
<slot name = "child-elements">
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
</slot>
</div>
<div id = "content">
<slot>
<p>Content</p>
</slot>
</div>
</parent-element>
Другими словами, я хочу обеспечить, чтобы <child-element> были разрешены только внутри элементов <parent-element>, аналогичных элементам <td>, которые разрешены только внутри элемента <tr>. И я хочу, чтобы они были помещены в элемент <slot name = "child-elements">. Необходимость указывать атрибут slot для каждого из них, чтобы поместить их в определенный слот <parent-element>, кажется избыточной.
В то же время остальная часть содержимого внутри <parent-element> должна автоматически размещаться во втором элементе <slot>.
Сначала я искал способ определить это при регистрации родительского элемента, хотя CustomElementRegistry.define() в настоящее время поддерживает только extends в качестве опции.
Потом я подумал, может быть есть функция, позволяющая ставить элементы вручную, т.е. что-то вроде childElement.slot('child-elements'), но ее вроде как нет.
Затем я попытался добиться этого программно в конструкторе родительского элемента следующим образом:
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
const childElements = this.getElementsByTagName('child-element');
const childElementSlot = this.shadowRoot.querySelector('[name = "child-elements"]');
for (let i = 0; i < childElements.length; i++) {
childElementSlot.appendChild(childElements[i]);
}
}
Хотя это не перемещает дочерние элементы в <slot name = "child-elements">, поэтому все они по-прежнему размещаются во втором элементе <slot>.
@Rounin, подумайте slotchange событие для вашего ответа ...
@Rounin Я расширил свой ответ, чтобы уточнить, каков ожидаемый результат и что я пробовал до сих пор.
@Danny'365CSI'Engelman Насколько я вижу, событие slotchange запускается для элемента <slot> только после того, как элемент был добавлен или удален из него, но я пока не понимаю, как это помогает мне помещать их в правильный слот.
Я полагаю, вы имеете в виду childEements[i] вместо tabs[i]??? В этом случае вы больше не используете технологию SLOT, потому что теперь вы перемещаете содержимое lightDOM в shadowDOM.
Ой! Конечно же я имел в виду childElements[i]. Я обобщил код для SO. Ну, как я уже писал, я хочу их вставить в слоты, хотя я пока не нашел подходящего способа, кроме как переместить их из Light DOM в Shadow DOM.






Ваш безымянный по умолчанию <slot></slot> захватит все элементы, не назначенные именованному слоту;
поэтому slotchange Событие может захватить их и заставить child-element попасть в правильный слот:
customElements.define('parent-element', class extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
this.shadowRoot.addEventListener("slotchange", (evt) => {
if (evt.target.name == "") {// <slot></slot> captures
[...evt.target.assignedElements()]
.filter(el => el.nodeName == 'CHILD-ELEMENT') //process child-elements
.map(el => el.slot = "child-elements"); // force them to their own slot
} else console.info(`SLOT: ${evt.target.name} got:`,evt.target.assignedNodes())
})}});
customElements.define('child-element', class extends HTMLElement {
connectedCallback(parent = this.closest("parent-element")) {
// or check and force slot name here
if (this.parentNode != parent) {
if (parent) parent.append(this); // Child 3 !!!
else console.error(this.innerHTML, "wants a PARENT-ELEMENT!");
}}});child-element { color: red; display: block; } /* style lightDOM in global CSS! */<template id=PARENT-ELEMENT>
<style>
:host { display: inline-block; border: 2px solid red; }
::slotted(child-element) { background: lightgreen }
div { border:3px dashed rebeccapurple }
</style>
<div><slot name=child-elements></slot></div>
<slot></slot>
</template>
<parent-element>
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
<b>Content</b>
<div><child-element>Child 3 !!!</child-element></div>
</parent-element>
<child-element>Child 4 !!!</child-element>Обратите внимание на то, что логика обработки <child-element> не является прямым потомком <parent-element>, вы, вероятно, захотите переписать это под свои нужды.
Это работает отлично. Мне просто интересно, почему вы должны реагировать на событие slotchange и не можете предварительно переместить их в <slot name = "child-elements">, то есть когда они создаются или обновляются или до того, как они будут помещены в слот.
Да, можно обойтись без события slotchange и поставить логику на дочерний элемент: если слот не правильный, установить его
Также connectedCallback() не принимает параметры , поэтому parent = this.closest("parent-element") должен быть определен в теле функции.
Нет, вы можете объявить переменные там.. это допустимый JavaScript, мы не ограничены синтаксисом TypeScript.. Если компоненты должны быть небольшими, это место для предотвращения операторов LET.. экономит 4 байта. См. метод svg() в моем IconMeister все "переменные" являются "параметрами"
Это допустимый JavaScript, и он экономит несколько байтов при передаче по сети, да, но я имел в виду, что спецификация DOM говорит, что connectedCallback() вызывается без параметров. Размещение его в списке параметров может позволить людям, читающим ваш ответ, подумать, что они могут как-то измениться parent. Так что делать это может быть хорошо, когда вы хотите минимизировать свой код, но для ясности здесь, на SO, лучше использовать здесь оператор const.
назначение el.slot, похоже, не работает, если узел Text... есть ли способ переназначить слот текстового узла?
@Майкл, задай вопрос, я не могу вставить код в комментарии
С недавнего времени да, вы можете, используя метод assign() элементов слота . К сожалению, Safari его пока не поддерживает, но полифилл есть.
Я еще не пробовал, но похоже, что это решает проблему. Недостатком этого подхода, похоже, является то, что вы не можете смешивать автоматическое назначение слотов с ручным назначением (пока). В моем примере было бы лучше, если бы я мог определить, что все <child-element> размещаются вручную, а остальные автоматически в безымянных <slot>.
Я думаю, что это может быть хорошим вопросом, но в его нынешнем виде он немного сбивает с толку, чего вы пытаетесь достичь и чего вам удалось достичь на данный момент. Пожалуйста, не могли бы вы отредактировать / переписать свой вопрос для ясности - я люблю веб-компоненты и хотел бы помочь вам в этом.