web-dev-qa-db-fra.com

Composants Web: comment travailler avec les enfants?

J'expérimente actuellement avec StencilJS pour créer des composants Web.

Maintenant, je sais qu'il y a <slot /> et des emplacements nommés et tout ça. Venant de React, je suppose que la machine à sous est similaire aux enfants de React. Vous pouvez faire beaucoup de choses en utilisant des enfants dans React. Ce que j'ai souvent fait:

  1. Vérifiez si des enfants sont fournis
  2. Itérer sur les enfants pour faire quelque chose à chaque enfant (par exemple, envelopper dans une div avec une classe, etc.)

Comment feriez-vous cela en utilisant slot/composants web/stencilJS?

Je peux obtenir l'élément hôte de mon composant Web dans Stencil en utilisant

@Element() hostElement: HTMLElement;

J'utilise mon composant comme

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

Je veux rendre quelque chose comme

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

Sincères amitiés

8
Schadenn

En utilisant des emplacements, vous n'avez pas besoin de mettre une condition dans votre fonction de rendu. Vous pouvez placer l'élément no children (dans votre exemple, la portée) à l'intérieur de l'élément slot et si aucun enfant n'est fourni à l'emplacement, il y reviendra. Par exemple:

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

En répondant au commentaire que vous avez écrit - vous pouvez faire une telle chose mais avec un peu de codage et pas hors de la boîte. Chaque élément slot a une fonction assignedNodes. En utilisant ces connaissances et la compréhension du cycle de vie des composants Stencil, vous pouvez faire quelque chose comme:

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

Ce n'est pas une solution optimale et cela nécessitera que le style de l'emplacement soit défini sur aucun (car vous ne voulez pas l'afficher). En outre, il ne fonctionnera qu'avec des éléments simples qui n'ont besoin que d'être rendus et ne nécessitant aucun événement ou autre (car il ne les utilise que comme chaîne html et non comme objets).

11
Gil Fink

Merci pour la réponse Gil.

Je pensais à quelque chose de similaire auparavant (définition de l'état, etc. - en raison de problèmes de synchronisation qui pourraient survenir). Cependant, je n'ai pas aimé la solution, car vous effectuez ensuite un changement d'état dans componentDidLoad, qui déclenchera un autre chargement juste après le chargement du composant. Cela semble sale et imperturbable.

Le peu avec innerHTML={child.outerHTML} M'a cependant beaucoup aidé.

Il semble que vous puissiez également faire simplement:

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

Je pensais que vous pourriez rencontrer des problèmes de synchronisation, car pendant render() les éléments enfants de l'hôte ont déjà été supprimés pour faire de la place pour ce que render() retourne. Mais comme shadow-dom et light-dom coexistent bien dans le composant Host, je suppose qu'il ne devrait pas y avoir de problème.

Je ne sais pas vraiment pourquoi vous devez utiliser innerHTML cependant. Venant de React j'ai l'habitude de faire:

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

Et je pensais que c'était la syntaxe JSX de base et que puisque Stencil utilise également JSX, je pouvais aussi le faire. Ne fonctionne pas cependant. innerHTML fait l'affaire pour moi. Merci encore.

EDIT: Les problèmes de synchronisation que j'ai mentionnés apparaîtront si vous n'utilisez pas shadow-dom. Certaines choses étranges commencent à se produire et vous vous retrouverez avec beaucoup d'enfants en double. Bien que vous puissiez le faire (pourrait avoir des effets secondaires):

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