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.
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
Feel free to create a github issue for any feedback or questions you might have.

View file

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

View file

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

View file

@ -1,14 +1,22 @@
import '@lion/calendar/lion-calendar.js';
import { html, ifDefined, render } from '@lion/core';
import { LionCalendar } from '@lion/calendar/src/LionCalendar';
import { html, ifDefined, ScopedElementsMixin } from '@lion/core';
import { LionInputDate } from '@lion/input-date';
import { OverlayMixin, withModalDialogConfig } from '@lion/overlays';
import './lion-calendar-overlay-frame.js';
import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
/**
* @customElement lion-input-datepicker
* @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() {
return {
/**
@ -47,7 +55,10 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
...super.slots,
[this._calendarInvokerSlot]: () => {
const renderParent = document.createElement('div');
render(this._invokerTemplate(), renderParent);
this.constructor.render(this._invokerTemplate(), renderParent, {
scopeName: this.localName,
eventContext: this,
});
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.
// This would make first paint quicker
return html`
<lion-calendar-overlay-frame>
<lion-calendar-overlay-frame class="calendar__overlay-frame">
<span slot="heading">${this.calendarHeading}</span>
${this._calendarTemplate()}
</lion-calendar-overlay-frame>
@ -349,7 +360,7 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
if (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;
}
}

View file

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

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 { ChoiceInputMixin } from '@lion/choice-input';
import { LionSwitchButton } from './LionSwitchButton.js';
import '../lion-switch-button.js';
export class LionSwitch extends ChoiceInputMixin(LionField) {
export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField)) {
static get styles() {
return [
super.styles,
@ -16,10 +15,20 @@ export class LionSwitch extends ChoiceInputMixin(LionField) {
];
}
static get scopedElements() {
return {
...super.scopedElements,
'lion-switch-button': LionSwitchButton,
};
}
get slots() {
return {
...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": {
"@lion/core": "0.4.4",
"@lion/localize": "0.8.9"
"@lion/localize": "0.8.9",
"@open-wc/scoped-elements": "^0.5.0"
},
"devDependencies": {
"@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 */
import { dedupeMixin, SlotMixin } from '@lion/core';
import { dedupeMixin, SlotMixin, ScopedElementsMixin, getScopedTagName } from '@lion/core';
import { localize } from '@lion/localize';
import { LionValidationFeedback } from './LionValidationFeedback.js';
import { ResultValidator } from './ResultValidator.js';
import { Unparseable } from './Unparseable.js';
import { AsyncQueue } from './utils/AsyncQueue.js';
@ -24,7 +25,14 @@ function arrayDiff(array1 = [], array2 = []) {
export const ValidateMixin = dedupeMixin(
superclass =>
// 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() {
return {
/**
@ -108,7 +116,10 @@ export const ValidateMixin = dedupeMixin(
get slots() {
return {
...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);
}
/**
* Should be overridden by subclasses if a different validation-feedback component is used
*/
async _loadFeedbackComponent() {
await import('../lion-validation-feedback.js');
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.__validateInitialized = true;
this.validate();
this._loadFeedbackComponent();
}
updateSync(name, oldValue) {

903
yarn.lock

File diff suppressed because it is too large Load diff