From eec24d2ed17fac836389c02bc789be961be20450 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Thu, 30 Sep 2021 18:32:59 -0400 Subject: [PATCH] add sl-mutation-observer --- docs/_sidebar.md | 1 + docs/components/mutation-observer.md | 104 ++++++++++++++++ docs/resources/changelog.md | 1 + .../mutation-observer.styles.ts | 10 ++ .../mutation-observer.test.ts | 13 ++ .../mutation-observer/mutation-observer.ts | 112 ++++++++++++++++++ src/shoelace.ts | 1 + 7 files changed, 242 insertions(+) create mode 100644 docs/components/mutation-observer.md create mode 100644 src/components/mutation-observer/mutation-observer.styles.ts create mode 100644 src/components/mutation-observer/mutation-observer.test.ts create mode 100644 src/components/mutation-observer/mutation-observer.ts diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 018064afc..73aff9306 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -59,6 +59,7 @@ - [Format Date](/components/format-date) - [Format Number](/components/format-number) - [Include](/components/include) + - [Mutation Observer](/components/mutation-observer) - [Relative Time](/components/relative-time) - [Resize Observer](/components/resize-observer) - [Responsive Media](/components/responsive-media) diff --git a/docs/components/mutation-observer.md b/docs/components/mutation-observer.md new file mode 100644 index 000000000..1eddefcb9 --- /dev/null +++ b/docs/components/mutation-observer.md @@ -0,0 +1,104 @@ +# Mutation Observer + +[component-header:sl-mutation-observer] + +The Mutation Observer component offers a thin, declarative interface to the [`MutationObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). + +The mutation observer will report changes to the content it wraps through the `sl-mutation` event. When emitted, a collection of [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects will be attached to `event.detail` that contains information about how it changed. + +```html preview +
+ + Click to mutate + + +
+ 👆 Click the button and watch the console + + + + +
+``` + +?> When you create a mutation observer, you must indicate what changes it should respond to by including at least one of `attr`, `child-list`, or `char-data`. If you don't specify at least one of these attributes, no mutation events will be emitted. + +## Examples + +### Child List + +Use the `child-list` attribute to watch for new child elements that are added or removed. + +```html preview +
+ +
+ Add button +
+
+ + 👆 Add and remove buttons and watch the console + + + + +
+``` + +[component-metadata:sl-mutation-observer] diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 639fa2662..46f41dc2b 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -11,6 +11,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - 🚨 BREAKING: removed `` (use `` instead) - 🚨 BREAKING: removed `percentage` attribute from `` and `` (use `value`) instead - 🚨 BREAKING: switched the default `type` of `` from `primary` to `neutral` +- Added the experimental `` component - Added the `` component - Added `--sl-surface-base` and `--sl-surface-base-alt` as early surface tokens to improve the appearance of alert, card, and panels in dark mode - Added the `--sl-panel-border-width` design token diff --git a/src/components/mutation-observer/mutation-observer.styles.ts b/src/components/mutation-observer/mutation-observer.styles.ts new file mode 100644 index 000000000..cb10fcc60 --- /dev/null +++ b/src/components/mutation-observer/mutation-observer.styles.ts @@ -0,0 +1,10 @@ +import { css } from 'lit'; +import componentStyles from '../../styles/component.styles'; + +export default css` + ${componentStyles} + + :host { + display: contents; + } +`; diff --git a/src/components/mutation-observer/mutation-observer.test.ts b/src/components/mutation-observer/mutation-observer.test.ts new file mode 100644 index 000000000..6285c3b79 --- /dev/null +++ b/src/components/mutation-observer/mutation-observer.test.ts @@ -0,0 +1,13 @@ +import { expect, fixture, html, waitUntil } from '@open-wc/testing'; +// import sinon from 'sinon'; + +import '../../../dist/shoelace.js'; +import type SlMutationObserver from './mutation-observer'; + +describe('', () => { + it('should render a component', async () => { + const el = await fixture(html` `); + + expect(el).to.exist; + }); +}); diff --git a/src/components/mutation-observer/mutation-observer.ts b/src/components/mutation-observer/mutation-observer.ts new file mode 100644 index 000000000..1ac6b9f49 --- /dev/null +++ b/src/components/mutation-observer/mutation-observer.ts @@ -0,0 +1,112 @@ +import { LitElement, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { emit } from '../../internal/event'; +import { watch } from '../../internal/watch'; +import styles from './mutation-observer.styles'; + +/** + * @since 2.0 + * @status experimental + * + * @event sl-mutation - Emitted when a mutation occurs. + * + * @slot - The content to watch for mutations. + */ +@customElement('sl-mutation-observer') +export default class SlMutationObserver extends LitElement { + static styles = styles; + + private mutationObserver: MutationObserver; + + /** + * Watches for changes to attributes. If empty, all changes will be reported. To watch only specific attributes, + * separate them by a space. + */ + @property({ reflect: true }) attr: string; + + /** Indicates whether or not the attribute's previous value should be recorded when monitoring changes. */ + @property({ attribute: 'attr-old-value', type: Boolean, reflect: true }) attrOldValue = false; + + /** Watches for changes to the character data contained within the node. */ + @property({ attribute: 'char-data', type: Boolean, reflect: true }) charData = false; + + /** Indicates whether or not the previous value of the node's text should be recorded. */ + @property({ attribute: 'char-data-old-value', type: Boolean, reflect: true }) charDataOldValue = false; + + /** Watches for the addition or removal of new child nodes. */ + @property({ attribute: 'child-list', type: Boolean, reflect: true }) childList = false; + + /** Disables the observer. */ + @property({ type: Boolean, reflect: true }) disabled = false; + + connectedCallback() { + super.connectedCallback(); + this.handleMutation = this.handleMutation.bind(this); + + this.mutationObserver = new MutationObserver(this.handleMutation); + this.startObserver(); + } + + disconnectedCallback() { + this.stopObserver(); + } + + @watch('disabled') + handleDisabledChange() { + if (this.disabled) { + this.stopObserver(); + } else { + this.startObserver(); + } + } + + @watch('attr', { waitUntilFirstUpdate: true }) + @watch('attr-old-value', { waitUntilFirstUpdate: true }) + @watch('char-data', { waitUntilFirstUpdate: true }) + @watch('char-data-old-value', { waitUntilFirstUpdate: true }) + @watch('childList', { waitUntilFirstUpdate: true }) + handleChange() { + this.stopObserver(); + this.startObserver(); + } + + handleMutation(mutationList: MutationRecord[]) { + emit(this, 'sl-mutation', { + detail: { mutationList } + }); + } + + startObserver() { + try { + this.mutationObserver.observe(this, { + subtree: true, + childList: this.childList, + attributes: typeof this.attr === 'string', + attributeFilter: typeof this.attr === 'string' && this.attr.length > 0 ? this.attr.split(' ') : undefined, + attributeOldValue: this.attrOldValue, + characterData: this.charData, + characterDataOldValue: this.charDataOldValue + }); + } catch { + // + // A mutation observer was created without one of the required attributes: attr, char-data, or child-list. The + // browser will normally throw an error, but we'll suppress that so it doesn't appear as attributes are added + // and removed. + // + } + } + + stopObserver() { + this.mutationObserver.disconnect(); + } + + render() { + return html` `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'sl-mutation-observer': SlMutationObserver; + } +} diff --git a/src/shoelace.ts b/src/shoelace.ts index e58bdfbae..0ae166038 100644 --- a/src/shoelace.ts +++ b/src/shoelace.ts @@ -27,6 +27,7 @@ export { default as SlInput } from './components/input/input'; export { default as SlMenu } from './components/menu/menu'; export { default as SlMenuItem } from './components/menu-item/menu-item'; export { default as SlMenuLabel } from './components/menu-label/menu-label'; +export { default as SlMutationObserver } from './components/mutation-observer/mutation-observer'; export { default as SlProgressBar } from './components/progress-bar/progress-bar'; export { default as SlProgressRing } from './components/progress-ring/progress-ring'; export { default as SlQrCode } from './components/qr-code/qr-code';