diff --git a/.changeset/old-tables-own.md b/.changeset/old-tables-own.md new file mode 100644 index 0000000000..6905c92bdf --- /dev/null +++ b/.changeset/old-tables-own.md @@ -0,0 +1,6 @@ +--- +'@lit/reactive-element': patch +'lit': patch +--- + +Bind `this` to custom attribute converter methods diff --git a/packages/reactive-element/src/reactive-element.ts b/packages/reactive-element/src/reactive-element.ts index a5d58f32f0..4cd1838c3a 100644 --- a/packages/reactive-element/src/reactive-element.ts +++ b/packages/reactive-element/src/reactive-element.ts @@ -1069,10 +1069,12 @@ export abstract class ReactiveElement this.constructor as typeof ReactiveElement ).__attributeNameForProperty(name, options); if (attr !== undefined && options.reflect === true) { - const toAttribute = - (options.converter as ComplexAttributeConverter)?.toAttribute ?? - defaultConverter.toAttribute; - const attrValue = toAttribute!(value, options.type); + const converter = + (options.converter as ComplexAttributeConverter)?.toAttribute !== + undefined + ? (options.converter as ComplexAttributeConverter) + : defaultConverter; + const attrValue = converter.toAttribute!(value, options.type); if ( DEV_MODE && (this.constructor as typeof ReactiveElement).enabledWarnings!.indexOf( @@ -1117,17 +1119,19 @@ export abstract class ReactiveElement // if it was just set because the attribute changed. if (propName !== undefined && this.__reflectingProperty !== propName) { const options = ctor.getPropertyOptions(propName); - const converter = options.converter; - const fromAttribute = - (converter as ComplexAttributeConverter)?.fromAttribute ?? - (typeof converter === 'function' - ? (converter as (value: string | null, type?: unknown) => unknown) - : null) ?? - defaultConverter.fromAttribute; + const converter = + typeof options.converter === 'function' + ? {fromAttribute: options.converter} + : options.converter?.fromAttribute !== undefined + ? options.converter + : defaultConverter; // mark state reflecting this.__reflectingProperty = propName; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this[propName as keyof this] = fromAttribute!(value, options.type) as any; + this[propName as keyof this] = converter.fromAttribute!( + value, + options.type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any; // mark state not reflecting this.__reflectingProperty = null; } diff --git a/packages/reactive-element/src/test/reactive-element_test.ts b/packages/reactive-element/src/test/reactive-element_test.ts index b998a24af9..db2da449d4 100644 --- a/packages/reactive-element/src/test/reactive-element_test.ts +++ b/packages/reactive-element/src/test/reactive-element_test.ts @@ -302,6 +302,79 @@ suite('ReactiveElement', () => { assert.equal(el.getAttribute('foo'), 'toAttribute: FooType'); }); + test('property option `converter` can use a class instance', async () => { + class IntegerAttributeConverter + implements ComplexAttributeConverter + { + private _defaultValue: Number; + + constructor(defaultValue: Number) { + this._defaultValue = defaultValue; + } + + toAttribute(value: Number, _type?: unknown): unknown { + if (!value) { + return this._defaultValue; + } + return `${value}`; + } + + fromAttribute(value: string | null, _type?: unknown): Number { + if (!value) { + return this._defaultValue; + } + + const parsedValue = Number.parseInt(value, 10); + if (isNaN(parsedValue)) { + return this._defaultValue; + } + return parsedValue; + } + } + + const defaultIntAttrConverterVal = 1; + + class E extends ReactiveElement { + static override get properties() { + return { + num: { + type: Number, + converter: new IntegerAttributeConverter( + defaultIntAttrConverterVal + ), + reflect: true, + }, + }; + } + + num?: number; + } + + customElements.define(generateElementName(), E); + const el = new E(); + container.appendChild(el); + await el.updateComplete; + + assert.equal(el.getAttribute('num'), null); + assert.equal(el.num, undefined); + + el.setAttribute('num', 'notANumber'); + await el.updateComplete; + assert.equal(el.num, defaultIntAttrConverterVal); + + el.num = 10; + await el.updateComplete; + assert.equal(el.getAttribute('num'), '10'); + + el.setAttribute('num', '5'); + await el.updateComplete; + assert.equal(el.num, 5); + + el.num = undefined; + await el.updateComplete; + assert.equal(el.getAttribute('num'), `${defaultIntAttrConverterVal}`); + }); + test('property/attribute values when attributes removed', async () => { class E extends ReactiveElement { static override get properties() {