Skip to content

Commit c37df0a

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

File tree

9 files changed

+206
-241
lines changed

9 files changed

+206
-241
lines changed

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

Lines changed: 8 additions & 6 deletions
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];
Lines changed: 10 additions & 13 deletions
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
});
Lines changed: 12 additions & 12 deletions
Loading
Lines changed: 65 additions & 76 deletions
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

Lines changed: 0 additions & 6 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy