Skip to content

Commit

Permalink
Introduce more permissive icon types
Browse files Browse the repository at this point in the history
With this change `angular-fontawesome` exposes more permissive variants of some types (`IconPrefix`, `IconName`, `IconLookup`, `IconDefinition` and `IconPack`) from the `fontawesome-svg-core`. These new types allow arbitrary string values as icon name and icon prefix while maintaining auto-completion for the core Font Awesome icons.

Firstly, this makes it possible to define and use custom icons without any casts, thus implementing part of the #172 and addressing part of the #423 (Kit packages with custom icons). The documentation for custom icons is coming later in a separate PR.

Secondly, this makes `angular-fontawesome` resilient to multiple instance of `fontawesome-common-types` packages, thus helps with issues like #125.

The drawback of this change is that if a user makes a typo in a core icon name or an icon prefix it will no longer produce a compile-time error, but will throw a runtime error instead. However, this trade-off seems to be overall the best option. Considerations:

1. To keep type safety while supporting custom icons, we'll need to somehow extend the mentioned icon types. It was investigated in the #172 (comment). The conclusion is that it requires very convoluted code to be added to the project and therefore is undesired.
2. For the explicit reference approach, the type safety/completion is not really needed as icon definitions are imported as runtime symbols and importing a symbol which does not exist will always result in complication error.
3. For the icon library approach, the type safety isn't perfect either. While it will catch cases where one specifies a completely incorrect icon name, it does not catch all problems. Icons are added to the library at runtime and if an icon name is correct, but the icon was not added to the library it will still result in a runtime error.
  • Loading branch information
