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

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

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

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

Я могу получить элемент хоста моего веб-компонента в 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>;
}

С наилучшими пожеланиями

17
Schadenn 20 Сен 2018 в 12:00

2 ответа

Лучший ответ

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

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

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

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>
        );
    }
}

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

12
Gil Fink 21 Сен 2018 в 14:47

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

Раньше я думал о чем-то подобном (настройка состояния и т. Д. - из-за проблем со временем, которые могли возникнуть). Однако мне не понравилось это решение, потому что вы затем выполняете изменение состояния в 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 поможет мне. Еще раз спасибо.

EDIT: проблемы с синхронизацией, о которых я упоминал, появятся, если вы не используете 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>
        );
    }
}
2
Schadenn 21 Сен 2018 в 14:41