Skip to content

Commit

Permalink
Issue #34: First draft demonstrating automatic prop forwarding using …
Browse files Browse the repository at this point in the history
…MutationObserver.
  • Loading branch information
patricknelson committed Dec 6, 2023
1 parent 8a5a037 commit d37a995
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 8 deletions.
14 changes: 12 additions & 2 deletions demo/vercel/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const shadowStylesheet = ''; // TODO: ISSUE-6: Find good solution for defining t
svelteRetag({
component: App,
tagname: 'app-tag',
attributes: ['pagetitle'],
attributes: true,
debugMode,
hydratable,
shadow,
Expand All @@ -42,6 +42,7 @@ svelteRetag({
svelteRetag({
component: Intro,
tagname: 'intro-tag',
attributes: true,
debugMode,
hydratable,
shadow,
Expand All @@ -51,6 +52,7 @@ svelteRetag({
svelteRetag({
component: FeaturesInfo,
tagname: 'features-info',
attributes: true,
debugMode,
hydratable,
shadow,
Expand All @@ -60,6 +62,7 @@ svelteRetag({
svelteRetag({
component: ExamplesInfo,
tagname: 'examples-info',
attributes: true,
debugMode,
hydratable,
shadow,
Expand All @@ -69,6 +72,7 @@ svelteRetag({
svelteRetag({
component: TabsInfo,
tagname: 'tabs-info',
attributes: true,
debugMode,
hydratable,
shadow,
Expand All @@ -78,7 +82,7 @@ svelteRetag({
svelteRetag({
component: Counter,
tagname: 'counter-tag',
attributes: ['count', 'award'],
attributes: true,
debugMode,
hydratable,
shadow,
Expand All @@ -88,6 +92,7 @@ svelteRetag({
svelteRetag({
component: ExampleTag,
tagname: 'example-tag',
attributes: true,
debugMode,
hydratable,
shadow,
Expand All @@ -97,6 +102,7 @@ svelteRetag({
svelteRetag({
component: TabPanel,
tagname: 'tab-panel',
attributes: true,
hydratable,
debugMode,
shadow,
Expand All @@ -107,6 +113,7 @@ svelteRetag({
svelteRetag({
component: TabButton,
tagname: 'tab-button',
attributes: true,
hydratable,
debugMode,
shadow,
Expand All @@ -116,6 +123,7 @@ svelteRetag({
svelteRetag({
component: TabsWrapper,
tagname: 'tabs-wrapper',
attributes: true,
hydratable,
debugMode,
shadow,
Expand All @@ -125,6 +133,7 @@ svelteRetag({
svelteRetag({
component: TabList,
tagname: 'tab-list',
attributes: true,
hydratable,
debugMode,
shadow,
Expand All @@ -140,6 +149,7 @@ svelteRetag({
svelteRetag({
component: TabsDemo,
tagname: 'tabs-demo',
attributes: true,
hydratable,
debugMode,
shadow,
Expand Down
53 changes: 47 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ import { createSvelteSlots, findSlotParent, unwrap } from './utils.js';
const propMapCache = new Map();


// Mutation observer must be used to track changes to attributes on our custom elements, since we cannot know the
// component props ahead of time (required if we were to use observedAttributes instead). In this case, only one
// observer is necessary, since each call to .observe() can have a different "attributeFilter" specified.
// NOTE: We can .observe() many separate elements and not have to .disconnect() each one individually, since if the
// element being observed is removed from the DOM and released by the garbage collector, the MutationObserver will
// stop observing the removed element automatically.
// TODO: Validate that disconnected/reconnected elements are still being observed properly (e.g. if drag/dropped in the DOM via DevTools).
const attributeObserver = new MutationObserver((mutations) => {
// Go through each mutation and forward the updated attribute value to the corresponding Svelte prop.
mutations.forEach(mutation => {
const element = mutation.target;
const attributeName = mutation.attributeName;
const newValue = element.getAttribute(attributeName);
element.forwardAttributeToProp(attributeName, newValue);
});
});


/**
* Processes the queued set of svelte-retag managed elements which have been initialized, connected and flagged as ready
* for render. This is done just before paint with the goal of processing as many as possible at once not only for speed
Expand Down Expand Up @@ -109,6 +127,9 @@ export default function svelteRetag(opts) {

this._debug('constructor()');

// New instances, attributes not yet being observed.
this.attributesObserved = false;


// Setup shadow root early (light-DOM root is initialized in connectedCallback() below).
if (opts.shadow) {
Expand All @@ -130,6 +151,9 @@ export default function svelteRetag(opts) {
/**
* Attributes we're watching for changes after render (doesn't affect attributes already present prior to render).
*
* NOTE: This only applies if opts.attributes is an array. If opts.attributes is true, then all attributes are
* watched using the mutation observer instead.
*
* @returns string[]
*/
static get observedAttributes() {
Expand Down Expand Up @@ -261,19 +285,30 @@ export default function svelteRetag(opts) {
}

/**
* Forward modifications to element attributes to the corresponding Svelte prop.
* Callback/hook for observedAttributes.
*
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
*/
attributeChangedCallback(name, oldValue, newValue) {
this._debug('attributes changed', { name, oldValue, newValue });
this.forwardAttributeToProp(name, newValue);
}

/**
* Forward modifications to element attributes to the corresponding Svelte prop (if applicable).
*
* @param {string} name
* @param {string} value
*/
forwardAttributeToProp(name, value) {
this._debug('forwardAttributeToProp', { name, value });

// If instance already available, pass it through immediately.
if (this.componentInstance && newValue !== oldValue) {
if (this.componentInstance) {
let translatedName = this._translateAttribute(name);
this.componentInstance.$set({ [translatedName]: newValue });
this.componentInstance.$set({ [translatedName]: value });
}
}

Expand Down Expand Up @@ -478,9 +513,15 @@ export default function svelteRetag(opts) {
// in the shadow DOM.
this.componentInstance = new opts.component({ target: this._root, props: props, context });

if (opts.attributes === true) {
// TODO: ISSUE-34: Check to see if this.propMap contains entries and, if so, setup the mutation observer ensuring
// that 'attributefilter' is passed to .observe().
// Setup mutation observer to watch for changes to attributes on this element (if not already done) now that we
// know the full set of component props. Only do this if configured and if the observer hasn't already been setup
// (since we can render an element multiple times).
if (opts.attributes === true && !this.attributesObserved && this.propMap.size > 0) {
attributeObserver.observe(this, {
attributes: true,
attributeFilter: [...this.propMap.keys()],
});
this.attributesObserved = true;
}


Expand Down

0 comments on commit d37a995

Please sign in to comment.