chore: refactor to reactive controller

This commit is contained in:
jorenbroekema 2022-02-28 16:49:44 +01:00
parent 006a47e530
commit 17ae20df86
9 changed files with 79 additions and 91 deletions

View file

@ -88,15 +88,16 @@ The pseudo-elements associated with the slider track/thumb are not tree-abiding,
``` ```
This means you will need to style the slotted native input from the LightDOM, This means you will need to style the slotted native input from the LightDOM,
and for this we added a helper method to `LionInputRange` which inserts a `<style>` element and for this we added added our ScopedStylesController as a controller to `LionInputRange`.
This controller inserts a `<style>` element
that emulates scoping by generating a uniquely generated class on the LionInputRange component. that emulates scoping by generating a uniquely generated class on the LionInputRange component.
This prevents the styling from conflicting with other elements on the page. This prevents the styling from conflicting with other elements on the page.
To use it when extending, override `static rangeStyles(scope)`: To use it when extending, override `static scopedStyles(scope)`:
```js ```js
class MyInputRange extends LionInputRange { class MyInputRange extends LionInputRange {
static rangeStyles(scope) { static scopedStyles(scope) {
return css` return css`
.${scope} .form-control::-webkit-slider-runnable-track { .${scope} .form-control::-webkit-slider-runnable-track {
background-color: lightgreen; background-color: lightgreen;

View file

@ -80,7 +80,7 @@ export { dedupeMixin } from '@open-wc/dedupe-mixin';
export { DelegateMixin } from './src/DelegateMixin.js'; 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 { ScopedStylesMixin } from './src/ScopedStylesMixin.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 { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.js'; export { browserDetection } from './src/browserDetection.js';

View file

@ -71,7 +71,7 @@ export { dedupeMixin } from '@open-wc/dedupe-mixin';
export { DelegateMixin } from './src/DelegateMixin.js'; 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 { ScopedStylesMixin } from './src/ScopedStylesMixin.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 { UpdateStylesMixin } from './src/UpdateStylesMixin.js';
export { browserDetection } from './src/browserDetection.js'; export { browserDetection } from './src/browserDetection.js';

View file

@ -0,0 +1,49 @@
import { unsafeCSS, css } from 'lit';
/**
* @typedef {import('lit').ReactiveControllerHost} ReactiveControllerHost
* @typedef {import('lit').ReactiveController} ReactiveController
* @implements {ReactiveController}
*/
export class ScopedStylesController {
/**
* @param {import('lit').CSSResult} scope
* @return {import('lit').CSSResultGroup}
*/
// eslint-disable-next-line no-unused-vars
static scopedStyles(scope) {
return css``;
}
/**
* @param {ReactiveControllerHost & import('lit').LitElement} host
*/
constructor(host) {
(this.host = host).addController(this);
// Perhaps use constructable stylesheets instead once Safari supports replace(Sync) methods
this.__styleTag = document.createElement('style');
this.scopedClass = `${this.host.localName}-${Math.floor(Math.random() * 10000)}`;
}
hostConnected() {
this.host.classList.add(this.scopedClass);
this.__setupStyleTag();
}
hostDisconnected() {
this.__teardownStyleTag();
}
__setupStyleTag() {
this.__styleTag.textContent = /** @type {typeof ScopedStylesController} */ (
this.host.constructor
)
.scopedStyles(unsafeCSS(this.scopedClass))
.toString();
this.host.insertBefore(this.__styleTag, this.host.childNodes[0]);
}
__teardownStyleTag() {
this.host.removeChild(this.__styleTag);
}
}

View file

@ -1,55 +0,0 @@
import { dedupeMixin } from '@open-wc/dedupe-mixin';
import { unsafeCSS, css } from 'lit';
/**
* @typedef {import('../types/ScopedStylesMixinTypes').ScopedStylesMixin} ScopedStylesMixin
*/
/**
* @type {ScopedStylesMixin}
* @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
*/
const ScopedStylesMixinImplementation = superclass =>
// eslint-disable-next-line no-shadow
// @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
class ScopedStylesHost extends superclass {
/**
* @param {import('lit').CSSResult} scope
* @return {import('lit').CSSResultGroup}
*/
// eslint-disable-next-line no-unused-vars
static scopedStyles(scope) {
return css``;
}
constructor() {
super();
// Perhaps use constructable stylesheets instead once Safari supports replace(Sync) methods
this.__styleTag = document.createElement('style');
this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`;
}
connectedCallback() {
super.connectedCallback();
this.classList.add(this.scopedClass);
this.__setupStyleTag();
}
disconnectedCallback() {
super.disconnectedCallback();
this.__teardownStyleTag();
}
__setupStyleTag() {
this.__styleTag.textContent = /** @type {typeof ScopedStylesHost} */ (this.constructor)
.scopedStyles(unsafeCSS(this.scopedClass))
.toString();
this.insertBefore(this.__styleTag, this.childNodes[0]);
}
__teardownStyleTag() {
this.removeChild(this.__styleTag);
}
};
export const ScopedStylesMixin = dedupeMixin(ScopedStylesMixinImplementation);

View file

@ -1,10 +1,10 @@
import { expect, fixture } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
// import { html as _html } from 'lit/static-html.js'; // import { html as _html } from 'lit/static-html.js';
import { LitElement, css, html } from '../index.js'; import { LitElement, css, html } from '../index.js';
import { ScopedStylesMixin } from '../src/ScopedStylesMixin.js'; import { ScopedStylesController } from '../src/ScopedStylesController.js';
describe('ScopedStylesMixin', () => { describe('ScopedStylesMixin', () => {
class Scoped extends ScopedStylesMixin(LitElement) { class Scoped extends LitElement {
/** /**
* @param {import('lit').CSSResult} scope * @param {import('lit').CSSResult} scope
* @returns {import('lit').CSSResultGroup} * @returns {import('lit').CSSResultGroup}
@ -17,6 +17,11 @@ describe('ScopedStylesMixin', () => {
`; `;
} }
constructor() {
super();
this.scopedStylesController = new ScopedStylesController(this);
}
render() { render() {
return html` <p class="test">Some Text</p> `; return html` <p class="test">Some Text</p> `;
} }
@ -31,18 +36,24 @@ describe('ScopedStylesMixin', () => {
}); });
it('contains the scoped css class for the slotted input style', async () => { it('contains the scoped css class for the slotted input style', async () => {
const el = /** @type {Scoped} */ (await fixture(html`<scoped-el></scoped-el>`)); const el = /** @type {Scoped & ScopedStylesController} */ (
expect(el.classList.contains(el.scopedClass)).to.equal(true); await fixture(html`<scoped-el></scoped-el>`)
);
expect(el.classList.contains(el.scopedStylesController.scopedClass)).to.equal(true);
}); });
it('adds a style tag as the first child which contains a class selector to the element', async () => { it('adds a style tag as the first child which contains a class selector to the element', async () => {
const el = /** @type {Scoped} */ (await fixture(html` <scoped-el></scoped-el> `)); const el = /** @type {Scoped & ScopedStylesController} */ (
await fixture(html` <scoped-el></scoped-el> `)
);
expect(el.children[0].tagName).to.equal('STYLE'); expect(el.children[0].tagName).to.equal('STYLE');
expect(el.children[0].innerHTML).to.contain(el.scopedClass); expect(el.children[0].innerHTML).to.contain(el.scopedStylesController.scopedClass);
}); });
it('the scoped styles are applied correctly to the DOM elements', async () => { it('the scoped styles are applied correctly to the DOM elements', async () => {
const el = /** @type {Scoped} */ (await fixture(html`<scoped-el></scoped-el>`)); const el = /** @type {Scoped & ScopedStylesController} */ (
await fixture(html`<scoped-el></scoped-el>`)
);
const testText = /** @type {HTMLElement} */ (el.querySelector('.test')); const testText = /** @type {HTMLElement} */ (el.querySelector('.test'));
const cl = Array.from(el.classList); const cl = Array.from(el.classList);
expect(cl.find(item => item.startsWith('scoped-el-'))).to.not.be.undefined; expect(cl.find(item => item.startsWith('scoped-el-'))).to.not.be.undefined;

View file

@ -1,20 +0,0 @@
import { Constructor } from '@open-wc/dedupe-mixin';
import { CSSResultGroup, CSSResult, LitElement } from 'lit';
export declare class ScopedStylesHost {
static scopedStyles(scope: CSSResult): CSSResultGroup;
__styleTag: HTMLStyleElement;
scopedClass: string;
private __setupStyleTag(): void;
private __teardownStyleTag(): void;
}
export declare function ScopedStylesMixinImplementation<T extends Constructor<LitElement>>(
superclass: T,
): T & Constructor<ScopedStylesHost> & Pick<typeof ScopedStylesHost, keyof typeof ScopedStylesHost>;
export type ScopedStylesMixin = typeof ScopedStylesMixinImplementation;

View file

@ -1,5 +1,5 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
import { css, html, ScopedStylesMixin } from '@lion/core'; import { css, html, ScopedStylesController } from '@lion/core';
import { LionInput } from '@lion/input'; import { LionInput } from '@lion/input';
import { formatNumber } from '@lion/localize'; import { formatNumber } from '@lion/localize';
@ -12,7 +12,7 @@ import { formatNumber } from '@lion/localize';
* *
* @customElement `lion-input-range` * @customElement `lion-input-range`
*/ */
export class LionInputRange extends ScopedStylesMixin(LionInput) { export class LionInputRange extends LionInput {
/** @type {any} */ /** @type {any} */
static get properties() { static get properties() {
return { return {
@ -59,6 +59,8 @@ export class LionInputRange extends ScopedStylesMixin(LionInput) {
constructor() { constructor() {
super(); super();
/** @type {ScopedStylesController} */
this.scopedStylesController = new ScopedStylesController(this);
this.min = Infinity; this.min = Infinity;
this.max = Infinity; this.max = Infinity;
this.step = 1; this.step = 1;

View file

@ -64,8 +64,8 @@ export class LionInput extends NativeTextFieldMixin(LionField) {
} }
/** /**
* @param {PropertyKey} name * @param {PropertyKey} [name]
* @param {?} oldValue * @param {?} [oldValue]
*/ */
requestUpdate(name, oldValue) { requestUpdate(name, oldValue) {
super.requestUpdate(name, oldValue); super.requestUpdate(name, oldValue);