add sl-mutation-observer

pull/552/head
Cory LaViska 2021-09-30 18:32:59 -04:00
rodzic 8fa9d629a3
commit eec24d2ed1
7 zmienionych plików z 242 dodań i 0 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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
<div class="mutation-overview">
<sl-mutation-observer attr>
<sl-button type="primary">Click to mutate</sl-button>
</sl-mutation-observer>
<br>
👆 Click the button and watch the console
<script>
const container = document.querySelector('.mutation-overview');
const mutationObserver = container.querySelector('sl-mutation-observer');
const button = container.querySelector('sl-button');
const types = ['primary', 'success', 'neutral', 'warning', 'danger'];
let clicks = 0;
// Change the button's type attribute
button.addEventListener('click', () => {
clicks++;
button.setAttribute('type', types[clicks % types.length]);
});
// Log mutations
mutationObserver.addEventListener('sl-mutation', event => {
console.log(event.detail);
});
</script>
<style>
.mutation-overview sl-button {
margin-bottom: 1rem;
}
</style>
</div>
```
?> 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
<div class="mutation-child-list">
<sl-mutation-observer child-list>
<div class="buttons">
<sl-button type="primary">Add button</sl-button>
</div>
</sl-mutation-observer>
👆 Add and remove buttons and watch the console
<script>
const container = document.querySelector('.mutation-child-list');
const mutationObserver = container.querySelector('sl-mutation-observer');
const buttons = container.querySelector('.buttons');
const button = container.querySelector('sl-button[type="primary"]');
let i = 0;
// Add a button
button.addEventListener('click', () => {
const button = document.createElement('sl-button');
button.textContent = ++i;
buttons.append(button);
});
// Remove a button
buttons.addEventListener('click', event => {
const target = event.target.closest('sl-button:not([type="primary"])');
event.stopPropagation();
if (target) {
target.remove();
}
});
// Log mutations
mutationObserver.addEventListener('sl-mutation', event => {
console.log(event.detail);
});
</script>
<style>
.mutation-child-list .buttons {
display: flex;
gap: .25rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
</style>
</div>
```
[component-metadata:sl-mutation-observer]

Wyświetl plik

@ -11,6 +11,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
- 🚨 BREAKING: removed `<sl-menu-divider>` (use `<sl-divider>` instead)
- 🚨 BREAKING: removed `percentage` attribute from `<sl-progress-bar>` and `<sl-progress-ring>` (use `value`) instead
- 🚨 BREAKING: switched the default `type` of `<sl-tag>` from `primary` to `neutral`
- Added the experimental `<sl-mutation-observer>` component
- Added the `<sl-divider>` 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

Wyświetl plik

@ -0,0 +1,10 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
export default css`
${componentStyles}
:host {
display: contents;
}
`;

Wyświetl plik

@ -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('<sl-mutation-observer>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-mutation-observer></sl-mutation-observer> `);
expect(el).to.exist;
});
});

Wyświetl plik

@ -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` <slot></slot> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'sl-mutation-observer': SlMutationObserver;
}
}

Wyświetl plik

@ -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';