BREAKING(core): remove unused UpdateStylesMixin & DelegateMixin
This commit is contained in:
parent
3d25b152f3
commit
cc294f2023
9 changed files with 5 additions and 913 deletions
5
.changeset/dirty-onions-drive.md
Normal file
5
.changeset/dirty-onions-drive.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/core': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
BREAKING: Remove unused `UpdateStylesMixin` & `DelegateMixin`
|
||||||
2
packages/core/index.d.ts
vendored
2
packages/core/index.d.ts
vendored
|
|
@ -77,12 +77,10 @@ export { until } from 'lit/directives/until.js';
|
||||||
export { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
export { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
||||||
export { dedupeMixin } from '@open-wc/dedupe-mixin';
|
export { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
// ours
|
// ours
|
||||||
export { DelegateMixin } from './src/DelegateMixin.js';
|
|
||||||
export { DisabledMixin } from './src/DisabledMixin.js';
|
export { DisabledMixin } from './src/DisabledMixin.js';
|
||||||
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
|
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
|
||||||
export { ScopedStylesController } from './src/ScopedStylesController.js';
|
export { ScopedStylesController } from './src/ScopedStylesController.js';
|
||||||
export { SlotMixin } from './src/SlotMixin.js';
|
export { SlotMixin } from './src/SlotMixin.js';
|
||||||
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
|
|
||||||
export { browserDetection } from './src/browserDetection.js';
|
export { browserDetection } from './src/browserDetection.js';
|
||||||
export { EventTargetShim } from './src/EventTargetShim.js';
|
export { EventTargetShim } from './src/EventTargetShim.js';
|
||||||
export { uuid } from './src/uuid.js';
|
export { uuid } from './src/uuid.js';
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,10 @@ export { until } from 'lit/directives/until.js';
|
||||||
export { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
export { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
||||||
export { dedupeMixin } from '@open-wc/dedupe-mixin';
|
export { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
// ours
|
// ours
|
||||||
export { DelegateMixin } from './src/DelegateMixin.js';
|
|
||||||
export { DisabledMixin } from './src/DisabledMixin.js';
|
export { DisabledMixin } from './src/DisabledMixin.js';
|
||||||
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
|
export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js';
|
||||||
export { ScopedStylesController } from './src/ScopedStylesController.js';
|
export { ScopedStylesController } from './src/ScopedStylesController.js';
|
||||||
export { SlotMixin } from './src/SlotMixin.js';
|
export { SlotMixin } from './src/SlotMixin.js';
|
||||||
export { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
|
|
||||||
export { browserDetection } from './src/browserDetection.js';
|
export { browserDetection } from './src/browserDetection.js';
|
||||||
export { EventTargetShim } from './src/EventTargetShim.js';
|
export { EventTargetShim } from './src/EventTargetShim.js';
|
||||||
export { uuid } from './src/uuid.js';
|
export { uuid } from './src/uuid.js';
|
||||||
|
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../types/DelegateMixinTypes').DelegateMixin} DelegateMixin
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef DelegateEvent
|
|
||||||
* @property {string} type - Type of event
|
|
||||||
* @property {(event: Event) => unknown} handler - Event arguments
|
|
||||||
* @property {boolean | AddEventListenerOptions} [opts]
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {DelegateMixin}
|
|
||||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('../index').LitElement>} superclass
|
|
||||||
*/
|
|
||||||
const DelegateMixinImplementation = superclass =>
|
|
||||||
// eslint-disable-next-line
|
|
||||||
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
|
|
||||||
class extends superclass {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {DelegateEvent[]}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this.__eventsQueue = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Object.<string,?>}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this.__propertiesQueue = {};
|
|
||||||
/** @private */
|
|
||||||
this.__setupPropertyDelegation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {{target: Function, events: string[], methods: string[], properties: string[], attributes: string[]}}
|
|
||||||
*/
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
target: () => {},
|
|
||||||
events: [],
|
|
||||||
methods: [],
|
|
||||||
properties: [],
|
|
||||||
attributes: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._connectDelegateMixin();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {import('lit-element').PropertyValues } changedProperties */
|
|
||||||
updated(changedProperties) {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
this._connectDelegateMixin();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} type
|
|
||||||
* @param {(event: Event) => unknown} handler
|
|
||||||
* @param {boolean | AddEventListenerOptions} [opts]
|
|
||||||
*/
|
|
||||||
addEventListener(type, handler, opts) {
|
|
||||||
const delegatedEvents = this.delegations.events;
|
|
||||||
if (delegatedEvents.indexOf(type) > -1) {
|
|
||||||
if (this.delegationTarget) {
|
|
||||||
this.delegationTarget.addEventListener(type, handler, opts);
|
|
||||||
} else {
|
|
||||||
this.__eventsQueue.push({ type, handler });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.addEventListener(type, handler, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
* @param {string} value
|
|
||||||
*/
|
|
||||||
setAttribute(name, value) {
|
|
||||||
const attributeNames = this.delegations.attributes;
|
|
||||||
if (attributeNames.indexOf(name) > -1) {
|
|
||||||
if (this.delegationTarget) {
|
|
||||||
this.delegationTarget.setAttribute(name, value);
|
|
||||||
}
|
|
||||||
super.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
super.setAttribute(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} name
|
|
||||||
*/
|
|
||||||
removeAttribute(name) {
|
|
||||||
const attributeNames = this.delegations.attributes;
|
|
||||||
if (attributeNames.indexOf(name) > -1) {
|
|
||||||
if (this.delegationTarget) {
|
|
||||||
this.delegationTarget.removeAttribute(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.removeAttribute(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
_connectDelegateMixin() {
|
|
||||||
if (this.__connectedDelegateMixin) return;
|
|
||||||
|
|
||||||
if (!this.delegationTarget) {
|
|
||||||
this.delegationTarget = this.delegations.target();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.delegationTarget) {
|
|
||||||
this.__emptyEventListenerQueue();
|
|
||||||
this.__emptyPropertiesQueue();
|
|
||||||
this.__initialAttributeDelegation();
|
|
||||||
|
|
||||||
this.__connectedDelegateMixin = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
__setupPropertyDelegation() {
|
|
||||||
const propertyNames = this.delegations.properties.concat(this.delegations.methods);
|
|
||||||
propertyNames.forEach(propertyName => {
|
|
||||||
Object.defineProperty(this, propertyName, {
|
|
||||||
get() {
|
|
||||||
const target = this.delegationTarget;
|
|
||||||
if (target) {
|
|
||||||
if (typeof target[propertyName] === 'function') {
|
|
||||||
return target[propertyName].bind(target);
|
|
||||||
}
|
|
||||||
return target[propertyName];
|
|
||||||
}
|
|
||||||
if (this.__propertiesQueue[propertyName]) {
|
|
||||||
return this.__propertiesQueue[propertyName];
|
|
||||||
}
|
|
||||||
// This is the moment the attribute is not delegated (and thus removed) yet.
|
|
||||||
// and the property is not set, but the attribute is (it serves as a fallback for
|
|
||||||
// __propertiesQueue).
|
|
||||||
return this.getAttribute(propertyName);
|
|
||||||
},
|
|
||||||
set(newValue) {
|
|
||||||
if (this.delegationTarget) {
|
|
||||||
const oldValue = this.delegationTarget[propertyName];
|
|
||||||
this.delegationTarget[propertyName] = newValue;
|
|
||||||
// connect with observer system if available
|
|
||||||
if (typeof this.triggerObserversFor === 'function') {
|
|
||||||
this.triggerObserversFor(propertyName, newValue, oldValue);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.__propertiesQueue[propertyName] = newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
__initialAttributeDelegation() {
|
|
||||||
const attributeNames = this.delegations.attributes;
|
|
||||||
attributeNames.forEach(attributeName => {
|
|
||||||
const attributeValue = this.getAttribute(attributeName);
|
|
||||||
if (typeof attributeValue === 'string') {
|
|
||||||
this.delegationTarget.setAttribute(attributeName, attributeValue);
|
|
||||||
super.removeAttribute(attributeName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
__emptyEventListenerQueue() {
|
|
||||||
this.__eventsQueue.forEach(ev => {
|
|
||||||
this.delegationTarget.addEventListener(ev.type, ev.handler, ev.opts);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
__emptyPropertiesQueue() {
|
|
||||||
Object.keys(this.__propertiesQueue).forEach(propName => {
|
|
||||||
this.delegationTarget[propName] = this.__propertiesQueue[propName];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DelegateMixin = dedupeMixin(DelegateMixinImplementation);
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
/* global ShadyCSS */
|
|
||||||
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../types/UpdateStylesMixinTypes').UpdateStylesMixin} UpdateStylesMixin
|
|
||||||
* @typedef {import('../types/UpdateStylesMixinTypes').StylesMap} StylesMap
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {UpdateStylesMixin}
|
|
||||||
* @param {import('@open-wc/dedupe-mixin').Constructor<HTMLElement>} superclass
|
|
||||||
*/
|
|
||||||
const UpdateStylesMixinImplementation = superclass =>
|
|
||||||
// eslint-disable-next-line no-shadow
|
|
||||||
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
|
|
||||||
class extends superclass {
|
|
||||||
/**
|
|
||||||
* @example
|
|
||||||
* <my-element>
|
|
||||||
* <style>
|
|
||||||
* :host {
|
|
||||||
* color: var(--foo);
|
|
||||||
* }
|
|
||||||
* </style>
|
|
||||||
* </my-element>
|
|
||||||
*
|
|
||||||
* $0.updateStyles({'background': 'orange', '--foo': '#fff'})
|
|
||||||
* Chrome, Firefox: <my-element style="background: orange; --foo: bar;">
|
|
||||||
* IE: <my-element>
|
|
||||||
* => to head: <style>color: #fff</style>
|
|
||||||
*
|
|
||||||
* @param {StylesMap} updateStyles
|
|
||||||
*/
|
|
||||||
updateStyles(updateStyles) {
|
|
||||||
const styleString = this.getAttribute('style') || this.getAttribute('data-style') || '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* reducer function
|
|
||||||
* @param {Object.<string, string>} acc
|
|
||||||
* @param {string} stylePair
|
|
||||||
*/
|
|
||||||
const reducer = (acc, stylePair) => {
|
|
||||||
/** @type {Array.<string>} */
|
|
||||||
const parts = stylePair.split(':');
|
|
||||||
if (parts.length === 2) {
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
acc[parts[0]] = parts[1];
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
};
|
|
||||||
const currentStyles = styleString.split(';').reduce(reducer, {});
|
|
||||||
|
|
||||||
const newStyles = { ...currentStyles, ...updateStyles };
|
|
||||||
let newStylesString = '';
|
|
||||||
// @ts-ignore not sure how to type ShadyCSS..
|
|
||||||
if (typeof ShadyCSS === 'object' && !ShadyCSS.nativeShadow) {
|
|
||||||
// No ShadowDOM => IE, Edge
|
|
||||||
|
|
||||||
/** @type {Object.<string, string>} */
|
|
||||||
const newCssVariablesObj = {};
|
|
||||||
|
|
||||||
Object.keys(newStyles).forEach(key => {
|
|
||||||
if (key.indexOf('--') === -1) {
|
|
||||||
newStylesString += `${key}:${newStyles[key]};`;
|
|
||||||
} else {
|
|
||||||
newCssVariablesObj[key] = newStyles[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.setAttribute('style', newStylesString);
|
|
||||||
// @ts-ignore not sure how to type ShadyCSS..
|
|
||||||
ShadyCSS.styleSubtree(this, newCssVariablesObj);
|
|
||||||
} else {
|
|
||||||
// has shadowdom => Chrome, Firefox, Safari
|
|
||||||
Object.keys(newStyles).forEach(key => {
|
|
||||||
newStylesString += `${key}: ${newStyles[key]};`;
|
|
||||||
});
|
|
||||||
this.setAttribute('style', newStylesString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpdateStylesMixin = dedupeMixin(UpdateStylesMixinImplementation);
|
|
||||||
|
|
@ -1,449 +0,0 @@
|
||||||
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
|
||||||
import sinon from 'sinon';
|
|
||||||
import { LitElement } from '../index.js';
|
|
||||||
import { DelegateMixin } from '../src/DelegateMixin.js';
|
|
||||||
|
|
||||||
describe('DelegateMixin', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
sinon.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates events', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
events: ['click'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const element = await fixture(`<${tag}></${tag}>`);
|
|
||||||
const cb = sinon.spy();
|
|
||||||
element.addEventListener('click', cb);
|
|
||||||
element.shadowRoot?.getElementById('button1')?.click();
|
|
||||||
expect(cb.callCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates events before delegation target is attached to DOM', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
events: ['click'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const element = /** @type {LitElement} */ (document.createElement(tag));
|
|
||||||
const cb = sinon.spy();
|
|
||||||
element.addEventListener('click', cb);
|
|
||||||
document.body.appendChild(element);
|
|
||||||
await element.updateComplete;
|
|
||||||
element.shadowRoot?.getElementById('button1')?.click();
|
|
||||||
expect(cb.callCount).to.equal(1);
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
document.body.removeChild(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates if light and shadow dom is used at the same time', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => Array.from(this.children).find(child => child.slot === 'button'),
|
|
||||||
events: ['click'],
|
|
||||||
methods: ['click'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<span>Outside</span>
|
|
||||||
<slot name="button2"></slot>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const element = await fixture(`<${tag}><button slot="button">click me</button></${tag}>`);
|
|
||||||
const cb = sinon.spy();
|
|
||||||
element.addEventListener('click', cb);
|
|
||||||
const childEl = /** @type {HTMLElement} */ (
|
|
||||||
Array.from(element.children)?.find(child => child.slot === 'button')
|
|
||||||
);
|
|
||||||
childEl?.click();
|
|
||||||
expect(cb.callCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will still support other events', async () => {
|
|
||||||
class FooDelegate extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
events: ['click'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
foo() {
|
|
||||||
this.dispatchEvent(new CustomEvent('foo-event', { bubbles: true, composed: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag = defineCE(FooDelegate);
|
|
||||||
const element = /** @type {FooDelegate} */ (await fixture(`<${tag}></${tag}>`));
|
|
||||||
const cb = sinon.spy();
|
|
||||||
element.addEventListener('foo-event', cb);
|
|
||||||
element.foo();
|
|
||||||
expect(cb.callCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will call delegated methods', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
methods: ['click'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const element = /** @type {HTMLElement} */ (await fixture(`<${tag}></${tag}>`));
|
|
||||||
const cb = sinon.spy();
|
|
||||||
element.shadowRoot?.getElementById('button1')?.addEventListener('click', cb);
|
|
||||||
element.click();
|
|
||||||
expect(cb.callCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports arguments for delegated methods', async () => {
|
|
||||||
class DelegateArgumentSub extends LitElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.foo = { a: 'a', b: 'b' };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?} a
|
|
||||||
* @param {?} b
|
|
||||||
*/
|
|
||||||
setFooAandB(a, b) {
|
|
||||||
this.foo.a = a;
|
|
||||||
this.foo.b = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('delegate-argument-sub', DelegateArgumentSub);
|
|
||||||
|
|
||||||
class DelegateArgumentParent extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('sub'),
|
|
||||||
methods: ['setFooAandB'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<delegate-argument-sub id="sub"></delegate-argument-sub>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tag = defineCE(DelegateArgumentParent);
|
|
||||||
|
|
||||||
const element = /** @type {DelegateArgumentParent} */ (await fixture(`<${tag}></${tag}>`));
|
|
||||||
|
|
||||||
// @ts-ignore because this method, even though it doesn't exist on the parent, gets delegated through delegations to the child, where it does exist!
|
|
||||||
element.setFooAandB('newA', 'newB');
|
|
||||||
|
|
||||||
const sub = /** @type {DelegateArgumentSub} */ (element.shadowRoot?.getElementById('sub'));
|
|
||||||
expect(sub.foo.a).to.equal('newA');
|
|
||||||
expect(sub.foo.b).to.equal('newB');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will set delegated properties', async () => {
|
|
||||||
class PropDelegate extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
properties: ['disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tag = defineCE(PropDelegate);
|
|
||||||
const element = /** @type {PropDelegate} */ (await fixture(`<${tag}></${tag}>`));
|
|
||||||
|
|
||||||
// @ts-ignore ignoring this one, because disabled is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
element.disabled = true;
|
|
||||||
|
|
||||||
await element.updateComplete;
|
|
||||||
|
|
||||||
/** @typedef {Object.<string,boolean>} Btn */
|
|
||||||
/** @typedef {Btn & HTMLElement} DelegatedBtn */
|
|
||||||
const btn = /** @type {DelegatedBtn} */ (element.shadowRoot?.getElementById('button1'));
|
|
||||||
expect(btn?.disabled).to.equal(true);
|
|
||||||
expect(btn?.hasAttribute('disabled')).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates properties before delegation target is attached to DOM', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
properties: ['disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
/** @typedef {Object.<string,boolean>} Btn */
|
|
||||||
/** @typedef {Btn & LitElement} DelegatedEl */
|
|
||||||
const element = /** @type {DelegatedEl} */ (document.createElement(tag));
|
|
||||||
|
|
||||||
element.disabled = true;
|
|
||||||
document.body.appendChild(element);
|
|
||||||
await element.updateComplete;
|
|
||||||
|
|
||||||
/** @typedef {Btn & HTMLElement} DelegatedBtn */
|
|
||||||
const btn = /** @type {DelegatedBtn} */ (element.shadowRoot?.getElementById('button1'));
|
|
||||||
|
|
||||||
expect(btn?.disabled).to.equal(true);
|
|
||||||
// cleanup
|
|
||||||
document.body.removeChild(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will delegate setAttribute', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
attributes: ['disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const element = /** @type {LitElement} */ (await fixture(`<${tag}></${tag}>`));
|
|
||||||
element.setAttribute('disabled', '');
|
|
||||||
await element.updateComplete;
|
|
||||||
expect(element.hasAttribute('disabled')).to.equal(false);
|
|
||||||
expect(element.shadowRoot?.getElementById('button1')?.hasAttribute('disabled')).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will read inital attributes', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
attributes: ['disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const element = await fixture(`<${tag} disabled></${tag}>`);
|
|
||||||
expect(element.hasAttribute('disabled')).to.equal(false);
|
|
||||||
expect(element.shadowRoot?.getElementById('button1')?.hasAttribute('disabled')).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will delegate removeAttribute', async () => {
|
|
||||||
const tag = defineCE(
|
|
||||||
class extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.getElementById('button1'),
|
|
||||||
attributes: ['disabled'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<button id="button1">with delegation</button>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const element = /** @type {LitElement} */ (await fixture(`<${tag} disabled></${tag}>`));
|
|
||||||
element.removeAttribute('disabled');
|
|
||||||
await element.updateComplete;
|
|
||||||
expect(element.hasAttribute('disabled')).to.equal(false);
|
|
||||||
expect(element.shadowRoot?.getElementById('button1')?.hasAttribute('disabled')).to.equal(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('respects user defined values for delegated attributes and properties', async () => {
|
|
||||||
class ScheduledElement extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
// this just means itś config is set to the queue when called before connectedCallback
|
|
||||||
target: () => this.scheduledElement,
|
|
||||||
attributes: ['type'],
|
|
||||||
properties: ['type'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get scheduledElement() {
|
|
||||||
return this.querySelector('input');
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.type = 'email'; // 1. here we set the delegated prop and it should be scheduled
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
// 2. this is where we add teh delegation target (so after 1)
|
|
||||||
this.appendChild(document.createElement('input'));
|
|
||||||
super.connectedCallback(); // let the DelegateMixin do its work
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag = defineCE(ScheduledElement);
|
|
||||||
const tagName = unsafeStatic(tag);
|
|
||||||
|
|
||||||
// Here, the Application Developerd tries to set the type via attribute
|
|
||||||
const elementAttr = /** @type {ScheduledElement} */ (
|
|
||||||
await fixture(`<${tag} type="radio"></${tag}>`)
|
|
||||||
);
|
|
||||||
expect(elementAttr.scheduledElement?.type).to.equal('radio');
|
|
||||||
// Here, the Application Developer tries to set the type via property
|
|
||||||
const elementProp = /** @type {ScheduledElement} */ (
|
|
||||||
await fixture(html`<${tagName} .type=${'radio'}></${tagName}>`)
|
|
||||||
);
|
|
||||||
expect(elementProp.scheduledElement?.type).to.equal('radio');
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`uses attribute value as a fallback for delegated property getter
|
|
||||||
when property not set by user and delegationTarget not ready`, async () => {
|
|
||||||
class FallbackEl extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.delegatedEl,
|
|
||||||
properties: ['type'],
|
|
||||||
attributes: ['type'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get delegatedEl() {
|
|
||||||
// returns null, so we can test that "cached" attr is used as fallback
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tag = defineCE(FallbackEl);
|
|
||||||
const element = /** @type {FallbackEl} */ (await fixture(`<${tag} type="radio"></${tag}>`));
|
|
||||||
expect(element.delegatedEl).to.equal(null);
|
|
||||||
// @ts-ignore ignoring this one, because type is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
expect(element.type).to.equal('radio'); // value retrieved from host instead of delegatedTarget
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works with connectedCallback', async () => {
|
|
||||||
class ConnectedElement extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.querySelector('div'),
|
|
||||||
properties: ['foo'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tag = await defineCE(ConnectedElement);
|
|
||||||
const element = /** @type {ConnectedElement} */ (await fixture(`<${tag}><div></div></${tag}>`));
|
|
||||||
|
|
||||||
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
element.foo = 'new';
|
|
||||||
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
expect(element.querySelector('div')?.foo).to.equal('new');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works with shadow dom', async () => {
|
|
||||||
class A extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.shadowRoot?.querySelector('div'),
|
|
||||||
properties: ['foo'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<div></div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tag = await defineCE(A);
|
|
||||||
const element = await fixture(`<${tag}></${tag}>`);
|
|
||||||
|
|
||||||
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
element.foo = 'new';
|
|
||||||
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
expect(element.shadowRoot?.querySelector('div')?.foo).to.equal('new');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works with light dom', async () => {
|
|
||||||
class A extends DelegateMixin(LitElement) {
|
|
||||||
get delegations() {
|
|
||||||
return {
|
|
||||||
...super.delegations,
|
|
||||||
target: () => this.querySelector('div'),
|
|
||||||
properties: ['foo'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
createRenderRoot() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<div></div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag = await defineCE(A);
|
|
||||||
const element = await fixture(`<${tag}></${tag}>`);
|
|
||||||
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
element.foo = 'new';
|
|
||||||
// @ts-ignore ignoring this one, because foo is delegated through target so it indeed does not inherently exist on the div element
|
|
||||||
expect(element.querySelector('div')?.foo).to.equal('new');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import { defineCE, expect, fixture } from '@open-wc/testing';
|
|
||||||
import { html } from 'lit/static-html.js';
|
|
||||||
import { css, LitElement } from '../index.js';
|
|
||||||
import { UpdateStylesMixin } from '../src/UpdateStylesMixin.js';
|
|
||||||
|
|
||||||
describe('UpdateStylesMixin', () => {
|
|
||||||
it('handles css variables && direct e.g. host css properties correctly', async () => {
|
|
||||||
class UpdateStylesElement extends UpdateStylesMixin(LitElement) {
|
|
||||||
static get styles() {
|
|
||||||
return [
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
--color: rgb(128, 128, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: var(--color);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<h1 id="header">hey</h1>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag = defineCE(UpdateStylesElement);
|
|
||||||
const el = /** @type {UpdateStylesElement} */ (await fixture(`<${tag}></${tag}>`));
|
|
||||||
const header = /** @type {Element} */ (el.shadowRoot?.getElementById('header'));
|
|
||||||
|
|
||||||
expect(window.getComputedStyle(header).color).to.equal('rgb(128, 128, 128)');
|
|
||||||
expect(window.getComputedStyle(el).textAlign).to.equal('right');
|
|
||||||
el.updateStyles({
|
|
||||||
'--color': 'rgb(255, 0, 0)',
|
|
||||||
'text-align': 'center',
|
|
||||||
});
|
|
||||||
|
|
||||||
await el.updateComplete;
|
|
||||||
expect(window.getComputedStyle(header).color).to.equal('rgb(255, 0, 0)');
|
|
||||||
expect(window.getComputedStyle(el).textAlign).to.equal('center');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves existing styles', async () => {
|
|
||||||
class UpdateStylesElement extends UpdateStylesMixin(LitElement) {
|
|
||||||
static get styles() {
|
|
||||||
return [
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
--color: rgb(128, 128, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: var(--color);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`<h1 id="header">hey</h1>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tag = defineCE(UpdateStylesElement);
|
|
||||||
const el = /** @type {UpdateStylesElement} */ (await fixture(`<${tag}></${tag}>`));
|
|
||||||
const header = /** @type {Element} */ (el.shadowRoot?.getElementById('header'));
|
|
||||||
|
|
||||||
expect(window.getComputedStyle(header).color).to.equal('rgb(128, 128, 128)');
|
|
||||||
el.updateStyles({ '--color': 'rgb(255, 0, 0)' });
|
|
||||||
|
|
||||||
expect(window.getComputedStyle(header).color).to.equal('rgb(255, 0, 0)');
|
|
||||||
el.updateStyles({ 'text-align': 'left' });
|
|
||||||
|
|
||||||
const styles = window.getComputedStyle(header);
|
|
||||||
expect(styles.color).to.equal('rgb(255, 0, 0)');
|
|
||||||
expect(styles.textAlign).to.equal('left');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
58
packages/core/types/DelegateMixinTypes.d.ts
vendored
58
packages/core/types/DelegateMixinTypes.d.ts
vendored
|
|
@ -1,58 +0,0 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
||||||
import { LitElement } from '../index.js';
|
|
||||||
|
|
||||||
export type Delegations = {
|
|
||||||
target: Function;
|
|
||||||
events: string[];
|
|
||||||
methods: string[];
|
|
||||||
properties: string[];
|
|
||||||
attributes: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export declare class DelegateHost {
|
|
||||||
protected get delegations(): Delegations;
|
|
||||||
|
|
||||||
protected _connectDelegateMixin(): void;
|
|
||||||
|
|
||||||
private __setupPropertyDelegation(): void;
|
|
||||||
|
|
||||||
private __initialAttributeDelegation(): void;
|
|
||||||
|
|
||||||
private __emptyEventListenerQueue(): void;
|
|
||||||
|
|
||||||
private __emptyPropertiesQueue(): void;
|
|
||||||
|
|
||||||
connectedCallback(): void;
|
|
||||||
updated(changedProperties: import('lit-element').PropertyValues): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # DelegateMixin
|
|
||||||
* Forwards defined events, methods, properties and attributes to the defined target.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* get delegations() {
|
|
||||||
* return {
|
|
||||||
* ...super.delegations,
|
|
||||||
* target: () => this.shadowRoot.getElementById('button1'),
|
|
||||||
* events: ['click'],
|
|
||||||
* methods: ['click'],
|
|
||||||
* properties: ['disabled'],
|
|
||||||
* attributes: ['disabled'],
|
|
||||||
* };
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* render() {
|
|
||||||
* return html`
|
|
||||||
* <button id="button1">with delegation</button>
|
|
||||||
* `;
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
declare function DelegateMixinImplementation<T extends Constructor<LitElement>>(
|
|
||||||
superclass: T,
|
|
||||||
): T &
|
|
||||||
Constructor<DelegateHost> &
|
|
||||||
Pick<typeof DelegateHost, keyof typeof DelegateHost> &
|
|
||||||
Pick<typeof LitElement, keyof typeof LitElement>;
|
|
||||||
|
|
||||||
export type DelegateMixin = typeof DelegateMixinImplementation;
|
|
||||||
37
packages/core/types/UpdateStylesMixinTypes.d.ts
vendored
37
packages/core/types/UpdateStylesMixinTypes.d.ts
vendored
|
|
@ -1,37 +0,0 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
||||||
|
|
||||||
export type StylesMap = {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
export declare class UpdateStylesHost {
|
|
||||||
/**
|
|
||||||
* @example
|
|
||||||
* <my-element>
|
|
||||||
* <style>
|
|
||||||
* :host {
|
|
||||||
* color: var(--foo);
|
|
||||||
* }
|
|
||||||
* </style>
|
|
||||||
* </my-element>
|
|
||||||
*
|
|
||||||
* $0.updateStyles({'background': 'orange', '--foo': '#fff'})
|
|
||||||
* Chrome, Firefox: <my-element style="background: orange; --foo: bar;">
|
|
||||||
* IE: <my-element>
|
|
||||||
* => to head: <style>color: #fff</style>
|
|
||||||
*
|
|
||||||
* @param {StylesMap} updateStyles
|
|
||||||
*/
|
|
||||||
public updateStyles(updateStyles: StylesMap): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # UpdateStylesMixin
|
|
||||||
*/
|
|
||||||
declare function UpdateStylesMixinImplementation<T extends Constructor<HTMLElement>>(
|
|
||||||
superclass: T,
|
|
||||||
): T &
|
|
||||||
Constructor<UpdateStylesHost> &
|
|
||||||
Pick<typeof UpdateStylesHost, keyof typeof UpdateStylesHost> &
|
|
||||||
Pick<typeof HTMLElement, keyof typeof HTMLElement>;
|
|
||||||
|
|
||||||
export type UpdateStylesMixin = typeof UpdateStylesMixinImplementation;
|
|
||||||
Loading…
Reference in a new issue