devoto13 committed Apr 17, 2024
1 parent 0fe2f8b commit 2602127
Show file tree
Hide file tree
Showing 14 changed files with 63 additions and 34 deletions.
5 changes: 2 additions & 3 deletions projects/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { DecimalPipe } from '@angular/common';
import { Component } from '@angular/core';
import { FaConfig, FaIconLibrary, FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { IconDefinition, IconName } from '@fortawesome/fontawesome-svg-core';
import { FaConfig, FaIconLibrary, FontAwesomeModule, IconDefinition } from '@fortawesome/angular-fontawesome';
import { faFlag, faUser as regularUser } from '@fortawesome/free-regular-svg-icons';
import {
faAdjust,
Expand Down Expand Up @@ -48,7 +47,7 @@ export class AppComponent {
faSpinner = faSpinner;
faDummy: IconDefinition = {
prefix: 'fad',
iconName: 'dummy' as IconName,
iconName: 'dummy',
icon: [512, 512, [], 'f030', ['M50 50 H412 V250 H50 Z', 'M50 262 H412 V462 H50 Z']],
};

Expand Down
2 changes: 1 addition & 1 deletion src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { IconDefinition, IconPrefix } from '@fortawesome/fontawesome-svg-core';
import { IconDefinition, IconPrefix } from './types';

@Injectable({ providedIn: 'root' })
export class FaConfig {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/icon-library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { IconDefinition, IconName, IconPack, IconPrefix } from '@fortawesome/fontawesome-svg-core';
import { IconDefinition, IconName, IconPack, IconPrefix } from './types';

export interface FaIconLibraryInterface {
addIcons(...icons: IconDefinition[]): void;
Expand Down
5 changes: 3 additions & 2 deletions src/lib/icon/duotone-icon.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, Input } from '@angular/core';
import { IconDefinition, IconParams, IconProp } from '@fortawesome/fontawesome-svg-core';
import { IconDefinition as CoreIconDefinition, IconParams } from '@fortawesome/fontawesome-svg-core';
import { IconDefinition, IconProp } from '../types';
import { FaIconComponent } from './icon.component';

@Component({
Expand Down Expand Up @@ -49,7 +50,7 @@ export class FaDuotoneIconComponent extends FaIconComponent {
*/
@Input() secondaryColor?: string;

protected findIconDefinition(i: IconProp | IconDefinition): IconDefinition | null {
protected findIconDefinition(i: IconProp | IconDefinition): CoreIconDefinition | null {
const definition = super.findIconDefinition(i);

if (definition != null && !Array.isArray(definition.icon[4])) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/icon/icon.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faUser as faUserRegular } from '@fortawesome/free-regular-svg-icons';
import { faCircle, faUser } from '@fortawesome/free-solid-svg-icons';
import { Subject } from 'rxjs';
import { startWith } from 'rxjs/operators';
import { initTest, queryByCss } from '../../testing/helpers';
import { FaConfig } from '../config';
import { FaIconLibrary } from '../icon-library';
import { IconProp } from '../types';
import { FaIconComponent } from './icon.component';

describe('FaIconComponent', () => {
Expand Down
23 changes: 9 additions & 14 deletions src/lib/icon/icon.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import {
FaSymbol,
FlipProp,
icon,
IconDefinition,
IconDefinition as CoreIconDefinition,
IconParams,
IconProp,
parse,
PullProp,
RotateProp,
Expand All @@ -22,6 +21,7 @@ import { faClassList } from '../shared/utils/classlist.util';
import { faNormalizeIconSpec } from '../shared/utils/normalize-icon-spec.util';
import { FaStackItemSizeDirective } from '../stack/stack-item-size.directive';
import { FaStackComponent } from '../stack/stack.component';
import { IconDefinition, IconProp } from '../types';

@Component({
selector: 'fa-icon',
Expand Down Expand Up @@ -86,18 +86,18 @@ export class FaIconComponent implements OnChanges {
}
}

ngOnChanges(changes: SimpleChanges) {
ngOnChanges(changes: SimpleChanges): void {
if (this.icon == null && this.config.fallbackIcon == null) {
faWarnIfIconSpecMissing();
return;
}

if (changes) {
const iconToBeRendered = this.icon != null ? this.icon : this.config.fallbackIcon;
const iconDefinition = this.findIconDefinition(iconToBeRendered);
const iconDefinition = this.findIconDefinition(this.icon ?? this.config.fallbackIcon);
if (iconDefinition != null) {
const params = this.buildParams();
this.renderIcon(iconDefinition, params);
const renderedIcon = icon(iconDefinition, params);
this.renderedIconHTML = this.sanitizer.bypassSecurityTrustHtml(renderedIcon.html.join('\n'));
}
}
}
Expand All @@ -113,15 +113,15 @@ export class FaIconComponent implements OnChanges {
this.ngOnChanges({});
}

protected findIconDefinition(i: IconProp | IconDefinition): IconDefinition | null {
protected findIconDefinition(i: IconProp | IconDefinition): CoreIconDefinition | null {
const lookup = faNormalizeIconSpec(i, this.config.defaultPrefix);
if ('icon' in lookup) {
return lookup;
return lookup as CoreIconDefinition;
}

const definition = this.iconLibrary.getIconDefinition(lookup.prefix, lookup.iconName);
if (definition != null) {
return definition;
return definition as CoreIconDefinition;
}

faWarnIfIconDefinitionMissing(lookup);
Expand Down Expand Up @@ -154,9 +154,4 @@ export class FaIconComponent implements OnChanges {
},
};
}

private renderIcon(definition: IconDefinition, params: IconParams) {
const renderedIcon = icon(definition, params);
this.renderedIconHTML = this.sanitizer.bypassSecurityTrustHtml(renderedIcon.html.join('\n'));
}
}
1 change: 1 addition & 0 deletions src/lib/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { FaLayersCounterComponent } from './layers/layers-counter.component';
export { FaStackComponent } from './stack/stack.component';
export { FaStackItemSizeDirective } from './stack/stack-item-size.directive';
export { FaIconLibrary, FaIconLibraryInterface } from './icon-library';
export { IconPrefix, IconName, IconLookup, IconDefinition, IconPack } from './types';
2 changes: 1 addition & 1 deletion src/lib/shared/errors/warn-if-icon-html-missing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconLookup } from '@fortawesome/fontawesome-svg-core';
import { IconLookup } from '../../types';

export const faWarnIfIconDefinitionMissing = (iconSpec: IconLookup) => {
throw new Error(
Expand Down
2 changes: 1 addition & 1 deletion src/lib/shared/models/props.model.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
FaSymbol,
FlipProp,
IconProp,
PullProp,
RotateProp,
SizeProp,
Styles,
Transform,
} from '@fortawesome/fontawesome-svg-core';
import { IconProp } from '../../types';

/**
* Fontawesome props.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/shared/utils/is-icon-lookup.util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core';
import { IconLookup, IconProp } from '../../types';

/**
* Returns if is IconLookup or not.
Expand Down
8 changes: 4 additions & 4 deletions src/lib/shared/utils/normalize-icon-spec.util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconDefinition, IconLookup, IconPrefix, IconProp } from '@fortawesome/fontawesome-svg-core';
import { IconDefinition, IconLookup, IconPrefix, IconProp } from '../../types';
import { isIconLookup } from './is-icon-lookup.util';

/**
Expand All @@ -12,9 +12,9 @@ export const faNormalizeIconSpec = (
return iconSpec;
}

if (typeof iconSpec === 'string') {
return { prefix: defaultPrefix, iconName: iconSpec };
if (Array.isArray(iconSpec) && iconSpec.length === 2) {
return { prefix: iconSpec[0], iconName: iconSpec[1] };
}

return { prefix: iconSpec[0], iconName: iconSpec[1] };
return { prefix: defaultPrefix, iconName: iconSpec };
};
34 changes: 34 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { IconName as CoreIconName, IconPrefix as CoreIconPrefix } from '@fortawesome/fontawesome-svg-core';

// Currently, when a union type of a primitive type is combined with literal
// types, TypeScript loses all information about the combined literals. Thus,
// when such type is used in an IDE with autocompletion, no suggestions are
// made for the declared literals.
// Below types use a workaround from [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729).

export type IconPrefix = CoreIconPrefix | (string & {});

export type IconName = CoreIconName | (string & {});

export interface IconLookup {
prefix: IconPrefix;
iconName: IconName;
}

export interface IconDefinition {
prefix: IconPrefix;
iconName: IconName;
icon: [
number, // width
number, // height
string[], // ligatures
string, // unicode
string | string[], // svgPathData
];
}

export interface IconPack {
[key: string]: IconDefinition;
}

export type IconProp = IconName | [IconPrefix, IconName] | IconLookup;
4 changes: 2 additions & 2 deletions src/testing/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { CommonModule } from '@angular/common';
import { Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IconDefinition, IconName } from '@fortawesome/fontawesome-svg-core';
import { FaDuotoneIconComponent } from '../lib/icon/duotone-icon.component';
import { FaIconComponent } from '../lib/icon/icon.component';
import { FaLayersCounterComponent } from '../lib/layers/layers-counter.component';
import { FaLayersTextComponent } from '../lib/layers/layers-text.component';
import { FaLayersComponent } from '../lib/layers/layers.component';
import { FaStackItemSizeDirective } from '../lib/stack/stack-item-size.directive';
import { FaStackComponent } from '../lib/stack/stack.component';
import { IconDefinition } from '../lib/types';

export const initTest = <T>(component: Type<T>, providers?: any[]): ComponentFixture<T> => {
TestBed.configureTestingModule({
Expand All @@ -33,6 +33,6 @@ export const queryByCss = (fixture: ComponentFixture<any>, cssSelector: string):

export const faDummy: IconDefinition = {
prefix: 'fad',
iconName: 'dummy' as IconName,
iconName: 'dummy',
icon: [512, 512, [], 'f030', ['M50 50 H412 V250 H50 Z', 'M50 262 H412 V462 H50 Z']],
};
5 changes: 2 additions & 3 deletions testing/src/icon/mock-icon-library.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Injectable } from '@angular/core';
import { FaIconLibraryInterface } from '@fortawesome/angular-fontawesome';
import { IconDefinition, IconName, IconPrefix } from '@fortawesome/fontawesome-svg-core';
import { FaIconLibraryInterface, IconDefinition, IconName, IconPrefix } from '@fortawesome/angular-fontawesome';

export const dummyIcon: IconDefinition = {
prefix: 'fad',
iconName: 'dummy' as IconName,
iconName: 'dummy',
icon: [512, 512, [], 'f030', 'M50 50 H462 V462 H50 Z'],
};

Expand Down

0 comments on commit 2602127

Please sign in to comment.