From aa45ad5c6fddb077554969313b066d7a15d35094 Mon Sep 17 00:00:00 2001 From: Bryan Jonker Date: Mon, 29 Jul 2024 16:09:08 -0500 Subject: [PATCH] Adding manual slot observer --- README.md | 4 +++ src/ManualSlotController.js | 72 +++++++++++++++++++++++++++++++++++++ src/ilw-columns.js | 38 +++----------------- 3 files changed, 80 insertions(+), 34 deletions(-) create mode 100644 src/ManualSlotController.js diff --git a/README.md b/README.md index 8993a95..60f14f8 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ Use the `reverse` attribute sparingly, as this disrupts the semantic order. Only These columns will shrink down until it gets to a certain container size. If you have a lot of columns, be aware that these may shrink to an unacceptable size or do weird results (like you have one word per line). +This is using the manual slot assignment process using the MutationObserver interface to watch for changes in the DOM. + ## External References * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox +* https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#slotassignment +* https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe diff --git a/src/ManualSlotController.js b/src/ManualSlotController.js new file mode 100644 index 0000000..c6ee0a0 --- /dev/null +++ b/src/ManualSlotController.js @@ -0,0 +1,72 @@ +/** + * A simple Lit reactive controller to apply manual slotting to a component. + * + * Using manual slotting gives us the ability to surround the child nodes with additional + * elements without resorting to manipulating the page's DOM. This is important for + * elements with strict parent-child relationships, like `ul` and `li`, since otherwise + * they would require the component's user to add those elements. + * + * When using this: + * - Make sure your component has `slotAssignment: "manual"` in its shadowRootOptions. + * - Add the `_observer = new ManualSlotController(this)` property to your component's class. + * - In your component's render, map over children to create the slots. Something like: + * + * ``` + * ${map(Array.from(this.children), () => html`
  • `)} + * ``` + */ +export class ManualSlotController { + /** + * @type import("lit").LitElement + * @private + */ + _host; + + /** + * @type MutationObserver + * @private + */ + _observer; + + /** + * @param {import("lit").LitElement} host + */ + constructor(host) { + this._host = host; + this._observer = new MutationObserver((list) => { + this._host.requestUpdate(); + }); + // This binds the controller to the element's lifecycle + host.addController(this); + } + + /** + * Find the child nodes and slots, and assign the children to each slot. + * + * The render of the host component is expected to create the slots, but this + * function will take care of assigning the elements to them. + * @private + */ + _refreshInternal() { + let items = Array.from(this._host.children); + let slots = Array.from(this._host.shadowRoot.querySelectorAll('slot')); + for (let slot of slots) { + if (items.length > 0) { + slot.assign(items.shift()); + } + } + } + + hostUpdated() { + // Called by Lit after the host component's render. + this._refreshInternal(); + } + + hostConnected() { + this._observer.observe(this._host, {childList: true}); + } + + disconnect() { + this._observer.disconnect(); + } +} \ No newline at end of file diff --git a/src/ilw-columns.js b/src/ilw-columns.js index eed9620..60a2ae1 100644 --- a/src/ilw-columns.js +++ b/src/ilw-columns.js @@ -1,6 +1,7 @@ import { LitElement, html } from 'lit'; import { map } from 'lit/directives/map.js'; import styles from './ilw-columns.styles'; +import { ManualSlotController } from './ManualSlotController.js'; import './ilw-columns.css'; class Columns extends LitElement { @@ -20,6 +21,8 @@ class Columns extends LitElement { return styles; } + _observer = new ManualSlotController(this); + constructor() { super(); this.theme = ''; @@ -73,44 +76,11 @@ class Columns extends LitElement { return this.width == 'auto' ? 'fixed' : ''; } - refresh() { - let items = this._items; - let slots = Array.from(this.shadowRoot.querySelectorAll('slot')); - if (items.length > slots.length) { - let columnBase = this.shadowRoot.querySelector('div.columns'); - for (let i = slots.length; i < items.length; i++) { - let div = document.createElement('div'); - div.appendChild(document.createElement('slot')); - columnBase.appendChild(div); - } - } else if (items.length < slots.length) { - let columnBase = this.shadowRoot.querySelector('div.columns'); - for (let i = items.length; i < slots.length; i++) { - columnBase.children[0].remove(); - } - } - this._refreshInternal(); - } - - _refreshInternal() { - let items = this._items; - let slots = Array.from(this.shadowRoot.querySelectorAll('slot')); - for (let slot of slots) { - if (items.length > 0) { - slot.assign(items.shift()); - } - } - } - - updated(changed) { - this._refreshInternal(); - } - render() { return html`
    - ${map(this._items, () => html`
    `)} + ${map(Array.from(this.children), () => html`
    `)}
    `;