Skip to content

Commit

Permalink
feat: implement remaining ClassList methods (#5233)
Browse files Browse the repository at this point in the history
* feat: implement remaining ClassList methods

* test: getter-class-list-modify

* chore: bump ci

* chore: add failing fixture test for SSRv1
  • Loading branch information
divmain authored Feb 25, 2025
1 parent 0faf4e5 commit 76cfad1
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"entry": "x/getter-class-list",
"ssrFiles": {
"error": "error-ssr.txt",
"expected": "expected-ssr.html"
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
classList.forEach is not a function
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<fixture-test class="a b c d-e f g h i" data-lwc-host-mutated="class">
<template shadowrootmode="open">
</template>
</fixture-test>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect } from 'vitest';
import { LightningElement } from 'lwc';

export default class GetterClassList extends LightningElement {
connectedCallback() {
const { classList } = this;

classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i');
classList.forEach((value) => {
classList.remove(value);
});
expect(this.getAttribute('class')).toBe('');
expect(this.classList.length).toBe(0);

classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i');
while (classList.length > 0) {
classList.remove(classList.item(0));
}
expect(this.getAttribute('class')).toBe('');
expect(this.classList.length).toBe(0);

classList.add('a', 'b', 'c', 'd-e', 'f', 'g', 'h', 'i');
expect(classList.item(3)).toBe('d-e');
}
}
52 changes: 30 additions & 22 deletions packages/@lwc/ssr-runtime/src/class-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

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

const MULTI_SPACE = /\s+/g;

// Copied from lib.dom
interface DOMTokenList {
readonly length: number;
Expand All @@ -28,6 +26,15 @@ interface DOMTokenList {
[index: number]: string;
}

const MULTI_SPACE = /\s+/g;

function parseClassName(className: string | null): string[] {
return (className ?? '')
.split(MULTI_SPACE)
.map((item) => item.trim())
.filter(Boolean);
}

export class ClassList implements DOMTokenList {
el: LightningElement;

Expand All @@ -36,22 +43,19 @@ export class ClassList implements DOMTokenList {
}

add(...newClassNames: string[]) {
const className = this.el.className;
const set = new Set(className.split(MULTI_SPACE).filter(Boolean));
const set = new Set(parseClassName(this.el.className));
for (const newClassName of newClassNames) {
set.add(newClassName);
}
this.el.className = Array.from(set).join(' ');
}

contains(className: string) {
const currentClassNameStr = this.el.className;
return currentClassNameStr.split(MULTI_SPACE).includes(className);
return parseClassName(this.el.className).includes(className);
}

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

replace(oldClassName: string, newClassName: string) {
let classWasReplaced = false;
const className = this.el.className;
const listOfClasses = className.split(MULTI_SPACE).filter(Boolean) as string[];
const listOfClasses = parseClassName(this.el.className);
listOfClasses.forEach((value, idx) => {
if (value === oldClassName) {
classWasReplaced = true;
Expand All @@ -73,8 +76,7 @@ export class ClassList implements DOMTokenList {
}

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

get length(): number {
const currentClassNameStr = this.el.className ?? '';
return currentClassNameStr.split(MULTI_SPACE).length;
return parseClassName(this.el.className).length;
}

// Stubs to satisfy DOMTokenList interface
[index: number]: never; // Can't implement arbitrary index getters without a proxy
item(_index: number): string | null {
throw new Error('Method "item" not implemented.');
}
supports(_token: string): boolean {
throw new Error('Method "supports" not implemented.');

item(index: number): string | null {
return parseClassName(this.el.className ?? '')[index] ?? null;
}

forEach(
_callbackfn: (value: string, key: number, parent: DOMTokenList) => void,
_thisArg?: any
callbackFn: (value: string, key: number, parent: DOMTokenList) => void,
thisArg?: any
): void {
throw new Error('Method "forEach" not implemented.');
parseClassName(this.el.className).forEach((value, index) =>
callbackFn.call(thisArg, value, index, this)
);
}

// This method is present on DOMTokenList but throws an error in the browser when used
// in connection with Element#classList.
supports(_token: string): boolean {
throw new TypeError('DOMTokenList has no supported tokens.');
}
}

0 comments on commit 76cfad1

Please sign in to comment.