Skip to content
This repository was archived by the owner on Jul 13, 2022. It is now read-only.

Commit 7acaa0a

Browse files
committed
feat: IconDirective
1 parent 2c9e882 commit 7acaa0a

File tree

6 files changed

+206
-4
lines changed

6 files changed

+206
-4
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Component, DebugElement, Renderer2, Type, ViewChild } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { By, DomSanitizer } from '@angular/platform-browser';
4+
5+
import { IconDirective } from './icon.directive';
6+
import { IconSetService } from '../icon-set';
7+
import { cilList } from '@coreui/icons';
8+
9+
@Component({
10+
template: `
11+
<svg [cIcon]="this.iconSet.icons.cilList" size="lg" class="test" title="Test"></svg>`
12+
})
13+
class TestComponent {
14+
@ViewChild(IconDirective, {read: IconDirective}) iconRef!: IconDirective;
15+
16+
constructor(
17+
public iconSet: IconSetService
18+
) {
19+
this.iconSet.icons = {cilList};
20+
}
21+
}
22+
23+
24+
describe('IconDirective', () => {
25+
let component: TestComponent;
26+
let fixture: ComponentFixture<TestComponent>;
27+
let svgEl: DebugElement;
28+
let renderer: Renderer2;
29+
let sanitizer: DomSanitizer;
30+
let iconSetService: IconSetService;
31+
32+
beforeEach(() => {
33+
TestBed.configureTestingModule({
34+
declarations: [TestComponent, IconDirective],
35+
providers: [IconSetService]
36+
}).compileComponents();
37+
38+
fixture = TestBed.createComponent(TestComponent);
39+
component = fixture.componentInstance;
40+
fixture.detectChanges();
41+
svgEl = fixture.debugElement.query(By.css('svg'));
42+
renderer = fixture.componentRef.injector.get(Renderer2 as Type<Renderer2>);
43+
sanitizer = fixture.componentRef.injector.get(DomSanitizer);
44+
iconSetService = fixture.componentRef.injector.get(IconSetService);
45+
});
46+
47+
it('should create an instance', () => {
48+
const directive = new IconDirective(renderer, svgEl, sanitizer, iconSetService);
49+
expect(directive).toBeTruthy();
50+
});
51+
it('icon classes should be applied', () => {
52+
console.log(svgEl.nativeElement);
53+
expect(svgEl.nativeElement).toBeTruthy();
54+
expect(svgEl.nativeElement).toHaveClass('icon');
55+
expect(svgEl.nativeElement).toHaveClass('icon-lg');
56+
expect(svgEl.nativeElement).toHaveClass('test');
57+
});
58+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Directive, ElementRef, HostBinding, Input, Renderer2 } from '@angular/core';
2+
import { DomSanitizer } from '@angular/platform-browser';
3+
import { IconSetService } from '../icon-set';
4+
5+
import { IconSize, IIcon } from './icon.interface';
6+
7+
@Directive({
8+
selector: 'svg[cIcon]',
9+
exportAs: 'cIcon'
10+
})
11+
export class IconDirective implements IIcon {
12+
13+
@Input('cIcon') content?: string | string[] | any[];
14+
@Input() size: IconSize = '';
15+
@Input() title?: string;
16+
@Input() customClasses?: string | string[] | Set<string> | { [klass: string]: any };
17+
@Input() width?: string;
18+
@Input() height?: string;
19+
20+
@Input()
21+
set name(name: string) {
22+
this._name = name.includes('-') ? this.toCamelCase(name) : name;
23+
}
24+
get name(): string {
25+
return this._name;
26+
}
27+
private _name!: string;
28+
29+
@HostBinding('attr.viewBox')
30+
@Input()
31+
set viewBox(viewBox: string) {
32+
this._viewBox = viewBox;
33+
}
34+
get viewBox(): string {
35+
return this._viewBox ?? this.scale;
36+
}
37+
private _viewBox!: string;
38+
39+
@HostBinding('attr.xmlns')
40+
@Input() xmlns = 'http://www.w3.org/2000/svg';
41+
42+
@HostBinding('attr.pointer-events')
43+
@Input('pointer-events') pointerEvents = 'none';
44+
45+
@HostBinding('class')
46+
get hostClasses() {
47+
const classes = {
48+
icon: true,
49+
[`icon-${this.computedSize}`]: !!this.computedSize
50+
};
51+
return this.customClasses ?? classes;
52+
}
53+
54+
@HostBinding('innerHtml')
55+
get innerHtml() {
56+
const code = Array.isArray(this.code) ? this.code[1] || this.code[0] : this.code ?? '';
57+
// todo proper sanitize
58+
// const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, code);
59+
return this.sanitizer.bypassSecurityTrustHtml((this.titleCode + code) ?? '');
60+
}
61+
62+
constructor(
63+
private renderer: Renderer2,
64+
private elementRef: ElementRef,
65+
private sanitizer: DomSanitizer,
66+
private iconSet: IconSetService
67+
) { }
68+
69+
get titleCode(): string {
70+
return this.title ? `<title>${this.title}</title>` : '';
71+
}
72+
73+
get code(): string | string[] | undefined {
74+
if (this.content) {
75+
return this.content;
76+
}
77+
if (this.iconSet && this.name) {
78+
return this.iconSet.getIcon(this.name);
79+
}
80+
if (this.name && !this.iconSet?.icons[this.name])
81+
console.warn(`c-icon component: icon name '${this.name}' does not exist for IconSet service. ` +
82+
`To use icon by 'name' prop you need to add it to IconSet service. \n`,
83+
this.name
84+
);
85+
return undefined;
86+
}
87+
88+
get scale(): string {
89+
return Array.isArray(this.code) && this.code.length > 1 ? `0 0 ${this.code[0]}` : '0 0 64 64';
90+
}
91+
92+
get computedSize(): Exclude<IconSize, 'custom'> | undefined {
93+
const addCustom = !this.size && (this.width || this.height);
94+
return this.size === 'custom' || addCustom ? 'custom-size' : this.size;
95+
}
96+
97+
get computedClasses(): any {
98+
const classes = {
99+
icon: true,
100+
[`icon-${this.computedSize}`]: !!this.computedSize
101+
};
102+
return !!this.customClasses ? this.customClasses : classes;
103+
}
104+
105+
toCamelCase(str: string): any {
106+
return str.replace(/([-_][a-z0-9])/ig, ($1: string) => {
107+
return $1.toUpperCase().replace('-', '');
108+
});
109+
}
110+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export interface IIcon {
2+
content?: string | string[] | any[];
3+
customClasses?: string | string[] | Set<string> | { [klass: string]: any };
4+
height?: string;
5+
name?: string;
6+
pointerEvents?: string;
7+
size?: IconSize;
8+
title?: string;
9+
viewBox?: string;
10+
width?: string;
11+
xmlns?: string;
12+
}
13+
14+
export type IconSize =
15+
'custom'
16+
| 'custom-size'
17+
| 'sm'
18+
| 'lg'
19+
| 'xl'
20+
| 'xxl'
21+
| '3xl'
22+
| '4xl'
23+
| '5xl'
24+
| '6xl'
25+
| '7xl'
26+
| '8xl'
27+
| '9xl'
28+
| string;
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import {NgModule} from '@angular/core';
1+
import { NgModule } from '@angular/core';
22
import { CommonModule } from '@angular/common';
33

4-
import {IconComponent} from './icon.component';
4+
import { IconComponent } from './icon.component';
5+
import { IconDirective } from './icon.directive';
56
import { HtmlAttributesDirective } from '../shared/html-attr.directive';
67

78
@NgModule({
89
declarations: [
910
IconComponent,
10-
HtmlAttributesDirective
11+
HtmlAttributesDirective,
12+
IconDirective
1113
],
1214
imports: [
1315
CommonModule,
1416
],
1517
exports: [
1618
IconComponent,
19+
IconDirective
1720
],
1821
})
19-
export class IconModule {}
22+
export class IconModule {
23+
}

projects/coreui-icons-angular/src/lib/icon/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { IconDirective } from './icon.directive';
12
export { IconComponent } from './icon.component';
23
export { IconModule } from './icon.module';
34

projects/coreui-icons-angular/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Public API Surface of @coreui/icons-angular
33
*/
4+
export { IconDirective } from './lib/icon/icon.directive';
45
export { IconComponent } from './lib/icon/icon.component';
56
export { IconModule } from './lib/icon/icon.module';
67
export { IconSetService, IIconSet } from './lib/icon-set/icon-set.service';

0 commit comments

Comments
 (0)