Skip to content

Commit

Permalink
feat(ssr): template class binding @ W-17219264 (#4931)
Browse files Browse the repository at this point in the history
  • Loading branch information
cardoso authored Nov 25, 2024
1 parent fb163f7 commit c257220
Show file tree
Hide file tree
Showing 15 changed files with 121 additions and 50 deletions.
44 changes: 2 additions & 42 deletions packages/@lwc/engine-core/src/framework/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ import {
isTrue,
isUndefined,
StringReplace,
StringTrim,
toString,
keys as ObjectKeys,
sanitizeHtmlContent,
normalizeClass,
} from '@lwc/shared';

import { logError } from '../shared/logger';
Expand Down Expand Up @@ -717,46 +716,7 @@ function shc(content: unknown): SanitizedHtmlContent {
return createSanitizedHtmlContent(sanitizedString);
}

/**
* [ncls] - Normalize class name attribute.
*
* Transforms the provided class property value from an object/string into a string the diffing algo
* can operate on.
*
* This implementation is borrowed from Vue:
* https://github.com/vuejs/core/blob/e790e1bdd7df7be39e14780529db86e4da47a3db/packages/shared/src/normalizeProp.ts#L63-L82
*/
function ncls(value: unknown): string | undefined {
if (isUndefined(value) || isNull(value)) {
// Returning undefined here improves initial render cost, because the old vnode's class will be considered
// undefined in the `patchClassAttribute` routine, so `oldClass === newClass` will be true so we return early
return undefined;
}

let res = '';

if (isString(value)) {
res = value;
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
const normalized = ncls(value[i]);
if (normalized) {
res += normalized + ' ';
}
}
} else if (isObject(value) && !isNull(value)) {
// Iterate own enumerable keys of the object
const keys = ObjectKeys(value);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if ((value as Record<string, unknown>)[key]) {
res += key + ' ';
}
}
}

return StringTrim.call(res);
}
const ncls = normalizeClass;

const api = ObjectFreeze({
s,
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<x-parent>
<template shadowrootmode="open">
<button class="button__icon">
</button>
</template>
</x-parent>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const tagName = 'x-parent';
export { default } from 'x/parent';
export * from 'x/parent';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<button class={computedClassNames}></button>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LightningElement } from 'lwc';

export default class CustomButton extends LightningElement {
get computedClassNames() {
return [{
button__icon: true
}]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<x-parent>
<template shadowrootmode="open">
<button class="button__icon">
</button>
</template>
</x-parent>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const tagName = 'x-parent';
export { default } from 'x/parent';
export * from 'x/parent';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<button class={computedClassNames}></button>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LightningElement } from 'lwc';

export default class CustomButton extends LightningElement {
get computedClassNames() {
return {
button__icon: true
}
}
}
1 change: 1 addition & 0 deletions packages/@lwc/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './html-attributes';
export * from './html-escape';
export * from './meta';
export * from './namespaces';
export * from './normalize-class';
export * from './overridable-hooks';
export * from './static-part-tokens';
export * from './style';
Expand Down
48 changes: 48 additions & 0 deletions packages/@lwc/shared/src/normalize-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { isArray, isObject, isString, isUndefined, keys, isNull, StringTrim } from './language';

/**
* [ncls] - Normalize class name attribute.
*
* Transforms the provided class property value from an object/string into a string the diffing algo
* can operate on.
*
* This implementation is borrowed from Vue:
* https://github.com/vuejs/core/blob/e790e1bdd7df7be39e14780529db86e4da47a3db/packages/shared/src/normalizeProp.ts#L63-L82
*/
export function normalizeClass(value: unknown): string | undefined {
if (isUndefined(value) || isNull(value)) {
// Returning undefined here improves initial render cost, because the old vnode's class will be considered
// undefined in the `patchClassAttribute` routine, so `oldClass === newClass` will be true so we return early
return undefined;
}

let res = '';

if (isString(value)) {
res = value;
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i]);
if (normalized) {
res += normalized + ' ';
}
}
} else if (isObject(value) && !isNull(value)) {
// Iterate own enumerable keys of the object
const _keys = keys(value);
for (let i = 0; i < _keys.length; i += 1) {
const key = _keys[i];
if ((value as Record<string, unknown>)[key]) {
res += key + ' ';
}
}
}

return StringTrim.call(res);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ const bYieldDynamicValue = esTemplateWithYield`
let attrValue = ${/* attribute value expression */ is.expression};
const isHtmlBooleanAttr = ${/* isHtmlBooleanAttr */ is.literal};
const shouldRenderScopeToken = attrName === 'class' &&
(hasScopedStylesheets || hasScopedStaticStylesheets(Cmp));
const prefix = shouldRenderScopeToken ? stylesheetScopeToken + ' ' : '';
// Global HTML boolean attributes are specially coerced into booleans
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/template-compiler/src/codegen/index.ts#L450-L454
if (isHtmlBooleanAttr) {
Expand All @@ -66,13 +62,26 @@ const bYieldDynamicValue = esTemplateWithYield`
if (attrValue !== undefined && attrValue !== null) {
yield ' ' + attrName;
if (attrValue !== '') {
yield \`="\${prefix}\${htmlEscape(String(attrValue), true)}"\`;
yield \`="\${htmlEscape(String(attrValue), true)}"\`;
}
}
}
`<EsBlockStatement>;

const bYieldClassDynamicValue = esTemplateWithYield`
{
const attrValue = normalizeClass(${/* attribute value expression */ is.expression});
const shouldRenderScopeToken = hasScopedStylesheets || hasScopedStaticStylesheets(Cmp);
if (attrValue) {
const prefix = shouldRenderScopeToken ? stylesheetScopeToken + ' ' : '';
yield \` class="\${prefix}\${htmlEscape(String(attrValue), true)}"\`;
}
}
`<EsBlockStatement>;

// TODO [#4714]: scope token renders as a suffix for literals, but prefix for expressions
const bStringLiteralYield = esTemplateWithYield`
{
Expand Down Expand Up @@ -136,9 +145,20 @@ function yieldAttrOrPropDynamicValue(
cxt: TransformerContext
): EsStatement[] {
cxt.import('htmlEscape');
const isHtmlBooleanAttr = isBooleanAttribute(name, elementName);
const scopedExpression = getScopedExpression(value as EsExpression, cxt);
return [bYieldDynamicValue(b.literal(name), scopedExpression, b.literal(isHtmlBooleanAttr))];
switch (name) {
case 'class':
cxt.import('normalizeClass');
return [bYieldClassDynamicValue(scopedExpression)];
default:
return [
bYieldDynamicValue(
b.literal(name),
scopedExpression,
b.literal(isBooleanAttribute(name, elementName))
),
];
}
}

function reorderAttributes(
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/ssr-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// Must be first so that later exports take precedence
export * from './stubs';

export { htmlEscape, setHooks, sanitizeHtmlContent } from '@lwc/shared';
export { htmlEscape, setHooks, sanitizeHtmlContent, normalizeClass } from '@lwc/shared';

export { ClassList } from './class-list';
export {
Expand Down

0 comments on commit c257220

Please sign in to comment.