Skip to content

Commit

Permalink
Showing 4 changed files with 81 additions and 60 deletions.
16 changes: 0 additions & 16 deletions packages/element/src/lib/attr.test.ts
Original file line number Diff line number Diff line change
@@ -3,22 +3,6 @@ import { expect, fixture, html } from '@open-wc/testing';
import { attr } from './attr.js';

describe('observable: attr()', () => {
it('should write default value to attribute', async () => {
class MyElement extends HTMLElement {
@attr accessor value1 = 'hello'; // no attribute
@attr accessor value2 = 0; // number
@attr accessor value3 = true; // boolean
}

customElements.define('attr-test-1', MyElement);

const el = await fixture(html`<attr-test-1></attr-test-1>`);

expect(el.getAttribute('value1')).to.equal('hello');
expect(el.getAttribute('value2')).to.equal('0');
expect(el.getAttribute('value3')).to.equal('');
});

it('should read and parse the correct values', async () => {
class MyElement extends HTMLElement {
@attr accessor value1 = 100; // no attribute
53 changes: 9 additions & 44 deletions packages/element/src/lib/attr.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,17 @@
// ensure that the metadata symbol exists
(Symbol as any).metadata ??= Symbol('Symbol.metadata');

import { ElementMetadata } from './element';

export function attr<This extends HTMLElement>(
{ get, set }: ClassAccessorDecoratorTarget<This, unknown>,
ctx: ClassAccessorDecoratorContext<This>
): ClassAccessorDecoratorResult<This, any> {
return {
init(value: unknown) {
if (typeof ctx.name === 'string') {
if (this.hasAttribute(ctx.name)) {
const attr = this.getAttribute(ctx.name);

// treat as boolean
if (attr === '') {
return true;
}
ctx.metadata.el ??= new ElementMetadata();
const meta = ctx.metadata.el as ElementMetadata;
meta.attrs.push(String(ctx.name));

// treat as number
if (typeof value === 'number') {
return Number(attr);
}

// treat as string
return attr;
}

/**
* should set attributes AFTER init to allow setup to complete
* this causes attribute changed callback to fire
* If the user attempts to read or write to this property in that cb it will fail
* this also normalizes when the attributeChangedCallback is called in different rendering scenarios
*/
Promise.resolve().then(() => {
const cached = get.call(this);

if (cached !== null && cached !== undefined && cached !== '') {
if (typeof cached === 'boolean') {
if (cached === true) {
// set boolean attribute
this.setAttribute(ctx.name.toString(), '');
}
} else {
// set key/value attribute
this.setAttribute(ctx.name.toString(), String(cached));
}
}
});
}

return value;
},
return {
set(value: unknown) {
if (typeof ctx.name === 'string') {
if (typeof value === 'boolean') {
24 changes: 24 additions & 0 deletions packages/element/src/lib/element.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, fixture, html } from '@open-wc/testing';

import { attr } from './attr.js';
import { element } from './element.js';
import { tagName } from './tag-name.js';

describe('element()', () => {
it('should write default value to attribute', async () => {
@element
class MyElement extends HTMLElement {
@tagName static tag = 'element-1';

@attr accessor value1 = 'hello'; // no attribute
@attr accessor value2 = 0; // number
@attr accessor value3 = true; // boolean
}

const el = await fixture<MyElement>(html`<element-1></element-1>`);

expect(el.getAttribute('value1')).to.equal('hello');
expect(el.getAttribute('value2')).to.equal('0');
expect(el.getAttribute('value3')).to.equal('');
});
});
48 changes: 48 additions & 0 deletions packages/element/src/lib/element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export class ElementMetadata {
attrs: string[] = [];
}

export interface ElementCtx {
metadata: {
el: ElementMetadata;
};
}

export function element<T extends new (...args: any[]) => HTMLElement>(
Base: T,
ctx: ClassDecoratorContext<T>
) {
const { metadata } = ctx as unknown as ElementCtx;

return class JoistElement extends Base {
// make all attrs observable
static observedAttributes = [...metadata.el.attrs];

constructor(...args: any[]) {
super(...args);
}

connectedCallback() {
for (let attr of metadata.el.attrs) {
const value = Reflect.get(this, attr);

// reflect values back to attributes
if (value !== null && value !== undefined && value !== '') {
if (typeof value === 'boolean') {
if (value === true) {
// set boolean attribute
this.setAttribute(attr, '');
}
} else {
// set key/value attribute
this.setAttribute(attr, String(value));
}
}
}

if (super.connectedCallback) {
super.connectedCallback();
}
}
};
}

0 comments on commit 398a00e

Please sign in to comment.