feat: use Scoped Elements

Co-authored-by: Alex Ghiu <Alex.Ghiu@ing.com>
Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
This commit is contained in:
Joren Broekema 2020-03-05 15:48:20 +01:00 committed by Thomas Allmer
parent d1beffbff5
commit 15b4a5ebb3
10 changed files with 558 additions and 552 deletions

View file

@ -135,6 +135,100 @@ Check out our [coding guidelines](./docs/README.md) for more detailed informatio
Lion Web Components are only as good as its contributions. Lion Web Components are only as good as its contributions.
Read our [contribution guide](./CONTRIBUTING.md) and feel free to enhance/improve Lion. We keep feature requests closed while we're not working on them. Read our [contribution guide](./CONTRIBUTING.md) and feel free to enhance/improve Lion. We keep feature requests closed while we're not working on them.
## Scoped elements
The [CustomElementRegistry](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry) provides methods for registering custom elements. One of the limitations of working with this global registry is that multiple versions of the same element cannot co-exist. This causes bottlenecks in software delivery that should be managed by the teams and complex build systems. [Scoped Custom Element Registries](https://github.com/w3c/webcomponents/issues/716) is a proposal that will solve the problem. Since this functionality won't be available (especially not cross browser) anytime soon, we've adopted [OpenWC's Scoped Elements](https://open-wc.org/scoped-element).
Whenever a lion component uses composition (meaning it uses another lion component inside), we
apply ScopedElementsMixin to make sure it uses the right version of this internal component.
```js
import { ScopedElementsMixin, LitElement, html } from '@lion/core';
import { LionInput } from '@lion/input';
import { LionButton } from '@lion/button';
class MyElement extends ScopedElementsMixin(LitElement) {
static get scopedElements() {
return {
'lion-input': LionInput,
'lion-button': LionButton,
};
}
render() {
return html`
<lion-input label="Greeting" name="greeting" .modelValue=${'Hello world'}></lion-input>
<lion-button>Save</lion-button>
`;
}
}
```
### Query selectors
Since Scoped Elements changes tagnames under the hood, a tagname querySelector should be written
like this:
```js
this.querySelector(getTagName('lion-input', this.constructor.scopedElements));
```
### CSS selectors
Avoid tagname css selectors (we already avoid query selectors internally in lion, but just be aware
that a selector like `lion-input {...}` will stop working ).
### Edge cases
Sometimes we need to render parts of a template to light dom for [accessibility](https://wicg.github.io/aom/explainer.html). For instance we render a node via lit-html that we append to the host element, so
it gets slotted in the right position.
In this case, we should also make sure that we also scope the rendered element.
We can do this as follows:
```js
_myLightTemplate() {
return html`
This template may be overridden by a Subclasser.
Even I don't end up in shadow root, I need to be scoped to constructor.scopedElements as well.
<div>
<lion-button>True</lion-button>
<lion-input label="xyz"></lion-input>
</div>
`;
}
__getLightDomNode() {
const renderParent = document.createElement('div');
this.constructor.render(this._myLightTemplate(), renderParent, {
scopeName: this.localName,
eventContext: this,
});
// this node will be appended to the host
return renderParent.firstElementChild;
}
connectedCallback() {
super.connectedCallback();
this.appendChild(this.__getLightDomNode());
}
```
In a less complex case, we might just want to add a child node to the dom.
```js
import { ScopedElementsMixin, LitElement, getScopedTagNamegetScopedTagName } from '@lion/core';
...
__getLightDomNode() {
return document.createElement(getScopedTagName('lion-input', this.constructor.scopedElements));
}
```
We encourage you to have a look at [OpenWC's Scoped elements](https://open-wc.org/scoped-elements).
## Contact ## Contact
Feel free to create a github issue for any feedback or questions you might have. Feel free to create a github issue for any feedback or questions you might have.

View file

@ -75,7 +75,7 @@
"bundlesize": [ "bundlesize": [
{ {
"path": "./bundlesize/dist/core/core-*.js", "path": "./bundlesize/dist/core/core-*.js",
"maxSize": "10 kB" "maxSize": "11 kB"
}, },
{ {
"path": "./bundlesize/dist/all/all-*.js", "path": "./bundlesize/dist/all/all-*.js",

View file

@ -44,7 +44,8 @@
"@lion/input-date": "0.5.17", "@lion/input-date": "0.5.17",
"@lion/localize": "0.8.9", "@lion/localize": "0.8.9",
"@lion/overlays": "0.12.3", "@lion/overlays": "0.12.3",
"@lion/validate": "0.7.1" "@lion/validate": "0.7.1",
"@open-wc/scoped-elements": "^0.5.0"
}, },
"devDependencies": { "devDependencies": {
"@open-wc/demoing-storybook": "^1.10.4", "@open-wc/demoing-storybook": "^1.10.4",

View file

@ -1,14 +1,22 @@
import '@lion/calendar/lion-calendar.js'; import { LionCalendar } from '@lion/calendar/src/LionCalendar';
import { html, ifDefined, render } from '@lion/core'; import { html, ifDefined, ScopedElementsMixin } from '@lion/core';
import { LionInputDate } from '@lion/input-date'; import { LionInputDate } from '@lion/input-date';
import { OverlayMixin, withModalDialogConfig } from '@lion/overlays'; import { OverlayMixin, withModalDialogConfig } from '@lion/overlays';
import './lion-calendar-overlay-frame.js'; import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
/** /**
* @customElement lion-input-datepicker * @customElement lion-input-datepicker
* @extends {LionInputDate} * @extends {LionInputDate}
*/ */
export class LionInputDatepicker extends OverlayMixin(LionInputDate) { export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionInputDate)) {
static get scopedElements() {
return {
...super.scopedElements,
'lion-calendar': LionCalendar,
'lion-calendar-overlay-frame': LionCalendarOverlayFrame,
};
}
static get properties() { static get properties() {
return { return {
/** /**
@ -47,7 +55,10 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
...super.slots, ...super.slots,
[this._calendarInvokerSlot]: () => { [this._calendarInvokerSlot]: () => {
const renderParent = document.createElement('div'); const renderParent = document.createElement('div');
render(this._invokerTemplate(), renderParent); this.constructor.render(this._invokerTemplate(), renderParent, {
scopeName: this.localName,
eventContext: this,
});
return renderParent.firstElementChild; return renderParent.firstElementChild;
}, },
}; };
@ -207,7 +218,7 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
// When not opened (usually on init), it does not need to be rendered. // When not opened (usually on init), it does not need to be rendered.
// This would make first paint quicker // This would make first paint quicker
return html` return html`
<lion-calendar-overlay-frame> <lion-calendar-overlay-frame class="calendar__overlay-frame">
<span slot="heading">${this.calendarHeading}</span> <span slot="heading">${this.calendarHeading}</span>
${this._calendarTemplate()} ${this._calendarTemplate()}
</lion-calendar-overlay-frame> </lion-calendar-overlay-frame>
@ -349,7 +360,7 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
if (this._cachedOverlayContentNode) { if (this._cachedOverlayContentNode) {
return this._cachedOverlayContentNode; return this._cachedOverlayContentNode;
} }
this._cachedOverlayContentNode = this.shadowRoot.querySelector('lion-calendar-overlay-frame'); this._cachedOverlayContentNode = this.shadowRoot.querySelector('.calendar__overlay-frame');
return this._cachedOverlayContentNode; return this._cachedOverlayContentNode;
} }
} }

View file

@ -1,12 +1,20 @@
import { ChoiceGroupMixin } from '@lion/choice-input'; import { ChoiceGroupMixin } from '@lion/choice-input';
import { css, html, LitElement, SlotMixin } from '@lion/core'; import {
css,
html,
LitElement,
SlotMixin,
ScopedElementsMixin,
getScopedTagName,
} from '@lion/core';
import { FormControlMixin, FormRegistrarMixin, InteractionStateMixin } from '@lion/field'; import { FormControlMixin, FormRegistrarMixin, InteractionStateMixin } from '@lion/field';
import { formRegistrarManager } from '@lion/field/src/registration/formRegistrarManager.js'; import { formRegistrarManager } from '@lion/field/src/registration/formRegistrarManager.js';
import { OverlayMixin, withDropdownConfig } from '@lion/overlays'; import { OverlayMixin, withDropdownConfig } from '@lion/overlays';
import { ValidateMixin } from '@lion/validate'; import { ValidateMixin } from '@lion/validate';
import '../lion-select-invoker.js';
import './differentKeyNamesShimIE.js'; import './differentKeyNamesShimIE.js';
import { LionSelectInvoker } from './LionSelectInvoker.js';
function uuid() { function uuid() {
return Math.random() return Math.random()
.toString(36) .toString(36)
@ -46,13 +54,22 @@ function isInView(container, element, partial = false) {
* @customElement lion-select-rich * @customElement lion-select-rich
* @extends {LitElement} * @extends {LitElement}
*/ */
export class LionSelectRich extends ChoiceGroupMixin( export class LionSelectRich extends ScopedElementsMixin(
ChoiceGroupMixin(
OverlayMixin( OverlayMixin(
FormRegistrarMixin( FormRegistrarMixin(
InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))), InteractionStateMixin(ValidateMixin(FormControlMixin(SlotMixin(LitElement)))),
), ),
), ),
),
) { ) {
static get scopedElements() {
return {
...super.scopedElements,
'lion-select-invoker': LionSelectInvoker,
};
}
static get properties() { static get properties() {
return { return {
disabled: { disabled: {
@ -94,7 +111,10 @@ export class LionSelectRich extends ChoiceGroupMixin(
get slots() { get slots() {
return { return {
...super.slots, ...super.slots,
invoker: () => document.createElement('lion-select-invoker'), invoker: () =>
document.createElement(
getScopedTagName('lion-select-invoker', this.constructor.scopedElements),
),
}; };
} }

View file

@ -217,7 +217,7 @@ describe('lion-select-rich', () => {
`); `);
expect(el._invokerNode).to.exist; expect(el._invokerNode).to.exist;
expect(el._invokerNode.tagName).to.equal('LION-SELECT-INVOKER'); expect(el._invokerNode.tagName).to.include('LION-SELECT-INVOKER');
}); });
it('sets the first option as the selectedElement if no option is checked', async () => { it('sets the first option as the selectedElement if no option is checked', async () => {

View file

@ -1,10 +1,9 @@
import { html, css } from '@lion/core'; import { html, css, ScopedElementsMixin, getScopedTagName } from '@lion/core';
import { LionField } from '@lion/field'; import { LionField } from '@lion/field';
import { ChoiceInputMixin } from '@lion/choice-input'; import { ChoiceInputMixin } from '@lion/choice-input';
import { LionSwitchButton } from './LionSwitchButton.js';
import '../lion-switch-button.js'; export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField)) {
export class LionSwitch extends ChoiceInputMixin(LionField) {
static get styles() { static get styles() {
return [ return [
super.styles, super.styles,
@ -16,10 +15,20 @@ export class LionSwitch extends ChoiceInputMixin(LionField) {
]; ];
} }
static get scopedElements() {
return {
...super.scopedElements,
'lion-switch-button': LionSwitchButton,
};
}
get slots() { get slots() {
return { return {
...super.slots, ...super.slots,
input: () => document.createElement('lion-switch-button'), input: () =>
document.createElement(
getScopedTagName('lion-switch-button', this.constructor.scopedElements),
),
}; };
} }

View file

@ -37,7 +37,8 @@
], ],
"dependencies": { "dependencies": {
"@lion/core": "0.4.4", "@lion/core": "0.4.4",
"@lion/localize": "0.8.9" "@lion/localize": "0.8.9",
"@open-wc/scoped-elements": "^0.5.0"
}, },
"devDependencies": { "devDependencies": {
"@open-wc/demoing-storybook": "^1.10.4", "@open-wc/demoing-storybook": "^1.10.4",

View file

@ -1,7 +1,8 @@
/* eslint-disable class-methods-use-this, camelcase, no-param-reassign, max-classes-per-file */ /* eslint-disable class-methods-use-this, camelcase, no-param-reassign, max-classes-per-file */
import { dedupeMixin, SlotMixin } from '@lion/core'; import { dedupeMixin, SlotMixin, ScopedElementsMixin, getScopedTagName } from '@lion/core';
import { localize } from '@lion/localize'; import { localize } from '@lion/localize';
import { LionValidationFeedback } from './LionValidationFeedback.js';
import { ResultValidator } from './ResultValidator.js'; import { ResultValidator } from './ResultValidator.js';
import { Unparseable } from './Unparseable.js'; import { Unparseable } from './Unparseable.js';
import { AsyncQueue } from './utils/AsyncQueue.js'; import { AsyncQueue } from './utils/AsyncQueue.js';
@ -24,7 +25,14 @@ function arrayDiff(array1 = [], array2 = []) {
export const ValidateMixin = dedupeMixin( export const ValidateMixin = dedupeMixin(
superclass => superclass =>
// eslint-disable-next-line no-unused-vars, no-shadow // eslint-disable-next-line no-unused-vars, no-shadow
class ValidateMixin extends SyncUpdatableMixin(SlotMixin(superclass)) { class ValidateMixin extends ScopedElementsMixin(SyncUpdatableMixin(SlotMixin(superclass))) {
static get scopedElements() {
return {
...super.scopedElements,
'lion-validation-feedback': LionValidationFeedback,
};
}
static get properties() { static get properties() {
return { return {
/** /**
@ -108,7 +116,10 @@ export const ValidateMixin = dedupeMixin(
get slots() { get slots() {
return { return {
...super.slots, ...super.slots,
feedback: () => document.createElement('lion-validation-feedback'), feedback: () =>
document.createElement(
getScopedTagName('lion-validation-feedback', this.constructor.scopedElements),
),
}; };
} }
@ -181,18 +192,10 @@ export const ValidateMixin = dedupeMixin(
localize.addEventListener('localeChanged', this._updateFeedbackComponent); localize.addEventListener('localeChanged', this._updateFeedbackComponent);
} }
/**
* Should be overridden by subclasses if a different validation-feedback component is used
*/
async _loadFeedbackComponent() {
await import('../lion-validation-feedback.js');
}
firstUpdated(changedProperties) { firstUpdated(changedProperties) {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
this.__validateInitialized = true; this.__validateInitialized = true;
this.validate(); this.validate();
this._loadFeedbackComponent();
} }
updateSync(name, oldValue) { updateSync(name, oldValue) {

903
yarn.lock

File diff suppressed because it is too large Load diff