Skip to content

Commit

Permalink
Merge pull request #558 from aurelia/classAttr
Browse files Browse the repository at this point in the history
 feat(observer): Add the ability to bind an array of objects and strings to a class
  • Loading branch information
fkleuver committed Aug 16, 2019
2 parents 60a2451 + 80fd26b commit cd94c43
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 23 deletions.
11 changes: 11 additions & 0 deletions docs/user-docs/1. getting-started/3. displaying-basic-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,21 @@ String interpolation can be used within HTML attributes as an alternative to `to

You can bind an element's `class` attribute using string interpolation or with `.bind`/`.to-view`.

This binding will accept a string, an array, or a single object and coerce them into a class string. If a string is passed it takes the literal value, when an object is passed each property value is coerced to bool and if true the property name is used, and when an array is passed it iterates each value in the same behavior as string or object.
```typescript
['class1', 'class2']; // <div class='class1 class2'></div>
['class1', { class2: true, 'class3 class4':true, class5: false}]; // <div class='class1 class2 class3 class4'></div>
{ class1: true, 'class2 class4': true, class3 : false}; // <div class='class1 class2 class4'></div>
{ class1: true,
['innerArray', {innerArray1:true, innerArray2: false}],
'class2 class4': true,
class3 : false}; // <div class='class1 class2 class4 innerArray innerArray1'></div>
```
```HTML Class Binding
<template>
<div class="foo ${isActive ? 'active' : ''} bar"></div>
<div class.bind="isActive ? 'active' : ''"></div>
<div class.bind="isActive ? 'active' : ''"></div>
<div class.one-time="isActive ? 'active' : ''"></div>
</template>
```
Expand Down
18 changes: 15 additions & 3 deletions packages/__tests__/runtime-html/target-observers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,21 @@ describe('ClassAccessor', function () {
}
}

if (secondClassList instanceof Array) {
for (const cls of secondClassList) {
assertClassChanges(initialClassList, classList, cls, secondUpdatedClassList);
}
return;
}

if (secondClassList instanceof Object) {
for (const cls in secondClassList) {
if (!!secondClassList[cls]) {
assert.includes(secondUpdatedClassList, cls, `secondUpdatedClassList includes class from secondClassList "${JSON.stringify(secondClassList)}"`);
continue;
}
if (!initialClasses.includes(cls)) {
assert.notIncludes(secondUpdatedClassList, cls, `secondUpdatedClassList does not exclude class from initial class prop but does if not secondClassList "${JSON.stringify(secondClassList)}"`);
assert.notIncludes(secondUpdatedClassList, cls, `secondUpdatedClassList ${JSON.stringify(secondUpdatedClassList)} does not exclude class ${cls} from initial class prop but does if not secondClassList "${JSON.stringify(secondClassList)}"`);
}
}
}
Expand All @@ -295,10 +302,15 @@ describe('ClassAccessor', function () {
'<div></div>',
'<div class=""></div>',
'<div class="foo"></div>',
'<div class="foo bar baz"></div>'
'<div class="foo bar baz"></div>',
'<div class="foo bar baz qux"></div>'
];
const classListArr = ['', 'foo', 'foo bar ', ' bar baz', 'qux', 'bar qux', 'qux quux'];
const secondClassListArr = ['', 'fooo ', { fooo: true }, { fooo: 'true' }, { fooo: true, baaar: false }, { fooo: 'true', baaar: 'false' }, { foo: true, bar: false, fooo: true }, { foo: false, bar: false }];
const secondClassListArr = ['', 'fooo ', { fooo: true }, { fooo: 'true' }, { fooo: true, baaar: false },
{ fooo: 'true', baaar: 'false' }, { foo: true, bar: false, fooo: true }, { foo: false, bar: false },
{ 'fooo baaar': true, 'baar': true, 'fono': false },
['fooo', ['bar', { baz: true }], 'bazz'],
['fooo', { baar: true }, 'bazz'], []]; //empty array test
for (const markup of markupArr) {
for (const classList of classListArr) {

Expand Down
67 changes: 47 additions & 20 deletions packages/runtime-html/src/observation/class-attribute-accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
LifecycleFlags,
Priority,
} from '@aurelia/runtime';
import { PLATFORM } from '@aurelia/kernel';

export class ClassAttributeAccessor implements IAccessor<unknown> {
public readonly lifecycle: ILifecycle;
Expand Down Expand Up @@ -62,17 +63,13 @@ export class ClassAttributeAccessor implements IAccessor<unknown> {
const { currentValue, nameIndex } = this;
let { version } = this;
this.oldValue = currentValue;
if (currentValue instanceof Object) {
const classesToAdd = this.getClassesToAdd(currentValue as Record<string, unknown>);
if (classesToAdd.length > 0) {
this.addClassesAndUpdateIndex(classesToAdd);
}
} else if (typeof currentValue === 'string' && currentValue.length) {
// Get strings split on a space not including empties
const classNames = currentValue.match(/\S+/g);
if (classNames != null && classNames.length > 0) {
this.addClassesAndUpdateIndex(classNames);
}

// tslint:disable-next-line: no-any
const classesToAdd = this.getClassesToAdd(currentValue as any);

// Get strings split on a space not including empties
if (classesToAdd.length > 0) {
this.addClassesAndUpdateIndex(classesToAdd);
}

this.version += 1;
Expand Down Expand Up @@ -110,17 +107,47 @@ export class ClassAttributeAccessor implements IAccessor<unknown> {
}
}

private getClassesToAdd(object: Record<string, unknown>): string[] {
const returnVal: string[] = [];
for (const property in object) {
// Let non typical values also evaluate true so disable bool check
// tslint:disable-next-line: strict-boolean-expressions
if (!!object[property]) {
returnVal.push(property);
continue;
private splitClassString(classString: string): string[] {
const matches = classString.match(/\S+/g);
if (matches === null) {
return PLATFORM.emptyArray;
}
return matches;
}

private getClassesToAdd(object: Record<string, unknown> | [] | string): string[] {
if (typeof object === 'string') {
return this.splitClassString(object);
}

if (object instanceof Array) {
const len = object.length;
if (len > 0) {
const classes: string[] = [];
for (let i = 0; i < len; ++i) {
classes.push(...this.getClassesToAdd(object[i]));
}
return classes;
} else {
return PLATFORM.emptyArray;
}
} else if (object instanceof Object) {
const classes: string[] = [];
for (const property in object) {
// Let non typical values also evaluate true so disable bool check
// tslint:disable-next-line: strict-boolean-expressions
if (!!object[property]) {
// We must do this in case object property has a space in the name which results in two classes
if (property.indexOf(' ') >= 0) {
classes.push(...this.splitClassString(property));
} else {
classes.push(property);
}
}
}
return classes;
}
return returnVal;
return PLATFORM.emptyArray;
}

private addClassesAndUpdateIndex(classes: string[]) {
Expand Down

0 comments on commit cd94c43

Please sign in to comment.