diff --git a/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts b/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts index 99268d4430..f9b799f7bf 100644 --- a/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts +++ b/packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts @@ -58,21 +58,43 @@ function bReflectedAttrsObj(reflectedPropNames: (keyof typeof AriaPropNameToAttr // // The props object will be kept up-to-date with any new values set on the corresponding // property name in the component instance. - const reflectedAttrGetters: Property[] = reflectedPropNames.map((propName) => - b.property( - 'get', - b.literal(AriaPropNameToAttrNameMap[propName]), - b.functionExpression( - null, - [], - b.blockStatement([ - b.returnStatement( - b.memberExpression(b.identifier('props'), b.identifier(propName)) - ), - ]) + const reflectedAttrAccessors: Property[] = []; + for (const propName of reflectedPropNames) { + reflectedAttrAccessors.push( + b.property( + 'get', + b.literal(AriaPropNameToAttrNameMap[propName]), + b.functionExpression( + null, + [], + b.blockStatement([ + b.returnStatement( + b.callExpression(b.identifier('String'), [ + b.memberExpression(b.identifier('props'), b.identifier(propName)), + ]) + ), + ]) + ) + ), + b.property( + 'set', + b.literal(AriaPropNameToAttrNameMap[propName]), + b.functionExpression( + null, + [b.identifier('val')], + b.blockStatement([ + b.expressionStatement( + b.assignmentExpression( + '=', + b.memberExpression(b.identifier('props'), b.identifier(propName)), + b.identifier('val') + ) + ), + ]) + ) ) - ) - ); + ); + } // This mutates the `attrs` object, adding the reflected aria attributes that have been // detected. Example: @@ -87,7 +109,7 @@ function bReflectedAttrsObj(reflectedPropNames: (keyof typeof AriaPropNameToAttr b.assignmentExpression( '=', b.identifier('attrs'), - b.objectExpression([b.spreadElement(b.identifier('attrs')), ...reflectedAttrGetters]) + b.objectExpression([b.spreadElement(b.identifier('attrs')), ...reflectedAttrAccessors]) ) ); } diff --git a/packages/@lwc/ssr-runtime/src/index.ts b/packages/@lwc/ssr-runtime/src/index.ts index 0ff2992d23..a079396d96 100644 --- a/packages/@lwc/ssr-runtime/src/index.ts +++ b/packages/@lwc/ssr-runtime/src/index.ts @@ -172,20 +172,32 @@ export class LightningElement implements PropsAvailableAtConstruction { return (this.__classList = new ClassList(this)); } - getAttribute(attrName: string): string | null { - return this.__attrs[attrName] ?? null; + #setAttribute(attrName: string, attrValue: string | null): void { + this.__attrs[attrName] = attrValue; } - setAttribute(attrName: string, value: string | null): void { - this.__attrs[attrName] = String(value); + setAttribute(attrName: string, attrValue: unknown): void { + this.#setAttribute(attrName, String(attrValue)); } - hasAttribute(attrName: string): boolean { - return Boolean(this.__attrs && attrName in this.__attrs); + getAttribute(attrName: unknown): string | null { + if (this.hasAttribute(attrName)) { + return this.__attrs[attrName as string]; + } + return null; + } + + hasAttribute(attrName: unknown): boolean { + return typeof attrName === 'string' && typeof this.__attrs[attrName] === 'string'; } removeAttribute(attrName: string): void { - this.__attrs[attrName] = null; + if (this.hasAttribute(attrName)) { + // Reflected attributes use accessor methods to update their + // corresponding properties so we can't simply `delete`. Instead, + // we use `null` when we want to remove. + this.#setAttribute(attrName, null); + } } addEventListener( @@ -299,11 +311,12 @@ export function* renderAttrs(attrs: Attributes) { if (!attrs) { return; } - for (const [key, val] of Object.entries(attrs)) { - if (typeof val === 'string') { - yield val === '' ? ` ${key}` : ` ${key}="${escapeAttrVal(val)}"`; - } else if (val === null) { - return ''; + for (const attrName of Object.getOwnPropertyNames(attrs)) { + const attrVal = attrs[attrName]; + if (typeof attrVal === 'string') { + yield attrVal === '' ? ` ${attrName}` : ` ${attrName}="${escapeAttrVal(attrVal)}"`; + } else if (attrVal === null) { + yield ''; } } }