Skip to content

Commit

Permalink
feat: added decorators and interfaces to replace AbstractFdNgxClass (#…
Browse files Browse the repository at this point in the history
…1770)

* upstream synced with fork

* added decorators and interfaces instead of abstract class

* decorators and interfaces added replacing abstract class

* small improvements

* added changes acording to comments

* Update NEW_COMPONENT.md

* Update NEW_COMPONENT.md

* Update NEW_COMPONENT.md

* Update README.md

* changelod sync with upstream

* fixing pr comments and unit tests
  • Loading branch information
rengare authored and JKMarkowski committed Jan 3, 2020
1 parent ad56ab9 commit d5893eb
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 54 deletions.
32 changes: 13 additions & 19 deletions libs/core/src/lib/button/button.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { By } from '@angular/platform-browser';
selector: 'fd-test-component',
template: '<button fd-button>Button</button>'
})
export class TestComponent {}
export class TestComponent { }

describe('ButtonComponent', () => {
let fixture: ComponentFixture<TestComponent>,
debugElement: DebugElement,
element: HTMLElement;

let component, componentInstance;
let component, componentInstance: ButtonComponent;

beforeEach(async(() => {
TestBed.configureTestingModule({
Expand All @@ -29,35 +29,29 @@ describe('ButtonComponent', () => {
fixture.detectChanges();
component = debugElement.query(By.directive(ButtonComponent));
componentInstance = component.injector.get(ButtonComponent);

spyOn(componentInstance, '_setProperties').and.callThrough();
spyOn(componentInstance, '_addClassToElement');
});

it('should create', () => {
expect(component).toBeTruthy();
componentInstance.ngOnInit();
expect(componentInstance._setProperties).toHaveBeenCalled();
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('fd-button');
});

it('should add appropriate classes', () => {
componentInstance.compact = 'true';
componentInstance.compact = true;
componentInstance.glyph = 'someGlyph';
componentInstance.fdType = 'someFdType';
componentInstance.options = 'someOption';
componentInstance.fdType = 'standard';
componentInstance.ngOnInit();
expect(componentInstance._setProperties).toHaveBeenCalled();
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('fd-button');
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('fd-button--compact');
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('sap-icon--someGlyph');
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('fd-button--someFdType');
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('fd-button--someOption');

let cssClass = componentInstance.buildComponentCssClass();
expect(cssClass).toContain('someGlyph');
expect(cssClass).toContain('standard');
expect(cssClass).toContain('standard');

// should handle an array of options
componentInstance.options = ['someOption1', 'someOption2'];
componentInstance.options = ['light'];
componentInstance.ngOnInit();
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('fd-button--someOption1');
expect(componentInstance._addClassToElement).toHaveBeenCalledWith('fd-button--someOption2');

cssClass = componentInstance.buildComponentCssClass();
expect(cssClass).toContain('light');
});
});
98 changes: 66 additions & 32 deletions libs/core/src/lib/button/button.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewEncapsulation } from '@angular/core';
import { AbstractFdNgxClass } from '../utils/abstract-fd-ngx-class';

import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewEncapsulation, OnInit } from '@angular/core';
import { CssStyleBuilder, Hash, applyCssClass, CssClassBuilder, applyCssStyle } from '../utils/public_api';

export type ButtonType = 'standard' | 'positive' | 'medium' | 'negative' | 'half';
export type ButtonOptions = 'light' | 'emphasized' | 'menu';
Expand All @@ -21,17 +20,29 @@ export type ButtonOptions = 'light' | 'emphasized' | 'menu';
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ButtonComponent extends AbstractFdNgxClass {
export class ButtonComponent implements OnInit, CssClassBuilder, CssStyleBuilder {
private _class: string = '';
@Input() set class(userClass: string) {
this._class = userClass;
this.buildComponentCssClass();
} // user's custom classes

/** The icon to include in the button. See the icon page for the list of icons.
* setter is used to control when css class have to be rebuilded
*/
private _glyph: string
@Input() set glyph(icon: string) {
this._glyph = icon;
this.buildComponentCssClass();
this.buildComponentCssStyle();
};

/** Defines if there will be added fd-button class. Enabled by default. */
@Input() fdButtonClass: boolean = true;

/** Whether to apply compact mode to the button. */
@Input() compact: boolean;

/** The icon to include in the button. See the icon page for the list of icons. */
@Input() glyph: string;

/** The type of the button. Types include 'standard', 'positive', 'medium', 'negative', 'half'.
* Leave empty for default (Action button).'*/
@Input() fdType: ButtonType;
Expand All @@ -46,35 +57,58 @@ export class ButtonComponent extends AbstractFdNgxClass {
@Input() size: string; // TODO: deprecated, leaving for backwards compatibility

/** @hidden */
_setProperties() {
if (this.fdButtonClass) {
this._addClassToElement('fd-button');
}
if (this.compact) {
this._addClassToElement('fd-button--compact');
}
if (this.glyph) {
this._addClassToElement('sap-icon--' + this.glyph);
}
if (this.fdType) {
this._addClassToElement('fd-button--' + this.fdType);
}
if (this.options) {
if (typeof this.options === 'string') {
this._addClassToElement('fd-button--' + this.options);
} else if (Array.isArray(this.options)) {
this.options.forEach(option => {
if (typeof option === 'string') {
this._addClassToElement('fd-button--' + option);
}
});
}
constructor(private _elementRef: ElementRef) {
}

/** Function runs when component is initialized
* function should build component css class
* function should build css style
*/
ngOnInit(): void {
this.buildComponentCssClass();
this.buildComponentCssStyle();
}

@applyCssClass
/** CssClassBuilder interface implementation
* function must return single string
* function is responsible for order which css classes are applied
*/
buildComponentCssClass(): string {
return [
this.fdButtonClass ? 'fd-button' : '',
this.compact ? 'fd-button--compact' : '',
this._glyph ? `sap-icon--${this._glyph}` : '',
this.fdType ? `fd-button--${this.fdType}` : '',
this.options ? this._getOptionCssClass(this.options) : '',
this._class
].filter(x => x !== '').join(' ');
}

@applyCssStyle
/** CssStyleBuilder interface implementation
* function must return hashmap where
* key:string
* value:any
*/
buildComponentCssStyle(): Hash<number | string> {
return {
}
}

/** HasElementRef interface implementation
* function used by applyCssClass and applyCssStyle decorators
*/
elementRef(): ElementRef<any> {
return this._elementRef;
}

/** @hidden */
constructor(private elementRef: ElementRef) {
super(elementRef);
private _getOptionCssClass(options: string | ButtonOptions[]): string {
if (Array.isArray(this.options)) {
return this.options.map(option => `fd-button--${option}`).join(' ');
}
return `fd-button--${options}`
}
}

Expand Down
14 changes: 11 additions & 3 deletions libs/core/src/lib/utils/abstract-fd-ngx-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export abstract class AbstractFdNgxClass implements OnInit, OnChanges {
private _elementRef: ElementRef;

/** @hidden */
@Input() class; // user's custom classes
@Input() class: string // user's custom classes

/*
each directive that extends this class will implement this function and populate it with one or more calls to
Expand All @@ -21,13 +21,21 @@ export abstract class AbstractFdNgxClass implements OnInit, OnChanges {
/** @hidden */
abstract _setProperties(): void;

_setClassToElement(className: string): void {
(this._elementRef.nativeElement as HTMLElement).classList.value = `${className} ${this.class}`;
}

_clearElementClass(): void {
(this._elementRef.nativeElement as HTMLElement).classList.value = '';
}

/** @hidden */
_addClassToElement(className: string) {
_addClassToElement(className: string): void {
(this._elementRef.nativeElement as HTMLElement).classList.add(...className.split(' '));
}

/** @hidden */
_addStyleToElement(attribute, value) {
_addStyleToElement(attribute, value): void {
(this._elementRef.nativeElement as HTMLElement).style[attribute] = value;
}

Expand Down
3 changes: 3 additions & 0 deletions libs/core/src/lib/utils/datatypes/hash.datatype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type Hash<valueType> = {
[key in string | number]: valueType;
};
23 changes: 23 additions & 0 deletions libs/core/src/lib/utils/decorators/apply-css-class.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ELEMENT_REF_EXCEPTION } from '../public_api';

/**
* Method decorator to apply css class to a component through native element
* decorator will store original method in variable and wrap it with custom one
* component has to implement CssClassBuilder interface
* more info abour method decorator: https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators
* @param target a component
* @param propertyKey name of the method
* @param descriptor method
*/
export function applyCssClass(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value
descriptor.value = function (): string {
if (!this.elementRef) { throw ELEMENT_REF_EXCEPTION; }

const _class = originalMethod.apply(this);

(this.elementRef().nativeElement as HTMLElement).classList.value = `${_class} ${this.class}`

return _class;
}
}
25 changes: 25 additions & 0 deletions libs/core/src/lib/utils/decorators/apply-css-style.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Hash, ELEMENT_REF_EXCEPTION } from '../public_api';

/**
* Method decorator to apply css styles to a component through native element
* decorator will store original method in variable and wrap it with custom one
* component has to implement HasElementRef or CssStyleBuilder interface
* more info abour method decorator: https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators
* @param target a component
* @param propertyKey name of the method
* @param descriptor method
*/
export function applyCssStyle(target: any, propertyKey: string, descriptor: PropertyDescriptor): void {
const originalMethod = descriptor.value;
descriptor.value = function (): Hash<number | string> {
if (!this.elementRef) { throw ELEMENT_REF_EXCEPTION; }

const _styles: Hash<number | string> = originalMethod.apply(this);

Object.keys(_styles).forEach(key => {
(this.elementRef().nativeElement as HTMLElement).style[key] = _styles[key];
});

return _styles;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { HasElementRef } from '../public_api';

export interface CssClassBuilder extends HasElementRef {
class: string
buildComponentCssClass(): string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { HasElementRef, Hash } from '../public_api';

export interface CssStyleBuilder extends HasElementRef {
buildComponentCssStyle(): Hash<number | string>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ElementRef } from '@angular/core';

export interface HasElementRef {
elementRef(): ElementRef;
}

export const ELEMENT_REF_EXCEPTION = 'HasElementRef interface has to be implemented';
7 changes: 7 additions & 0 deletions libs/core/src/lib/utils/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export * from './pipes/search-highlight.pipe';
export * from './drag-and-drop/drag-and-drop.module';
export * from './drag-and-drop/dnd-list/dnd-list.directive';
export * from './drag-and-drop/dnd-container/dnd-container.directive';

export * from './interfaces/css-class-builder.interface';
export * from './interfaces/css-style-builder.interface';
export * from './interfaces/has-element-ref.interface';
export * from './decorators/apply-css-class.decorator';
export * from './decorators/apply-css-style.decorator';
export * from './dynamic-component/dynamic-component-config';
export * from './dynamic-component/dynamic-component-injector';
export * from './dynamic-component/dynamic-component.service';
export * from './datatypes/hash.datatype';

0 comments on commit d5893eb

Please sign in to comment.