Skip to content

Commit

Permalink
Added support for creating/updating icon dynamically
Browse files Browse the repository at this point in the history
Fixes #153
  • Loading branch information
devoto13 committed Aug 9, 2019
1 parent bf95641 commit 3d45099
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 28 deletions.
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Take your icons to the next level with these advanced features.
* [Layers](./usage/features.md#layers)
* [Layers with text](./usage/features.md#layers-with-text)
* [Layers with counter](./usage/features.md#layers-with-counter)
* [Programmatic API](./usage/features.md#programmatic-api)

## Configurations
* [Changing the Default Prefix](./usage/default-prefix.md)
47 changes: 47 additions & 0 deletions docs/usage/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,50 @@ The following features are available as part of Font Awesome. Note that the synt
<fa-layers-counter content="99+"></fa-layers-counter>
</fa-layers>
```

### Programmatic API

To create `FaIconComponent` dynamically using `ComponentFactoryResolver`:

```ts
@Component({
selector: 'fa-host',
template: '<ng-container #host></ng-container>'
})
class HostComponent {
@ViewChild('host', {static: true, read: ViewContainerRef}) container: ViewContainerRef;

constructor(private cfr: ComponentFactoryResolver) {
}

createIcon() {
const factory = this.cfr.resolveComponentFactory(FaIconComponent);
const componentRef = this.container.createComponent(factory);
componentRef.instance.icon = faUser;
// Note that FaIconComponent.render() should be called to update the
// rendered SVG after setting/updating component inputs.
componentRef.instance.render();
}
}
```

To update `FaIconComponent` programmatically:

```ts
@Component({
selector: 'fa-host',
template: '<fa-icon [icon]="faUser" (click)="spinIcon()"></fa-icon>'
})
class HostComponent {
faUser = faUser;

@ViewChild(FaIconComponent, {static: true}) iconComponent: FaIconComponent;

spinIcon() {
this.iconComponent.spin = true;
// Note that FaIconComponent.render() should be called to update the
// rendered SVG after setting/updating component inputs.
this.iconComponent.render();
}
}
```
4 changes: 4 additions & 0 deletions src/lib/fontawesome.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import { FaStackComponent } from './stack/stack.component';
FaStackComponent,
FaStackItemSizeDirective,
],
entryComponents: [
FaIconComponent,
FaDuotoneIconComponent,
]
})
export class FontAwesomeModule {
}
30 changes: 29 additions & 1 deletion src/lib/icon/duotone-icon.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { faDummy, initTest, queryByCss } from '../../testing/helpers';
import { FaDuotoneIconComponent } from './duotone-icon.component';
Expand Down Expand Up @@ -104,4 +104,32 @@ describe('FaDuotoneIconComponent', () => {
'or use: <fa-icon icon="user"></fa-icon> instead.'
));
});

it('should be able to create component dynamically', () => {
@Component({
selector: 'fa-host',
template: '<ng-container #host></ng-container>'
})
class HostComponent {
@ViewChild('host', {static: true, read: ViewContainerRef}) container: ViewContainerRef;

constructor(private cfr: ComponentFactoryResolver) {
}

createIcon() {
const factory = this.cfr.resolveComponentFactory(FaDuotoneIconComponent);
const componentRef = this.container.createComponent(factory);
componentRef.instance.icon = faDummy;
componentRef.instance.render();
}
}

const fixture = initTest(HostComponent);
fixture.detectChanges();
expect(queryByCss(fixture, 'svg')).toBeFalsy();

fixture.componentInstance.createIcon();
fixture.detectChanges();
expect(queryByCss(fixture, 'svg')).toBeTruthy();
});
});
86 changes: 60 additions & 26 deletions src/lib/icon/icon.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import { Component, Type } from '@angular/core';
import { ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { faUser as faUserRegular } from '@fortawesome/free-regular-svg-icons';
import { Component, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { ComponentFixture, inject } from '@angular/core/testing';

import { library } from '@fortawesome/fontawesome-svg-core';
import { faUser as faUserRegular } from '@fortawesome/free-regular-svg-icons';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { initTest, queryByCss } from '../../testing/helpers';
import { FaIconComponent } from './icon.component';
import { FaIconService } from './icon.service';

const iconServiceStub: FaIconService = {
defaultPrefix: 'fas',
};

function initTest<T>(component: Type<T>): ComponentFixture<T> {
TestBed.configureTestingModule({
declarations: [ FaIconComponent, component ],
providers: [ {provide: FaIconService, useValue: iconServiceStub } ]
});
return TestBed.createComponent(component);
}

function svgIcon(fixture: ComponentFixture<any>): SVGElement {
return fixture.debugElement.nativeElement.querySelector('svg');
}

describe('FaIconComponent', () => {
it('should render SVG icon', () => {
@Component({
Expand All @@ -35,7 +20,7 @@ describe('FaIconComponent', () => {

const fixture = initTest(HostComponent);
fixture.detectChanges();
expect(svgIcon(fixture)).toBeTruthy();
expect(queryByCss(fixture, 'svg')).toBeTruthy();
});

it('should support binding to boolean inputs', () => {
Expand All @@ -50,11 +35,60 @@ describe('FaIconComponent', () => {

const fixture = initTest(HostComponent);
fixture.detectChanges();
expect(svgIcon(fixture).classList.contains('fa-spin')).toBeFalsy();
expect(queryByCss(fixture, 'svg').classList.contains('fa-spin')).toBeFalsy();

fixture.componentInstance.isAnimated = true;
fixture.detectChanges();
expect(svgIcon(fixture).classList.contains('fa-spin')).toBeTruthy();
expect(queryByCss(fixture, 'svg').classList.contains('fa-spin')).toBeTruthy();
});

it('should be able to create component dynamically', () => {
@Component({
selector: 'fa-host',
template: '<ng-container #host></ng-container>'
})
class HostComponent {
@ViewChild('host', {static: true, read: ViewContainerRef}) container: ViewContainerRef;

constructor(private cfr: ComponentFactoryResolver) {
}

createIcon() {
const factory = this.cfr.resolveComponentFactory(FaIconComponent);
const componentRef = this.container.createComponent(factory);
componentRef.instance.icon = faUser;
componentRef.instance.render();
}
}

const fixture = initTest(HostComponent);
fixture.detectChanges();
expect(queryByCss(fixture, 'svg')).toBeFalsy();

fixture.componentInstance.createIcon();
fixture.detectChanges();
expect(queryByCss(fixture, 'svg')).toBeTruthy();
});

it('should be able to update icon programmatically', () => {
@Component({
selector: 'fa-host',
template: '<fa-icon [icon]="faUser"></fa-icon>'
})
class HostComponent {
faUser = faUser;

@ViewChild(FaIconComponent, {static: true}) iconComponent: FaIconComponent;
}

const fixture = initTest(HostComponent);
fixture.detectChanges();
expect(queryByCss(fixture, 'svg').classList.contains('fa-spin')).toBeFalsy();

fixture.componentInstance.iconComponent.spin = true;
fixture.componentInstance.iconComponent.render();
fixture.detectChanges();
expect(queryByCss(fixture, 'svg').classList.contains('fa-spin')).toBeTruthy();
});

describe('custom service configuration', () => {
Expand All @@ -70,18 +104,18 @@ describe('FaIconComponent', () => {

beforeEach(() => {
library.add(faUser, faUserRegular);
fixture = initTest(HostComponent);
fixture = initTest(HostComponent, [{provide: FaIconService, useValue: {defaultPrefix: 'fas'}}]);
});

it('should use default prefix', () => {
fixture.detectChanges();
expect(svgIcon(fixture).getAttribute('data-prefix')).toEqual('fas');
expect(queryByCss(fixture, 'svg').getAttribute('data-prefix')).toEqual('fas');
});

it('should override default prefix', inject([FaIconService], (service: FaIconService) => {
service.defaultPrefix = 'far';
fixture.detectChanges();
expect(svgIcon(fixture).getAttribute('data-prefix')).toEqual('far');
expect(queryByCss(fixture, 'svg').getAttribute('data-prefix')).toEqual('far');
}));

});
Expand Down
11 changes: 11 additions & 0 deletions src/lib/icon/icon.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ export class FaIconComponent implements OnChanges {
}
}

/**
* Programmatically trigger rendering of the icon.
*
* This method is useful, when creating {@link FaIconComponent} dynamically or
* changing its inputs programmatically as in these cases icon won't be
* re-rendered automatically.
*/
render() {
this.ngOnChanges({});
}

protected normalizeIcon() {
return faNormalizeIconSpec(this.icon, this.iconService.defaultPrefix);
}
Expand Down
12 changes: 11 additions & 1 deletion src/testing/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { IconDefinition, IconName, library } from '@fortawesome/fontawesome-svg-core';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { FaDuotoneIconComponent } from '../lib/icon/duotone-icon.component';
Expand All @@ -10,7 +11,7 @@ import { FaLayersComponent } from '../lib/layers/layers.component';
import { FaStackItemSizeDirective } from '../lib/stack/stack-item-size.directive';
import { FaStackComponent } from '../lib/stack/stack.component';

export function initTest<T>(component: Type<T>): ComponentFixture<T> {
export function initTest<T>(component: Type<T>, providers?: any[]): ComponentFixture<T> {
TestBed.configureTestingModule({
declarations: [
FaIconComponent,
Expand All @@ -22,6 +23,15 @@ export function initTest<T>(component: Type<T>): ComponentFixture<T> {
FaStackItemSizeDirective,
component,
],
providers
});
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [
FaIconComponent,
FaDuotoneIconComponent,
],
},
});
library.add(faUser);
return TestBed.createComponent(component);
Expand Down

0 comments on commit 3d45099

Please sign in to comment.