From b10324b387a4509915c177e7f315c6105b1603f4 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Wed, 25 Mar 2026 11:19:28 +0100 Subject: [PATCH 1/2] feat(icon): add ItIconRegistryService for custom SVG icons (#563) - Create ItIconRegistryService (providedIn: 'root') for registering custom SVG icons by name (inline SVG content or external sprite refs) - Modify ItIconComponent to check registry first, then fall back to built-in Bootstrap Italia sprite - Widen name input type to accept IconName | string for custom names - Support three registration methods: registerIcon (inline SVG), registerIcons (bulk), registerIconFromSprite (external sprite) - Custom icons take precedence over built-in sprites on name collision - Add 27 comprehensive tests covering: inline SVG rendering, sprite rendering, CSS class propagation, accessibility, collision handling, bulk registration, removal, and registry unit operations - Export ItIconRegistryService from public_api.ts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/utils/icon/icon.component.html | 6 +- .../utils/icon/icon.component.spec.ts | 396 +++++++++++++++++- .../components/utils/icon/icon.component.ts | 33 +- .../icon-registry/icon-registry.service.ts | 119 ++++++ projects/design-angular-kit/src/public_api.ts | 1 + 5 files changed, 542 insertions(+), 13 deletions(-) create mode 100644 projects/design-angular-kit/src/lib/services/icon-registry/icon-registry.service.ts diff --git a/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.html b/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.html index ea873d797..929101818 100644 --- a/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.html +++ b/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.html @@ -2,5 +2,9 @@ @if (title || labelWaria) { {{ title || labelWaria }} } - + @if (isCustomInlineSvg) { + + } @else { + + } diff --git a/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.spec.ts b/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.spec.ts index 4a5e94038..577bf2815 100644 --- a/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.spec.ts +++ b/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.spec.ts @@ -1,21 +1,399 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { ItIconComponent } from './icon.component'; +import { ItIconRegistryService } from '../../../services/icon-registry/icon-registry.service'; import { tb_base } from '../../../../test'; -describe('ItIconComponent', () => { - let component: ItIconComponent; - let fixture: ComponentFixture; +// ─── Host Components ───────────────────────────────────────────────────────── + +@Component({ + selector: 'it-test-icon-default', + template: '', + imports: [ItIconComponent], +}) +class DefaultIconHost {} + +@Component({ + selector: 'it-test-icon-size', + template: '', + imports: [ItIconComponent], +}) +class SizedIconHost {} + +@Component({ + selector: 'it-test-icon-color', + template: '', + imports: [ItIconComponent], +}) +class ColoredIconHost {} + +@Component({ + selector: 'it-test-icon-padded', + template: '', + imports: [ItIconComponent], +}) +class PaddedIconHost {} + +@Component({ + selector: 'it-test-icon-svg-class', + template: '', + imports: [ItIconComponent], +}) +class SvgClassIconHost {} + +@Component({ + selector: 'it-test-icon-title', + template: '', + imports: [ItIconComponent], +}) +class TitledIconHost {} +@Component({ + selector: 'it-test-icon-label', + template: '', + imports: [ItIconComponent], +}) +class AriaLabelledIconHost {} + +@Component({ + selector: 'it-test-icon-custom', + template: '', + imports: [ItIconComponent], +}) +class CustomIconHost {} + +@Component({ + selector: 'it-test-icon-custom-sprite', + template: '', + imports: [ItIconComponent], +}) +class CustomSpriteIconHost {} + +// ─── Spec ──────────────────────────────────────────────────────────────────── + +describe('ItIconComponent', () => { beforeEach(async () => { - await TestBed.configureTestingModule(tb_base).compileComponents(); + await TestBed.configureTestingModule({ + ...tb_base, + imports: [ + ...(tb_base.imports || []), + ItIconComponent, + DefaultIconHost, + SizedIconHost, + ColoredIconHost, + PaddedIconHost, + SvgClassIconHost, + TitledIconHost, + AriaLabelledIconHost, + CustomIconHost, + CustomSpriteIconHost, + ], + }).compileComponents(); + }); - fixture = TestBed.createComponent(ItIconComponent); - component = fixture.componentInstance; + // ── Basic creation ────────────────────────────────────────────────────────── + + it('should create', () => { + const fixture = TestBed.createComponent(ItIconComponent); + fixture.componentInstance.name = 'search'; fixture.detectChanges(); + expect(fixture.componentInstance).toBeTruthy(); }); - it('should create', () => { - expect(component).toBeTruthy(); + // ── Built-in sprite rendering ─────────────────────────────────────────────── + + it('should render an SVG with pointing to the built-in sprite', () => { + const fixture = TestBed.createComponent(DefaultIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')); + expect(svg).toBeTruthy(); + const use = svg.query(By.css('use')); + expect(use).toBeTruthy(); + const href = use.nativeElement.getAttribute('href') || use.nativeElement.getAttributeNS('http://www.w3.org/1999/xlink', 'href'); + expect(href).toContain('sprites.svg#it-search'); + }); + + it('should NOT render a element for built-in icons', () => { + const fixture = TestBed.createComponent(DefaultIconHost); + fixture.detectChanges(); + const g = fixture.debugElement.query(By.css('svg > g')); + expect(g).toBeNull(); + }); + + // ── CSS classes ───────────────────────────────────────────────────────────── + + it('should apply base "icon" class by default', () => { + const fixture = TestBed.createComponent(DefaultIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.classList.contains('icon')).toBe(true); + }); + + it('should apply size class when size input is set', () => { + const fixture = TestBed.createComponent(SizedIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.classList.contains('icon-lg')).toBe(true); + }); + + it('should apply color class when color input is set', () => { + const fixture = TestBed.createComponent(ColoredIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.classList.contains('icon-primary')).toBe(true); + }); + + it('should apply padded class when padded is true', () => { + const fixture = TestBed.createComponent(PaddedIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.classList.contains('icon-padded')).toBe(true); + }); + + it('should apply custom svgClass', () => { + const fixture = TestBed.createComponent(SvgClassIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.classList.contains('my-custom')).toBe(true); + }); + + // ── Accessibility ─────────────────────────────────────────────────────────── + + it('should be aria-hidden when no title or labelWaria', () => { + const fixture = TestBed.createComponent(DefaultIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.getAttribute('aria-hidden')).toBe('true'); + expect(svg.getAttribute('role')).toBeNull(); + }); + + it('should render and set aria-label when title is provided', () => { + const fixture = TestBed.createComponent(TitledIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.getAttribute('aria-hidden')).toBe('false'); + expect(svg.getAttribute('role')).toBe('img'); + expect(svg.getAttribute('aria-label')).toBe('Search icon'); + const titleEl = fixture.debugElement.query(By.css('svg > title')); + expect(titleEl).toBeTruthy(); + expect(titleEl.nativeElement.textContent).toBe('Search icon'); + }); + + it('should set role=img and aria-label when labelWaria is provided', () => { + const fixture = TestBed.createComponent(AriaLabelledIconHost); + fixture.detectChanges(); + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.getAttribute('aria-hidden')).toBe('false'); + expect(svg.getAttribute('role')).toBe('img'); + expect(svg.getAttribute('aria-label')).toBe('Search'); + }); + + // ── Custom inline SVG icons ───────────────────────────────────────────────── + + describe('ItIconRegistryService — inline SVG', () => { + let registry: ItIconRegistryService; + + beforeEach(() => { + registry = TestBed.inject(ItIconRegistryService); + registry.clear(); + }); + + afterEach(() => { + registry.clear(); + }); + + it('should render custom inline SVG when icon is registered', () => { + const svgContent = '<circle cx="12" cy="12" r="10" fill="red"/>'; + registry.registerIcon('my-custom-icon', svgContent); + + const fixture = TestBed.createComponent(CustomIconHost); + fixture.detectChanges(); + + const svg = fixture.debugElement.query(By.css('svg')); + expect(svg).toBeTruthy(); + + // Must have a <g> with innerHTML (no <use>) + const g = svg.query(By.css('g')); + expect(g).toBeTruthy(); + expect(g.nativeElement.innerHTML).toContain('circle'); + expect(g.nativeElement.innerHTML).toContain('r="10"'); + + // Must NOT have a <use> element + const use = svg.query(By.css('use')); + expect(use).toBeNull(); + }); + + it('should fall back to built-in sprite when custom icon is NOT registered', () => { + // Do NOT register 'my-custom-icon' + const fixture = TestBed.createComponent(CustomIconHost); + fixture.detectChanges(); + + const use = fixture.debugElement.query(By.css('svg use')); + expect(use).toBeTruthy(); + const href = use.nativeElement.getAttribute('href') || use.nativeElement.getAttributeNS('http://www.w3.org/1999/xlink', 'href'); + expect(href).toContain('sprites.svg#it-my-custom-icon'); + }); + + it('should apply all CSS classes to custom inline SVG icons', () => { + registry.registerIcon('search', '<rect width="24" height="24"/>'); + const fixture = TestBed.createComponent(SizedIconHost); + fixture.detectChanges(); + + const svg = fixture.debugElement.query(By.css('svg')).nativeElement; + expect(svg.classList.contains('icon')).toBe(true); + expect(svg.classList.contains('icon-lg')).toBe(true); + }); + + it('should allow removing a registered icon (falls back to sprite)', () => { + registry.registerIcon('my-custom-icon', '<path d="M0 0"/>'); + expect(registry.hasIcon('my-custom-icon')).toBe(true); + + const removed = registry.removeIcon('my-custom-icon'); + expect(removed).toBe(true); + expect(registry.hasIcon('my-custom-icon')).toBe(false); + + const fixture = TestBed.createComponent(CustomIconHost); + fixture.detectChanges(); + + // Should render as sprite fallback + const use = fixture.debugElement.query(By.css('svg use')); + expect(use).toBeTruthy(); + }); + + it('should support registerIcons bulk registration', () => { + registry.registerIcons({ + 'my-custom-icon': '<rect width="10" height="10"/>', + 'another-icon': '<circle cx="5" cy="5" r="5"/>', + }); + + expect(registry.hasIcon('my-custom-icon')).toBe(true); + expect(registry.hasIcon('another-icon')).toBe(true); + }); + + it('should clear all registered icons', () => { + registry.registerIcon('my-custom-icon', '<rect/>'); + registry.registerIconFromSprite('my-sprite-icon', '/sprites.svg', 'star'); + registry.clear(); + + expect(registry.hasIcon('my-custom-icon')).toBe(false); + expect(registry.hasIcon('my-sprite-icon')).toBe(false); + }); + + it('custom icon should take precedence over built-in sprite when name collides', () => { + const customSvg = '<path d="M1 2 L3 4" class="custom-override"/>'; + registry.registerIcon('search', customSvg); + + const fixture = TestBed.createComponent(DefaultIconHost); + fixture.detectChanges(); + + // Should render inline SVG, NOT <use> + const g = fixture.debugElement.query(By.css('svg g')); + expect(g).toBeTruthy(); + expect(g.nativeElement.innerHTML).toContain('custom-override'); + + const use = fixture.debugElement.query(By.css('svg use')); + expect(use).toBeNull(); + }); + }); + + // ── Custom sprite icons ───────────────────────────────────────────────────── + + describe('ItIconRegistryService — external sprite', () => { + let registry: ItIconRegistryService; + + beforeEach(() => { + registry = TestBed.inject(ItIconRegistryService); + registry.clear(); + }); + + afterEach(() => { + registry.clear(); + }); + + it('should render <use> with custom sprite href', () => { + registry.registerIconFromSprite('my-sprite-icon', '/assets/custom-sprites.svg', 'star'); + + const fixture = TestBed.createComponent(CustomSpriteIconHost); + fixture.detectChanges(); + + const use = fixture.debugElement.query(By.css('svg use')); + expect(use).toBeTruthy(); + const href = use.nativeElement.getAttribute('href') || use.nativeElement.getAttributeNS('http://www.w3.org/1999/xlink', 'href'); + expect(href).toBe('/assets/custom-sprites.svg#star'); + }); + + it('should NOT render <g> element for sprite-based custom icons', () => { + registry.registerIconFromSprite('my-sprite-icon', '/assets/custom-sprites.svg', 'star'); + + const fixture = TestBed.createComponent(CustomSpriteIconHost); + fixture.detectChanges(); + + const g = fixture.debugElement.query(By.css('svg g')); + expect(g).toBeNull(); + }); + + it('inline SVG registration takes priority over sprite for the same name', () => { + registry.registerIconFromSprite('my-custom-icon', '/sprites.svg', 'star'); + registry.registerIcon('my-custom-icon', '<rect class="inline-wins"/>'); + + const fixture = TestBed.createComponent(CustomIconHost); + fixture.detectChanges(); + + const g = fixture.debugElement.query(By.css('svg g')); + expect(g).toBeTruthy(); + expect(g.nativeElement.innerHTML).toContain('inline-wins'); + }); + }); + + // ── ItIconRegistryService standalone tests ────────────────────────────────── + + describe('ItIconRegistryService — unit', () => { + let registry: ItIconRegistryService; + + beforeEach(() => { + registry = TestBed.inject(ItIconRegistryService); + registry.clear(); + }); + + afterEach(() => { + registry.clear(); + }); + + it('should return false for hasIcon on unregistered name', () => { + expect(registry.hasIcon('nonexistent')).toBe(false); + }); + + it('should return undefined for getIconSvg on unregistered name', () => { + expect(registry.getIconSvg('nonexistent')).toBeUndefined(); + }); + + it('should return undefined for getIconSpriteHref on unregistered name', () => { + expect(registry.getIconSpriteHref('nonexistent')).toBeUndefined(); + }); + + it('removeIcon should return false when icon does not exist', () => { + expect(registry.removeIcon('nonexistent')).toBe(false); + }); + + it('should store and retrieve inline SVG content as SafeHtml', () => { + registry.registerIcon('test-icon', '<circle r="5"/>'); + const svg = registry.getIconSvg('test-icon'); + expect(svg).toBeTruthy(); + }); + + it('should store and retrieve sprite href', () => { + registry.registerIconFromSprite('test-sprite', '/my-sprites.svg', 'heart'); + const href = registry.getIconSpriteHref('test-sprite'); + expect(href).toBe('/my-sprites.svg#heart'); + }); + + it('hasIcon returns true for both inline and sprite registrations', () => { + registry.registerIcon('inline-one', '<path/>'); + registry.registerIconFromSprite('sprite-one', '/s.svg', 'id'); + expect(registry.hasIcon('inline-one')).toBe(true); + expect(registry.hasIcon('sprite-one')).toBe(true); + }); }); }); diff --git a/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.ts b/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.ts index 6bc31118f..5a148b3b0 100644 --- a/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.ts +++ b/projects/design-angular-kit/src/lib/components/utils/icon/icon.component.ts @@ -1,7 +1,9 @@ import { ChangeDetectionStrategy, Component, inject, Input } from '@angular/core'; +import { SafeHtml } from '@angular/platform-browser'; import { IconColor, IconName, IconSize } from '../../../interfaces/icon'; import { inputToBoolean } from '../../../utils/coercion'; import { IT_ASSET_BASE_PATH } from '../../../interfaces/design-angular-kit-config'; +import { ItIconRegistryService } from '../../../services/icon-registry/icon-registry.service'; @Component({ selector: 'it-icon', @@ -12,9 +14,14 @@ import { IT_ASSET_BASE_PATH } from '../../../interfaces/design-angular-kit-confi }) export class ItIconComponent { /** - * The icon name + * The icon name. + * + * Accepts built-in Bootstrap Italia icon names (e.g. `'arrow-down'`, `'search'`) + * or custom icon names registered via `ItIconRegistryService`. + * + * Custom icons registered in the registry take precedence over built-in sprite icons. */ - @Input({ required: true }) name!: IconName; + @Input({ required: true }) name!: IconName | string; /** * The icon size @@ -47,10 +54,30 @@ export class ItIconComponent { */ @Input() labelWaria: string | undefined; + private readonly iconRegistry = inject(ItIconRegistryService); + + /** + * Whether the icon is a custom inline SVG from the registry. + */ + protected get isCustomInlineSvg(): boolean { + return this.iconRegistry.getIconSvg(this.name) !== undefined; + } + /** - * Return the icon href + * The sanitized inline SVG content for custom icons. + */ + protected get customSvgContent(): SafeHtml | undefined { + return this.iconRegistry.getIconSvg(this.name); + } + + /** + * Return the icon href — either from a custom sprite registration or the built-in sprite. */ protected get iconHref(): string { + const customSpriteHref = this.iconRegistry.getIconSpriteHref(this.name); + if (customSpriteHref) { + return customSpriteHref; + } return `${this.assetBasePath}/dist/svg/sprites.svg#it-${this.name}`; } diff --git a/projects/design-angular-kit/src/lib/services/icon-registry/icon-registry.service.ts b/projects/design-angular-kit/src/lib/services/icon-registry/icon-registry.service.ts new file mode 100644 index 000000000..9d36f97b6 --- /dev/null +++ b/projects/design-angular-kit/src/lib/services/icon-registry/icon-registry.service.ts @@ -0,0 +1,119 @@ +import { inject, Injectable } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +/** + * Registry for custom SVG icons used by `<it-icon>`. + * + * Allows registering custom SVG icons by name so they can be rendered + * using `<it-icon name="my-custom-icon">` alongside the built-in + * Bootstrap Italia sprite icons. + * + * Custom icons take precedence over built-in sprite icons when names collide. + * + * @usageNotes + * + * ### Register inline SVG content + * + * ```typescript + * const registry = inject(ItIconRegistryService); + * registry.registerIcon('home', '<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>'); + * ``` + * + * ### Register from an external sprite + * + * ```typescript + * registry.registerIconFromSprite('custom-star', '/assets/custom-sprites.svg', 'star-icon'); + * ``` + * + * ### Use in template + * + * ```html + * <it-icon name="home"></it-icon> + * <it-icon name="custom-star" size="lg"></it-icon> + * ``` + */ +@Injectable({ providedIn: 'root' }) +export class ItIconRegistryService { + private readonly icons = new Map<string, SafeHtml>(); + private readonly spriteRefs = new Map<string, string>(); + private readonly sanitizer = inject(DomSanitizer); + + /** + * Register a custom icon with inline SVG content. + * + * Only the inner SVG content (paths, shapes, groups) should be provided. + * The outer `<svg>` wrapper with accessibility attributes and CSS classes + * is managed by the `<it-icon>` component. + * + * @param name unique icon name (without the `it-` prefix) + * @param svgContent raw SVG markup string (inner content only) + */ + registerIcon(name: string, svgContent: string): void { + this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svgContent)); + } + + /** + * Register multiple custom icons at once. + * + * @param icons record mapping icon names to SVG content strings + */ + registerIcons(icons: Record<string, string>): void { + for (const [name, svgContent] of Object.entries(icons)) { + this.registerIcon(name, svgContent); + } + } + + /** + * Register a custom icon from an external SVG sprite file. + * + * The icon will be rendered using `<use href="...">` pointing to + * the specified sprite URL and fragment ID. + * + * @param name unique icon name (without the `it-` prefix) + * @param spriteUrl URL of the sprite SVG file + * @param fragmentId the symbol/fragment ID within the sprite + */ + registerIconFromSprite(name: string, spriteUrl: string, fragmentId: string): void { + this.spriteRefs.set(name, `${spriteUrl}#${fragmentId}`); + } + + /** + * Check whether a custom icon (inline or sprite) is registered for the given name. + */ + hasIcon(name: string): boolean { + return this.icons.has(name) || this.spriteRefs.has(name); + } + + /** + * Get the inline SVG content for a registered icon. + * Returns `undefined` if the icon is not registered as inline SVG. + */ + getIconSvg(name: string): SafeHtml | undefined { + return this.icons.get(name); + } + + /** + * Get the sprite href for a registered sprite icon. + * Returns `undefined` if the icon is not registered as a sprite reference. + */ + getIconSpriteHref(name: string): string | undefined { + return this.spriteRefs.get(name); + } + + /** + * Remove a previously registered custom icon. + * + * @returns `true` if the icon was found and removed, `false` otherwise. + */ + removeIcon(name: string): boolean { + return this.icons.delete(name) || this.spriteRefs.delete(name); + } + + /** + * Remove all registered custom icons. + */ + clear(): void { + this.icons.clear(); + this.spriteRefs.clear(); + } +} diff --git a/projects/design-angular-kit/src/public_api.ts b/projects/design-angular-kit/src/public_api.ts index 344db7a64..672bd72f0 100644 --- a/projects/design-angular-kit/src/public_api.ts +++ b/projects/design-angular-kit/src/public_api.ts @@ -126,6 +126,7 @@ export * from './lib/components/utils/language-switcher/language-switcher.compon // Services export * from './lib/services/notification/notification.service'; +export * from './lib/services/icon-registry/icon-registry.service'; // Pipes export * from './lib/pipes/date-ago.pipe'; From fb1e311aeb47e9cb8f6f95223660d83264007130 Mon Sep 17 00:00:00 2001 From: giulio-leone <giulio97.leone@gmail.com> Date: Wed, 25 Mar 2026 17:59:49 +0100 Subject: [PATCH 2/2] docs: add exhaustive icon registry service example Show inline SVG registration, batch registration, and external sprite registration with ItIconRegistryService. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../icon-examples/icon-examples.component.tpl | 13 +++++++++ .../icon-registry-example.component.html | 21 ++++++++++++++ .../icon-registry-example.component.ts | 28 +++++++++++++++++++ src/app/icon/icon.module.ts | 2 ++ 4 files changed, 64 insertions(+) create mode 100644 src/app/icon/icon-registry-example/icon-registry-example.component.html create mode 100644 src/app/icon/icon-registry-example/icon-registry-example.component.ts diff --git a/src/app/icon/icon-examples/icon-examples.component.tpl b/src/app/icon/icon-examples/icon-examples.component.tpl index a4b0d0fc5..bfb55fbad 100644 --- a/src/app/icon/icon-examples/icon-examples.component.tpl +++ b/src/app/icon/icon-examples/icon-examples.component.tpl @@ -63,3 +63,16 @@ <it-icon-list-example></it-icon-list-example> <it-source-display html="{$ sanitize(htmlList) $}" typescript="{$ sanitize(typescriptList) $}"></it-source-display> + + +{% set htmlRegistry %} + {% include "../icon-registry-example/icon-registry-example.component.html" %} +{% endset %} + +{% set typescriptRegistry %} + {% include "../icon-registry-example/icon-registry-example.component.ts" %} +{% endset %} + +<it-icon-registry-example></it-icon-registry-example> + +<it-source-display html="{$ sanitize(htmlRegistry) $}" typescript="{$ sanitize(typescriptRegistry) $}"></it-source-display> diff --git a/src/app/icon/icon-registry-example/icon-registry-example.component.html b/src/app/icon/icon-registry-example/icon-registry-example.component.html new file mode 100644 index 000000000..82e7548ce --- /dev/null +++ b/src/app/icon/icon-registry-example/icon-registry-example.component.html @@ -0,0 +1,21 @@ +<h4>Custom Icon Registry</h4> +<p>Icone registrate tramite <code>ItIconRegistryService</code>:</p> + +<div class="row mb-3"> + <div class="col-12 col-md-4 text-center"> + <p><strong>Inline SVG</strong></p> + <it-icon name="custom-heart" size="lg" color="danger"></it-icon> + <p class="mt-2"><code>custom-heart</code></p> + </div> + <div class="col-12 col-md-4 text-center"> + <p><strong>Batch registration</strong></p> + <it-icon name="custom-star" size="lg" color="warning"></it-icon> + <it-icon name="custom-check" size="lg" color="success" class="ms-2"></it-icon> + <p class="mt-2"><code>custom-star</code> / <code>custom-check</code></p> + </div> + <div class="col-12 col-md-4 text-center"> + <p><strong>External sprite</strong></p> + <it-icon name="external-icon" size="lg"></it-icon> + <p class="mt-2"><code>external-icon</code> (sprite)</p> + </div> +</div> diff --git a/src/app/icon/icon-registry-example/icon-registry-example.component.ts b/src/app/icon/icon-registry-example/icon-registry-example.component.ts new file mode 100644 index 000000000..cff8435bf --- /dev/null +++ b/src/app/icon/icon-registry-example/icon-registry-example.component.ts @@ -0,0 +1,28 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { ItIconRegistryService } from 'design-angular-kit'; + +@Component({ + selector: 'it-icon-registry-example', + templateUrl: './icon-registry-example.component.html', + standalone: false, +}) +export class IconRegistryExampleComponent implements OnInit { + private readonly iconRegistry = inject(ItIconRegistryService); + + ngOnInit(): void { + // Register a single inline SVG icon + this.iconRegistry.registerIcon( + 'custom-heart', + '<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>' + ); + + // Register multiple icons at once + this.iconRegistry.registerIcons({ + 'custom-star': '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>', + 'custom-check': '<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/>', + }); + + // Register from an external sprite file + this.iconRegistry.registerIconFromSprite('external-icon', '/assets/custom-sprites.svg', 'my-symbol'); + } +} diff --git a/src/app/icon/icon.module.ts b/src/app/icon/icon.module.ts index 746241e58..61dd414d6 100644 --- a/src/app/icon/icon.module.ts +++ b/src/app/icon/icon.module.ts @@ -10,6 +10,7 @@ import { IconSizeExampleComponent } from './icon-size-example/icon-size-example. import { IconListExampleComponent } from './icon-list-example/icon-list-example.component'; import { IconColorExampleComponent } from './icon-color-example/icon-color-example.component'; import { IconAlignmentExampleComponent } from './icon-alignment-example/icon-alignment-example.component'; +import { IconRegistryExampleComponent } from './icon-registry-example/icon-registry-example.component'; @NgModule({ declarations: [ @@ -20,6 +21,7 @@ import { IconAlignmentExampleComponent } from './icon-alignment-example/icon-ali IconListExampleComponent, IconColorExampleComponent, IconAlignmentExampleComponent, + IconRegistryExampleComponent, ], imports: [CommonModule, SharedModule, IconRoutingModule, FormsModule], })