Skip to content

Commit

Permalink
fix: (core) applyCssClass decorator (#3328)
Browse files Browse the repository at this point in the history
* fix(core): apply class names decorator

* fix typo

* typo
  • Loading branch information
dimamarksman committed Sep 21, 2020
1 parent a29828c commit f452eb6
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 27 deletions.
51 changes: 33 additions & 18 deletions libs/core/src/lib/utils/decorators/apply-css-class.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,45 +17,60 @@ export function applyCssClass(target: any, propertyKey: string, descriptor: Prop
throw ELEMENT_REF_EXCEPTION;
}

const newComponentClassList: string[] = sanitize(originalMethod.apply(this));
const classListToApply: string[] = sanitize(originalMethod.apply(this));

const elementRef = this.elementRef.apply(this);

if (elementRef) {
if (!elementRef.nativeElement._classMap) {
elementRef.nativeElement._classMap = {};
const nativeElement: HTMLElement & { _classMap?: any } = elementRef?.nativeElement;

if (nativeElement) {
if (!nativeElement._classMap) {
nativeElement._classMap = {};
}

if (!this._uuidv4) {
this._uuidv4 = uuidv4();
elementRef.nativeElement._classMap[this._uuidv4] = newComponentClassList;
}

const allClassList = [...elementRef.nativeElement.classList];
const previousComponentClassList = elementRef.nativeElement._classMap[this._uuidv4] || [];
const newClassList = updateComponentClassList(allClassList, previousComponentClassList, newComponentClassList);
const currentClassList = Array.from(nativeElement.classList);

const previousClassListToApply = nativeElement._classMap[this._uuidv4] || [];

const newClassList = createComponentClassList(currentClassList, previousClassListToApply, classListToApply);

elementRef.nativeElement._classMap[this._uuidv4] = newComponentClassList;
(elementRef.nativeElement as HTMLElement).className = newClassList.join(' ');
nativeElement.className = newClassList.join(' ');

nativeElement._classMap[this._uuidv4] = classListToApply;
}

return newComponentClassList;
return classListToApply;
};
}

/** Splits merged classes and removes falsy elements from string array */
function sanitize(array: string[]): string [] {
/** Filter list to unique items */
function unique(value: unknown, index: number, list: unknown[]): boolean {
return list.indexOf(value) === index;
}

/** Splits merged classes, removes falsy elements and leaves only unique items */
function sanitize(array: string[]): string[] {
return array
.filter(Boolean)
.reduce((classList: string[], cssClass: string) => [...classList, ...cssClass.split(' ')], []);
.reduce((classList: string[], cssClass: string) => [...classList, ...cssClass.split(/\s+/)], [])
.filter(unique);
}

/** Returns an array1[index] of first array1 and array2 shared element */
function firstCommonElementIndex(array1: string[], array2: string[]): number {
return array1.findIndex(element => array2.indexOf(element) !== -1);
return array1.findIndex((element) => array2.indexOf(element) !== -1);
}

/** Replaces previous set of component classes with new set of component classes */
function updateComponentClassList(allClasses: string[], previousComponentClassList: string[], newComponentClassList: string[]): string[] {
/** Create set of component classes based on previous set and new set */
function createComponentClassList(
allClasses: string[],
previousComponentClassList: string[],
newComponentClassList: string[]
): string[] {
allClasses = allClasses.slice();
let index = firstCommonElementIndex(allClasses, previousComponentClassList);
index = index === -1 ? 0 : index;
allClasses.splice(index, previousComponentClassList.length, ...newComponentClassList);
Expand Down
74 changes: 65 additions & 9 deletions libs/core/src/lib/utils/decorators/apply-css-class.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component, Directive, OnChanges, OnInit, ElementRef, ViewChild } from '@angular/core';
import { Component, Directive, OnChanges, OnInit, ElementRef, ViewChild, ContentChild, Renderer2 } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { CommonModule } from '@angular/common';

import { ButtonComponent } from '../../button/public_api';
import { CssClassBuilder, applyCssClass } from '../../utils/public_api';

const testDirectiveClass = 'fd-test-directive';
const TEST_DIRECTIVE_CLASS = 'fd-test-directive';

@Directive({
// tslint:disable-next-line: directive-selector
Expand All @@ -25,20 +26,49 @@ export class TestDirective implements OnInit, OnChanges, CssClassBuilder {

@applyCssClass
buildComponentCssClass(): string[] {
return [ testDirectiveClass ];
return [TEST_DIRECTIVE_CLASS];
}

elementRef(): ElementRef<any> {
return this._elementRef;
}
}

const TEST_CONTENT_CHILD_PHASE_CLASS_NAME = 'content-child-phase-class-name';

@Component({
selector: 'fd-test-proxy-component',
template: '<ng-content></ng-content>'
})
export class TestProxyComponent {
@ContentChild(ButtonComponent, { read: ElementRef })
set buttonElementRef(elementRef: ElementRef<HTMLElement>) {
this._renderer.addClass(elementRef?.nativeElement, TEST_CONTENT_CHILD_PHASE_CLASS_NAME);
}

constructor(private _renderer: Renderer2) {}
}

const TEST_VIEW_CHILD_PHASE_CLASS_NAME = 'view-child-phase-class-name';

@Component({
selector: 'fd-test-component',
template: '<button #element fd-test-directive fd-button>Button</button>'
template: `
<fd-test-proxy-component>
<button class="initial-button-class" #element fd-test-directive fd-button>Button</button>
</fd-test-proxy-component>
`
})
export class TestComponent {
@ViewChild('element') element: ElementRef;
@ViewChild('element')
element: ButtonComponent;

@ViewChild('element', { read: ElementRef })
set viewChildClassName(elementRef: ElementRef<HTMLElement>) {
this._renderer.addClass(elementRef?.nativeElement, TEST_VIEW_CHILD_PHASE_CLASS_NAME);
}

constructor(private _renderer: Renderer2) {}
}

describe('ButtonComponent', () => {
Expand All @@ -48,30 +78,56 @@ describe('ButtonComponent', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestDirective, ButtonComponent, TestComponent]
imports: [CommonModule],
declarations: [TestDirective, ButtonComponent, TestComponent, TestProxyComponent]
});
}));

beforeEach(async () => {
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
await fixture.whenStable();
buttonInstance = (fixture.componentInstance.element as unknown) as ButtonComponent;
testDirectiveInstance = (fixture.componentInstance.element as unknown) as TestDirective;
});

beforeEach(() => {
/**
* We have to check that calling buildComponentCssClass() in a row
* at the beginning does not impact on expected result
*/
buttonInstance.buildComponentCssClass();
buttonInstance.buildComponentCssClass();
});

it('should keep initial class', async () => {
const componentClasses = (buttonInstance.elementRef().nativeElement as HTMLElement).className;
expect(componentClasses).toContain('initial-button-class');
});

it('should handle styles for 2 directives', async () => {
buttonInstance.compact = true;
buttonInstance.fdType = 'standard';

buttonInstance.ngOnInit();
testDirectiveInstance.ngOnInit();

await fixture.whenStable();

const componentClasses = (buttonInstance.elementRef().nativeElement as HTMLElement).className;

expect(componentClasses).toContain('standard');
expect(componentClasses).toContain('compact');
expect(componentClasses).toContain(testDirectiveClass);
expect(componentClasses).toContain(TEST_DIRECTIVE_CLASS);
});

it('should keep classes added at @ContentChild phase', async () => {
const componentClasses = (buttonInstance.elementRef().nativeElement as HTMLElement).className;

expect(componentClasses).toContain(TEST_CONTENT_CHILD_PHASE_CLASS_NAME);
});

it('should keep classes added at @ViewChild phase', async () => {
const componentClasses = (buttonInstance.elementRef().nativeElement as HTMLElement).className;

expect(componentClasses).toContain(TEST_VIEW_CHILD_PHASE_CLASS_NAME);
});
});

0 comments on commit f452eb6

Please sign in to comment.