Веб-компоненты: как работать с детьми?

В настоящее время я экспериментирую с StencilJS для создания некоторых веб-компонентов.

Теперь я знаю, что есть <slot /> и названные слоты и все такое. Исходя из React, я думаю, слот похож на детей в React. Вы можете делать много вещей, используя детей в React. То, что я часто делал:

  1. Проверьте, есть ли дети
  2. Итерируйте детей, чтобы что-то сделать с каждым ребенком (например, оберните это в div с классом и т. Д.)

Как бы вы сделали это, используя слот / веб-компоненты /stencilJS?

Я могу получить Host Element моего веб-компонента в Stencil, используя

@Element() hostElement: HTMLElement;

Я использую свой компонент как

<my-custom-component>
  <button>1</button>
  <button>2</button>
  <button>3</button>
</my-custom-component>

Я хочу сделать что-то вроде

render() {
  return slottedChildren ?
    <span>No Elements</span> :
    <ul class="my-custom-component">
      slottedChildren.map(child => <li class="my-custom-element>{child}</li>)
    </ul>;
}

С уважением

2 ответа

Решение

Используя слоты, вам не нужно помещать условие в вашу функцию рендеринга. Вы можете поместить элемент no children (в вашем примере span) внутрь элемента slot, и если в слоте нет дочерних элементов, он отступит к нему. Например:

render() {
    return (
        <div>
            <slot><span>no elements</span></slot>
        </div>
    );
}

Отвечая на комментарий, который вы написали - вы можете сделать это, но с некоторой кодировкой, а не из коробки. Каждый элемент слота имеет assignedNodes функция. Используя эти знания и понимание жизненного цикла компонентов трафарета, вы можете сделать что-то вроде:

import {Component, Element, State} from '@stencil/core';

@Component({
    tag: 'slotted-element',
    styleUrl: 'slotted-element.css',
    shadow: true
})
export class SlottedElement {
    @Element() host: HTMLDivElement;
    @State() children: Array<any> = [];

    componentWillLoad() {
        let slotted = this.host.shadowRoot.querySelector('slot') as HTMLSlotElement;
        this.children = slotted.assignedNodes().filter((node) => { return node.nodeName !== '#text'; });
    }

    render() {
        return (
            <div>
                <slot />
                <ul>
                    {this.children.map(child => { return <li innerHTML={child.outerHTML}></li>; })}
                </ul>
            </div>
        );
    }
}

Это не оптимальное решение, и для него потребуется, чтобы стиль слота отображался равным none (потому что вы не хотите его показывать). Кроме того, он будет работать только с простыми элементами, которые требуют только рендеринга и не требуют событий или чего-либо еще (потому что он использует их только как строку html, а не как объекты).

Спасибо за ответ, Гил.

Я думал о чем-то подобном раньше (установка состояния и т. Д. - из-за проблем с синхронизацией, которые могут возникнуть). Однако решение мне не понравилось, потому что тогда вы выполняете изменение состояния в componentDidLoad, которое инициирует другую загрузку сразу после загрузки компонента. Это кажется грязным и бесполезным.

Немного с innerHTML={child.outerHTML} помогло мне много, хотя.

Кажется, что вы также можете просто сделать:

import {Component, Element, State} from '@stencil/core';

@Component({
    tag: 'slotted-element',
    styleUrl: 'slotted-element.css',
    shadow: true
})
export class SlottedElement {
    @Element() host: HTMLDivElement;

    render() {
        return (
            <div>
                <ul>
                    {Array.from(this.host.children)
                          .map(child => <li innerHTML={child.outerHTML} />)}
                </ul>
            </div>
        );
    }
}

Я думал, что вы можете столкнуться с проблемами времени, потому что во время render() дочерние элементы хоста уже удалены, чтобы освободить место для чего угодно render() возвращается. Но так как shadow-dom и light-dom прекрасно сосуществуют в компоненте хоста, я думаю, что не должно быть никаких проблем.

Я действительно не знаю, почему вы должны использовать innerHTML хоть. Исходя из React, я привык делать:

{Array.from(this.host.children)
      .map(child => <li>{child}</li>)}

И я подумал, что это базовый синтаксис JSX, и, поскольку Stencil также использует JSX, я тоже могу это сделать. Не работает, хотя innerHTML делает трюк для меня. Еще раз спасибо.

РЕДАКТИРОВАТЬ: проблемы синхронизации я упомянул появится, если вы не используете Shadow-Dom, хотя. Некоторые странные вещи начинают происходить, и в итоге у вас будет много дублирующих детей. Хотя вы можете сделать (может иметь побочные эффекты):

import {Component, Element, State} from '@stencil/core';

@Component({
    tag: 'slotted-element',
    styleUrl: 'slotted-element.css',
    shadow: true
})
export class SlottedElement {
    children: Element[];

    @Element() host: HTMLDivElement;

    componentWillLoad() {
      this.children = Array.from(this.host.children);
      this.host.innerHTML = '';
    }

    render() {
        return (
            <div>
                <ul>
                    {this.children.map(child => <li innerHTML={child.outerHTML} />)}
                </ul>
            </div>
        );
    }
}
Другие вопросы по тегам