Skip to content

Commit 76cfad1

Browse files
authored
feat: implement remaining ClassList methods (#5233)
* feat: implement remaining ClassList methods * test: getter-class-list-modify * chore: bump ci * chore: add failing fixture test for SSRv1
1 parent 0faf4e5 commit 76cfad1

File tree

7 files changed

+67
-22
lines changed

7 files changed

+67
-22
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"entry": "x/getter-class-list",
3+
"ssrFiles": {
4+
"error": "error-ssr.txt",
5+
"expected": "expected-ssr.html"
6+
}
7+
}

packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/error-ssr.txt

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
classList.forEach is not a function
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<fixture-test class="a b c d-e f g h i" data-lwc-host-mutated="class">
2+
<template shadowrootmode="open">
3+
</template>
4+
</fixture-test>

packages/@lwc/engine-server/src/__tests__/fixtures/getter-class-list-modify/expected.html

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from 'vitest';
2+
import { LightningElement } from 'lwc';
3+
4+
export default class GetterClassList extends LightningElement {
5+
connectedCallback() {
6+
const { classList } = this;
7+
8+
classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i');
9+
classList.forEach((value) => {
10+
classList.remove(value);
11+
});
12+
expect(this.getAttribute('class')).toBe('');
13+
expect(this.classList.length).toBe(0);
14+
15+
classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i');
16+
while (classList.length > 0) {
17+
classList.remove(classList.item(0));
18+
}
19+
expect(this.getAttribute('class')).toBe('');
20+
expect(this.classList.length).toBe(0);
21+
22+
classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i');
23+
expect(classList.item(3)).toBe('d-e');
24+
}
25+
}

packages/@lwc/ssr-runtime/src/class-list.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
import type { LightningElement } from './lightning-element';
99

10-
const MULTI_SPACE = /\s+/g;
11-
1210
// Copied from lib.dom
1311
interface DOMTokenList {
1412
readonly length: number;
@@ -28,6 +26,15 @@ interface DOMTokenList {
2826
[index: number]: string;
2927
}
3028

29+
const MULTI_SPACE = /\s+/g;
30+
31+
function parseClassName(className: string | null): string[] {
32+
return (className ?? '')
33+
.split(MULTI_SPACE)
34+
.map((item) => item.trim())
35+
.filter(Boolean);
36+
}
37+
3138
export class ClassList implements DOMTokenList {
3239
el: LightningElement;
3340

@@ -36,22 +43,19 @@ export class ClassList implements DOMTokenList {
3643
}
3744

3845
add(...newClassNames: string[]) {
39-
const className = this.el.className;
40-
const set = new Set(className.split(MULTI_SPACE).filter(Boolean));
46+
const set = new Set(parseClassName(this.el.className));
4147
for (const newClassName of newClassNames) {
4248
set.add(newClassName);
4349
}
4450
this.el.className = Array.from(set).join(' ');
4551
}
4652

4753
contains(className: string) {
48-
const currentClassNameStr = this.el.className;
49-
return currentClassNameStr.split(MULTI_SPACE).includes(className);
54+
return parseClassName(this.el.className).includes(className);
5055
}
5156

5257
remove(...classNamesToRemove: string[]) {
53-
const className = this.el.className;
54-
const set = new Set(className.split(MULTI_SPACE).filter(Boolean));
58+
const set = new Set(parseClassName(this.el.className));
5559
for (const newClassName of classNamesToRemove) {
5660
set.delete(newClassName);
5761
}
@@ -60,8 +64,7 @@ export class ClassList implements DOMTokenList {
6064

6165
replace(oldClassName: string, newClassName: string) {
6266
let classWasReplaced = false;
63-
const className = this.el.className;
64-
const listOfClasses = className.split(MULTI_SPACE).filter(Boolean) as string[];
67+
const listOfClasses = parseClassName(this.el.className);
6568
listOfClasses.forEach((value, idx) => {
6669
if (value === oldClassName) {
6770
classWasReplaced = true;
@@ -73,8 +76,7 @@ export class ClassList implements DOMTokenList {
7376
}
7477

7578
toggle(classNameToToggle: string, force?: boolean) {
76-
const classNameStr = this.el.className;
77-
const set = new Set(classNameStr.split(MULTI_SPACE).filter(Boolean));
79+
const set = new Set(parseClassName(this.el.className));
7880
if (!set.has(classNameToToggle) && force !== false) {
7981
set.add(classNameToToggle);
8082
} else if (set.has(classNameToToggle) && force !== true) {
@@ -93,22 +95,28 @@ export class ClassList implements DOMTokenList {
9395
}
9496

9597
get length(): number {
96-
const currentClassNameStr = this.el.className ?? '';
97-
return currentClassNameStr.split(MULTI_SPACE).length;
98+
return parseClassName(this.el.className).length;
9899
}
99100

100101
// Stubs to satisfy DOMTokenList interface
101102
[index: number]: never; // Can't implement arbitrary index getters without a proxy
102-
item(_index: number): string | null {
103-
throw new Error('Method "item" not implemented.');
104-
}
105-
supports(_token: string): boolean {
106-
throw new Error('Method "supports" not implemented.');
103+
104+
item(index: number): string | null {
105+
return parseClassName(this.el.className ?? '')[index] ?? null;
107106
}
107+
108108
forEach(
109-
_callbackfn: (value: string, key: number, parent: DOMTokenList) => void,
110-
_thisArg?: any
109+
callbackFn: (value: string, key: number, parent: DOMTokenList) => void,
110+
thisArg?: any
111111
): void {
112-
throw new Error('Method "forEach" not implemented.');
112+
parseClassName(this.el.className).forEach((value, index) =>
113+
callbackFn.call(thisArg, value, index, this)
114+
);
115+
}
116+
117+
// This method is present on DOMTokenList but throws an error in the browser when used
118+
// in connection with Element#classList.
119+
supports(_token: string): boolean {
120+
throw new TypeError('DOMTokenList has no supported tokens.');
113121
}
114122
}

0 commit comments

Comments
 (0)