Skip to content

Commit 2fff8fa

Browse files
crisbetothePunderWoman
authored andcommitted
fix(core): handle invalid classes in class array bindings (#49924)
When binding an array to `class` like `[class]="['foo', 'bar']"`, the runtime treats it the same as a literal binding with all the values being `true`, e.g. `{foo: true, bar: true}`. While object literals can only have string keys, arrays can have any value which can lead to errors if the array contains non-string values. These changes add some logic to stringify the keys and ignore invalid ones. Fixes #48473. PR Close #49924
1 parent 0454158 commit 2fff8fa

File tree

2 files changed

+151
-1
lines changed

2 files changed

+151
-1
lines changed

packages/core/src/render3/instructions/styling.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export function styleStringParser(keyValueArray: KeyValueArray<any>, text: strin
136136
*/
137137
export function ɵɵclassMap(classes: {[className: string]: boolean|undefined|null}|string|undefined|
138138
null): void {
139-
checkStylingMap(keyValueArraySet, classStringParser, classes, true);
139+
checkStylingMap(classKeyValueArraySet, classStringParser, classes, true);
140140
}
141141

142142
/**
@@ -616,6 +616,27 @@ export function styleKeyValueArraySet(keyValueArray: KeyValueArray<any>, key: st
616616
keyValueArraySet(keyValueArray, key, unwrapSafeValue(value));
617617
}
618618

619+
/**
620+
* Class-binding-specific function for setting the `value` for a `key`.
621+
*
622+
* See: `keyValueArraySet` for details
623+
*
624+
* @param keyValueArray KeyValueArray to add to.
625+
* @param key Style key to add.
626+
* @param value The value to set.
627+
*/
628+
export function classKeyValueArraySet(keyValueArray: KeyValueArray<any>, key: unknown, value: any) {
629+
// We use `classList.add` to eventually add the CSS classes to the DOM node. Any value passed into
630+
// `add` is stringified and added to the `class` attribute, e.g. even null, undefined or numbers
631+
// will be added. Stringify the key here so that our internal data structure matches the value in
632+
// the DOM. The only exceptions are empty strings and strings that contain spaces for which
633+
// the browser throws an error. We ignore such values, because the error is somewhat cryptic.
634+
const stringKey = String(key);
635+
if (stringKey !== '' && !stringKey.includes(' ')) {
636+
keyValueArraySet(keyValueArray, stringKey, value);
637+
}
638+
}
639+
619640
/**
620641
* Update map based styling.
621642
*

packages/core/test/acceptance/styling_spec.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,135 @@ describe('styling', () => {
300300
});
301301
});
302302

303+
describe('non-string class keys', () => {
304+
it('should allow null in a class array binding', () => {
305+
@Component({
306+
template: `<div [class]="['a', null, 'c']" [class.extra]="true"></div>`,
307+
standalone: true,
308+
})
309+
class Cmp {
310+
}
311+
312+
const fixture = TestBed.createComponent(Cmp);
313+
fixture.detectChanges();
314+
const div = fixture.nativeElement.querySelector('div');
315+
expect(div.getAttribute('class')).toBe('a c null extra');
316+
});
317+
318+
it('should allow undefined in a class array binding', () => {
319+
@Component({
320+
template: `<div [class]="['a', undefined, 'c']" [class.extra]="true"></div>`,
321+
standalone: true,
322+
})
323+
class Cmp {
324+
}
325+
326+
const fixture = TestBed.createComponent(Cmp);
327+
fixture.detectChanges();
328+
const div = fixture.nativeElement.querySelector('div');
329+
expect(div.getAttribute('class')).toBe('a c undefined extra');
330+
});
331+
332+
it('should allow zero in a class array binding', () => {
333+
@Component({
334+
template: `<div [class]="['a', 0, 'c']" [class.extra]="true"></div>`,
335+
standalone: true,
336+
})
337+
class Cmp {
338+
}
339+
340+
const fixture = TestBed.createComponent(Cmp);
341+
fixture.detectChanges();
342+
const div = fixture.nativeElement.querySelector('div');
343+
expect(div.getAttribute('class')).toBe('0 a c extra');
344+
});
345+
346+
it('should allow false in a class array binding', () => {
347+
@Component({
348+
template: `<div [class]="['a', false, 'c']" [class.extra]="true"></div>`,
349+
standalone: true,
350+
})
351+
class Cmp {
352+
}
353+
354+
const fixture = TestBed.createComponent(Cmp);
355+
fixture.detectChanges();
356+
const div = fixture.nativeElement.querySelector('div');
357+
expect(div.getAttribute('class')).toBe('a c false extra');
358+
});
359+
360+
it('should ignore an empty string in a class array binding', () => {
361+
@Component({
362+
template: `<div [class]="['a', '', 'c']" [class.extra]="true"></div>`,
363+
standalone: true,
364+
})
365+
class Cmp {
366+
}
367+
368+
const fixture = TestBed.createComponent(Cmp);
369+
fixture.detectChanges();
370+
const div = fixture.nativeElement.querySelector('div');
371+
expect(div.getAttribute('class')).toBe('a c extra');
372+
});
373+
374+
it('should ignore a string containing spaces in a class array binding', () => {
375+
@Component({
376+
template: `<div [class]="['a', 'hello there', 'c']" [class.extra]="true"></div>`,
377+
standalone: true,
378+
})
379+
class Cmp {
380+
}
381+
382+
const fixture = TestBed.createComponent(Cmp);
383+
fixture.detectChanges();
384+
const div = fixture.nativeElement.querySelector('div');
385+
expect(div.getAttribute('class')).toBe('a c extra');
386+
});
387+
388+
it('should ignore a string containing spaces in a class object literal binding', () => {
389+
@Component({
390+
template:
391+
`<div [class]="{a: true, 'hello there': true, c: true}" [class.extra]="true"></div>`,
392+
standalone: true,
393+
})
394+
class Cmp {
395+
}
396+
397+
const fixture = TestBed.createComponent(Cmp);
398+
fixture.detectChanges();
399+
const div = fixture.nativeElement.querySelector('div');
400+
expect(div.getAttribute('class')).toBe('a c extra');
401+
});
402+
403+
it('should ignore an object literal in a class array binding', () => {
404+
@Component({
405+
template: `<div [class]="['a', {foo: true}, 'c']" [class.extra]="true"></div>`,
406+
standalone: true,
407+
})
408+
class Cmp {
409+
}
410+
411+
const fixture = TestBed.createComponent(Cmp);
412+
fixture.detectChanges();
413+
const div = fixture.nativeElement.querySelector('div');
414+
expect(div.getAttribute('class')).toBe('a c extra');
415+
});
416+
417+
it('should handle a string array in a class array binding', () => {
418+
@Component({
419+
template: `<div [class]="['a', ['foo', 'bar'], 'c']" [class.extra]="true"></div>`,
420+
standalone: true,
421+
})
422+
class Cmp {
423+
}
424+
425+
const fixture = TestBed.createComponent(Cmp);
426+
fixture.detectChanges();
427+
const div = fixture.nativeElement.querySelector('div');
428+
expect(div.getAttribute('class')).toBe('a c foo,bar extra');
429+
});
430+
});
431+
303432
it('should bind [class] as input to directive', () => {
304433
@Component({
305434
template: `

0 commit comments

Comments
 (0)