Skip to content

Commit c37df0a

Browse files
committed
refactor(icon): signal inputs, host bindings, cleanup
1 parent 7340728 commit c37df0a

9 files changed

+206
-241
lines changed

projects/coreui-icons-angular/src/lib/icon-set/icon-set.service.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ export interface IIconSet {
99
})
1010
export class IconSetService {
1111
public get iconNames() {
12-
return this._iconNames;
12+
return this.#iconNames;
1313
}
1414

15-
private _iconNames: { [key: string]: string } = {};
15+
#iconNames: Record<string, string> = {};
1616

1717
get icons(): IIconSet {
18-
return this._icons;
18+
return this.#icons;
1919
}
20+
2021
set icons(iconSet) {
2122
for (const iconsKey in iconSet) {
22-
this._iconNames[iconsKey] = iconsKey;
23+
this.#iconNames[iconsKey] = iconsKey;
2324
}
24-
this._icons = iconSet;
25+
this.#icons = iconSet;
2526
}
26-
private _icons: IIconSet = {};
27+
28+
#icons: IIconSet = {};
2729

2830
public getIcon(name: string): string[] {
2931
const icon = this.icons[name];
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { Component, DebugElement, ViewChild } from '@angular/core';
3+
import { By } from '@angular/platform-browser';
34

45
import { cilList } from '@coreui/icons';
56
import { HtmlAttributesDirective } from '../shared/html-attr.directive';
6-
import { IconComponent } from './icon.component';
77
import { IconSetService } from '../icon-set';
8-
import { By } from '@angular/platform-browser';
8+
import { IconComponent } from './icon.component';
99

1010
@Component({
1111
template: '<c-icon #icon name="cil-list" size="lg" class="test" />',
@@ -16,15 +16,13 @@ import { By } from '@angular/platform-browser';
1616
class TestComponent {
1717
@ViewChild('icon', { read: IconComponent }) iconRef!: IconComponent;
1818

19-
constructor(
20-
public iconSet: IconSetService
21-
) {
19+
constructor(public iconSet: IconSetService) {
2220
this.iconSet.icons = { cilList };
2321
}
2422
}
2523

2624
describe('IconComponent', () => {
27-
let inputEl: DebugElement;
25+
let debugEl: DebugElement;
2826
let component: TestComponent;
2927
let fixture: ComponentFixture<TestComponent>;
3028

@@ -33,14 +31,13 @@ describe('IconComponent', () => {
3331
imports: [TestComponent, IconComponent, HtmlAttributesDirective],
3432
providers: [IconSetService]
3533
}).compileComponents();
36-
3734
});
3835

3936
beforeEach(() => {
4037
fixture = TestBed.createComponent(TestComponent);
4138
component = fixture.componentInstance;
4239
fixture.detectChanges();
43-
inputEl = fixture.debugElement.query(By.css('svg'));
40+
debugEl = fixture.debugElement.query(By.css('svg'));
4441
});
4542

4643
it('should create', () => {
@@ -52,13 +49,13 @@ describe('IconComponent', () => {
5249
});
5350
it('icon component should render', () => {
5451
expect(component.iconRef).toBeTruthy();
55-
expect(component.iconRef.name).toBe('cilList');
52+
expect(component.iconRef.name()).toBe('cilList');
5653
expect(component.iconRef.svgElementRef).toBeTruthy();
5754
});
5855
it('icon classes should be applied', () => {
59-
expect(inputEl.nativeElement).toBeTruthy();
60-
expect(inputEl.nativeElement).toHaveClass('icon');
61-
expect(inputEl.nativeElement).toHaveClass('icon-lg');
62-
expect(inputEl.nativeElement).toHaveClass('test');
56+
expect(debugEl.nativeElement).toBeTruthy();
57+
expect(debugEl.nativeElement).toHaveClass('icon');
58+
expect(debugEl.nativeElement).toHaveClass('icon-lg');
59+
expect(debugEl.nativeElement).toHaveClass('test');
6360
});
6461
});
Loading
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import { NgClass } from '@angular/common';
22
import {
3-
AfterViewInit,
3+
afterNextRender,
44
Component,
55
computed,
6+
effect,
67
ElementRef,
78
inject,
8-
Input,
9+
input,
910
Renderer2,
1011
signal,
11-
ViewChild
12+
viewChild
1213
} from '@angular/core';
1314
import { DomSanitizer } from '@angular/platform-browser';
1415

1516
import { HtmlAttributesDirective } from '../shared/html-attr.directive';
1617
import { IconSetService } from '../icon-set';
17-
import { IconSize, IIcon } from './icon.interface';
18+
import { IconSize, IIcon, NgCssClass } from './icon.interface';
1819
import { transformName } from './icon.utils';
1920

2021
@Component({
@@ -24,109 +25,97 @@ import { transformName } from './icon.utils';
2425
standalone: true,
2526
styleUrls: ['./icon.component.scss'],
2627
templateUrl: './icon.component.svg',
27-
host: { ngSkipHydration: 'true' }
28+
host: { ngSkipHydration: 'true', style: 'display: none' }
2829
})
29-
export class IconComponent implements IIcon, AfterViewInit {
30+
export class IconComponent implements IIcon {
3031
readonly #renderer = inject(Renderer2);
3132
readonly #elementRef = inject(ElementRef);
3233
readonly #sanitizer = inject(DomSanitizer);
3334
readonly #iconSet = inject(IconSetService);
35+
readonly #hostElement = signal<ElementRef<any> | undefined>(undefined);
3436

3537
constructor() {
36-
this.#renderer.setStyle(this.#elementRef.nativeElement, 'display', 'none');
37-
}
38-
39-
@Input()
40-
set content(value: string | string[] | any[]) {
41-
this.#content.set(value);
42-
}
43-
44-
readonly #content = signal<string | string[] | any[]>('');
45-
46-
@Input() attributes: any = { role: 'img' };
47-
@Input() customClasses?: string | string[] | Set<string> | { [klass: string]: any };
48-
@Input() size: IconSize = '';
49-
@Input() title?: string;
50-
@Input() use = '';
51-
@Input() height?: string;
52-
@Input() width?: string;
53-
54-
@Input({ transform: transformName })
55-
set name(value: string) {
56-
this.#name.set(value);
57-
}
58-
59-
get name() {
60-
return this.#name();
61-
}
62-
63-
readonly #name = signal('');
64-
65-
@Input()
66-
set viewBox(viewBox: string) {
67-
this._viewBox = viewBox;
68-
}
69-
70-
get viewBox(): string {
71-
return this._viewBox ?? this.scale();
38+
afterNextRender(() => {
39+
this.#hostElement.set(this.#elementRef);
40+
});
7241
}
7342

74-
private _viewBox!: string;
75-
76-
@ViewChild('svgElement', { read: ElementRef }) svgElementRef!: ElementRef;
43+
readonly content = input<string | string[] | any[]>();
44+
45+
readonly attributes = input<Record<string, any>>({ role: 'img' });
46+
readonly customClasses = input<NgCssClass>();
47+
readonly size = input<IconSize>('');
48+
readonly title = input<string>();
49+
readonly use = input<string>('');
50+
readonly height = input<string>();
51+
readonly width = input<string>();
52+
readonly name = input('', { transform: transformName });
53+
readonly viewBoxInput = input<string | undefined>(undefined, { alias: 'viewBox' });
54+
55+
readonly svgElementRef = viewChild<ElementRef>('svgElement');
56+
57+
readonly svgElementEffect = effect(() => {
58+
const svgElementRef = this.svgElementRef();
59+
const hostElement = this.#hostElement()?.nativeElement;
60+
if (svgElementRef && hostElement) {
61+
const svgElement = svgElementRef.nativeElement;
62+
hostElement.classList?.values()?.forEach((item: string) => {
63+
this.#renderer.addClass(svgElement, item);
64+
});
65+
const parentElement = this.#renderer.parentNode(hostElement);
66+
this.#renderer.insertBefore(parentElement, svgElement, hostElement);
67+
this.#renderer.removeChild(parentElement, hostElement);
68+
}
69+
});
7770

78-
ngAfterViewInit(): void {
79-
this.#elementRef.nativeElement.classList.forEach((item: string) => {
80-
this.#renderer.addClass(this.svgElementRef.nativeElement, item);
81-
});
82-
const parentElement = this.#renderer.parentNode(this.#elementRef.nativeElement);
83-
const svgElement = this.svgElementRef.nativeElement;
84-
this.#renderer.insertBefore(parentElement, svgElement, this.#elementRef.nativeElement);
85-
this.#renderer.removeChild(parentElement, this.#elementRef.nativeElement);
86-
}
71+
readonly viewBox = computed(() => {
72+
return this.viewBoxInput() ?? this.scale();
73+
});
8774

8875
readonly innerHtml = computed(() => {
89-
const code = Array.isArray(this.code()) ? (this.code()[1] ?? this.code()[0] ?? '') : this.code() || '';
76+
const codeVal = this.code();
77+
const code = Array.isArray(codeVal) ? (codeVal?.[1] ?? codeVal?.[0] ?? '') : codeVal || '';
9078
// todo proper sanitize
9179
// const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, code);
92-
return this.#sanitizer.bypassSecurityTrustHtml(this.titleCode + code || '');
80+
return this.#sanitizer.bypassSecurityTrustHtml(this.#titleCode() + code || '');
9381
});
9482

95-
get titleCode(): string {
96-
return this.title ? `<title>${this.title}</title>` : '';
97-
}
83+
readonly #titleCode = computed(() => {
84+
return this.title() ? `<title>${this.title()}</title>` : '';
85+
});
9886

9987
readonly code = computed(() => {
100-
if (this.#content()) {
101-
return this.#content();
88+
const content = this.content();
89+
if (content) {
90+
return content;
10291
}
103-
if (this.#iconSet && this.#name()) {
104-
return this.#iconSet.getIcon(this.#name());
92+
const name = this.name();
93+
if (this.#iconSet && name) {
94+
return this.#iconSet.getIcon(name);
10595
}
106-
if (this.#name() && !this.#iconSet?.icons[this.#name()]) {
96+
if (name && !this.#iconSet?.icons[name]) {
10797
console.warn(
108-
`c-icon component: icon name '${this.#name()}' does not exist for IconSet service. ` +
109-
`To use icon by 'name' prop you need to add it to IconSet service. \n`,
110-
this.#name()
98+
`c-icon component: The '${name}' icon not found. Add it to the IconSet service for use with the 'name' property. \n`,
99+
name
111100
);
112101
}
113102
return '';
114103
});
115104

116105
readonly scale = computed(() => {
117-
return Array.isArray(this.code()) && this.code().length > 1 ? `0 0 ${this.code()[0]}` : '0 0 64 64';
106+
return Array.isArray(this.code()) && (this.code()?.length ?? 0) > 1 ? `0 0 ${this.code()?.[0]}` : '0 0 64 64';
118107
});
119108

120-
get computedSize(): Exclude<IconSize, 'custom'> | undefined {
121-
const addCustom = !this.size && (this.width || this.height);
122-
return this.size === 'custom' || addCustom ? 'custom-size' : this.size;
123-
}
109+
readonly computedSize = computed(() => {
110+
const addCustom = !this.size() && (this.width() || this.height());
111+
return this.size() === 'custom' || addCustom ? 'custom-size' : this.size();
112+
});
124113

125-
get computedClasses() {
114+
readonly computedClasses = computed(() => {
126115
const classes = {
127116
icon: true,
128-
[`icon-${this.computedSize}`]: !!this.computedSize
117+
[`icon-${this.computedSize()}`]: !!this.computedSize()
129118
};
130-
return this.customClasses ?? classes;
131-
}
119+
return this.customClasses() ?? classes;
120+
});
132121
}

projects/coreui-icons-angular/src/lib/icon/icon.directive.spec.ts

-6
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ describe('IconDirective', () => {
2626
let component: TestComponent;
2727
let fixture: ComponentFixture<TestComponent>;
2828
let svgEl: DebugElement;
29-
// let renderer: Renderer2;
30-
// let sanitizer: DomSanitizer;
31-
// let iconSetService: IconSetService;
3229

3330
beforeEach(() => {
3431
TestBed.configureTestingModule({
@@ -40,9 +37,6 @@ describe('IconDirective', () => {
4037
component = fixture.componentInstance;
4138
fixture.detectChanges();
4239
svgEl = fixture.debugElement.query(By.css('svg'));
43-
// renderer = fixture.componentRef.injector.get(Renderer2 as Type<Renderer2>);
44-
// sanitizer = fixture.componentRef.injector.get(DomSanitizer);
45-
// iconSetService = fixture.componentRef.injector.get(IconSetService);
4640
});
4741
it('should create an instance', () => {
4842
TestBed.runInInjectionContext(() => {

0 commit comments

Comments
 (0)