Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(icon-service): cache svg icons as text #10202

Merged
merged 28 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
699ca5f
refactor(icon-service): cache svg icons as text
simeonoff Sep 27, 2021
421a55f
refactor(icon): return SafeHtml from IconService
simeonoff Sep 27, 2021
954a326
spec(icon-service): update
simeonoff Sep 27, 2021
1d355d3
refactor(icon): remove lifecycle hook changes
simeonoff Sep 27, 2021
1914241
docs(icon, icon-service): update after changes
simeonoff Sep 27, 2021
9a66b44
docs(icon-service): update
simeonoff Sep 28, 2021
52ee506
refactor(icon): update template and theme
simeonoff Sep 28, 2021
e95d257
chore(*): update svg assets
simeonoff Sep 28, 2021
02de7a8
refactor(icon-service): simplify family initialization
simeonoff Sep 28, 2021
9053bd4
spec(icon-service): fix failing test
simeonoff Sep 28, 2021
1481f3c
Merge branch 'master' into simeonoff/icon-service
simeonoff Sep 28, 2021
676e2b1
Merge branch 'master' into simeonoff/icon-service
simeonoff Oct 1, 2021
f60159c
refactor(advanced-filtering): show friendly name for igx-select text
simeonoff Oct 1, 2021
d0a7a28
Merge branch 'master' into simeonoff/icon-service
simeonoff Oct 1, 2021
b56d8be
refactor(excel-filter): use condition friendly name
simeonoff Oct 1, 2021
967a80b
Merge branch 'master' into simeonoff/icon-service
simeonoff Oct 4, 2021
54ca87f
refactor(icon, icon-service): simplify code
simeonoff Oct 4, 2021
0252f7c
test(grid): dont include the svg icon in the query for dropdown item
hanastasov Oct 4, 2021
99305c9
Merge branch 'master' into simeonoff/icon-service
simeonoff Oct 4, 2021
b14c0ee
refactor(icon, icon service): cache svg icons as SafeHtml
simeonoff Oct 7, 2021
2839515
refactor(icon): replace svg wrapper with a div
simeonoff Oct 7, 2021
52ab7d5
refactor(demos): add a new test icon
simeonoff Oct 7, 2021
9e14ddc
Merge branch 'master' into simeonoff/icon-service
simeonoff Oct 7, 2021
5ad93ba
Merge branch 'master' into simeonoff/icon-service
hanastasov Oct 8, 2021
5f652ac
refactor(icon): set svg and div to display block
simeonoff Oct 8, 2021
bded70b
Merge branch 'master' into simeonoff/icon-service
hanastasov Oct 8, 2021
28e7d01
Merge branch 'master' into simeonoff/icon-service
simeonoff Oct 8, 2021
a03d45e
Merge branch 'master' into simeonoff/icon-service
simeonoff Oct 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@
width: inherit;
height: inherit;
fill: currentColor;

use {
pointer-events: none;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ <h6 class="igx-filter-empty__title">
<igx-icon family="imx-icons" [name]="selectedColumn.filters.condition(conditionSelect.value).iconName">
</igx-icon>
</igx-prefix>
<igx-select-item *ngFor="let condition of getConditionList()" [value]="condition">
<igx-select-item *ngFor="let condition of getConditionList()" [value]="condition" [text]="getConditionFriendlyName(condition)">
simeonoff marked this conversation as resolved.
Show resolved Hide resolved
<div class="igx-grid__filtering-dropdown-items">
<igx-icon family="imx-icons"
[name]="selectedColumn.filters.condition(condition).iconName">
Expand Down
10 changes: 7 additions & 3 deletions projects/igniteui-angular/src/lib/icon/icon.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
</ng-template>

<ng-template #svgImage>
<svg>
<use [attr.href]="getSvgKey"></use>
</svg>
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
[innerHTML]="getSvg"
simeonoff marked this conversation as resolved.
Show resolved Hide resolved
></svg>
</ng-template>

<ng-container *ngTemplateOutlet="template"></ng-container>
32 changes: 19 additions & 13 deletions projects/igniteui-angular/src/lib/icon/icon.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IgxIconService } from './icon.service';
import { first, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { DeprecateProperty } from '../core/deprecateDecorators';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

/**
* Icon provides a way to include material icons to markup
Expand Down Expand Up @@ -114,16 +115,20 @@ export class IgxIconComponent implements OnInit, OnDestroy {

private destroy$ = new Subject<void>();

constructor(public el: ElementRef,
private iconService: IgxIconService,
private ref: ChangeDetectorRef) {
constructor(
public el: ElementRef,
private iconService: IgxIconService,
private ref: ChangeDetectorRef,
private sanitizer: DomSanitizer
) {
this.family = this.iconService.defaultFamily;
this.iconService.registerFamilyAlias('material', 'material-icons');
this.iconService.iconLoaded.pipe(
first(e => e.name === this.name && e.family === this.family),
takeUntil(this.destroy$)
)
.subscribe(() => this.ref.detectChanges());
this.iconService.iconLoaded
.pipe(
first((e) => e.name === this.name && e.family === this.family),
takeUntil(this.destroy$)
)
.subscribe(() => this.ref.detectChanges());
}

/**
Expand Down Expand Up @@ -226,21 +231,22 @@ export class IgxIconComponent implements OnInit, OnDestroy {
}

/**
* An accessor that returns the key of the SVG image.
* The key consists of the font-family and the name separated by underscore.
* An accessor that returns the underlying SVG image as SafeHtml.
*
* @example
* ```typescript
* @ViewChild("MyIcon")
* public icon: IgxIconComponent;
* ngAfterViewInit() {
* let svgKey = this.icon.getSvgKey;
* let svg: SafeHtml = this.icon.getSvg;
* }
* ```
*/
public get getSvgKey(): string {
public get getSvg(): SafeHtml {
if (this.iconService.isSvgIconCached(this.name, this.family)) {
return '#' + this.iconService.getSvgIconKey(this.name, this.family);
const svgText = this.iconService.getSvgIcon(this.name, this.family);
const svg = this.sanitizer.bypassSecurityTrustHtml(svgText);
return svg;
}

return null;
Expand Down
35 changes: 7 additions & 28 deletions projects/igniteui-angular/src/lib/icon/icon.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { TestBed, fakeAsync } from '@angular/core/testing';
import { IgxIconService } from './icon.service';
import { DOCUMENT } from '@angular/common';

import { configureTestSuite } from '../test-utils/configure-suite';
import { first } from 'rxjs/operators';
Expand Down Expand Up @@ -53,13 +52,11 @@ describe('Icon Service', () => {
expect(iconService.familyClassName(ALIAS)).toBe(MY_FONT);
});

it('should add custom svg icon from url', () => {
it('should add custom svg icon from url', fakeAsync((done) => {
const iconService = TestBed.inject(IgxIconService) as IgxIconService;
const document = TestBed.inject(DOCUMENT);

const name = 'test';
const family = 'svg-icons';
const iconKey = family + '_' + name;

spyOn(XMLHttpRequest.prototype, 'open').and.callThrough();
spyOn(XMLHttpRequest.prototype, 'send');
Expand All @@ -69,25 +66,20 @@ describe('Icon Service', () => {
expect(XMLHttpRequest.prototype.open).toHaveBeenCalledTimes(1);
expect(XMLHttpRequest.prototype.send).toHaveBeenCalledTimes(1);

const svgElement = document.querySelector(`svg[id='${iconKey}']`);
expect(svgElement).toBeDefined();
});
iconService.iconLoaded.pipe().subscribe(() => {
expect(iconService.isSvgIconCached(name, family)).toBeTruthy();
done();
});
}));

it('should add custom svg icon from text', () => {
const iconService = TestBed.inject(IgxIconService) as IgxIconService;
const document = TestBed.inject(DOCUMENT);

const name = 'test';
const family = 'svg-icons';
const iconKey = family + '_' + name;

iconService.addSvgIconFromText(name, svgText, family);

expect(iconService.isSvgIconCached(name, family)).toBeTruthy();
expect(iconService.getSvgIconKey(name, family)).toEqual(iconKey);

const svgElement = document.querySelector(`svg[id='${iconKey}']`);
expect(svgElement).toBeDefined();
});

it('should emit loading event for a custom svg icon from url', done => {
Expand All @@ -113,17 +105,4 @@ describe('Icon Service', () => {

iconService.addSvgIcon(name, 'test.svg', family);
});

it('should create svg container inside the body', () => {
const iconService = TestBed.inject(IgxIconService) as IgxIconService;
const document = TestBed.inject(DOCUMENT);

const name = 'test';
const family = 'svg-icons';

iconService.addSvgIconFromText(name, svgText, family);

const svgContainer = document.body.querySelector('.igx-svg-container');
expect(svgContainer).not.toBeNull();
});
});
80 changes: 22 additions & 58 deletions projects/igniteui-angular/src/lib/icon/icon.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, SecurityContext, Inject, OnDestroy, Optional } from '@angular/core';
import { Injectable, SecurityContext, Inject, Optional } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { DOCUMENT } from '@angular/common';
import { HttpClient } from '@angular/common/http';
Expand Down Expand Up @@ -32,7 +32,7 @@ export interface IgxIconLoadedEvent {
@Injectable({
providedIn: 'root'
})
export class IgxIconService implements OnDestroy {
export class IgxIconService {
/**
* Observable that emits when an icon is successfully loaded
* through a HTTP request.
Expand All @@ -46,8 +46,7 @@ export class IgxIconService implements OnDestroy {

private _family = 'material-icons';
private _familyAliases = new Map<string, string>();
private _svgContainer: HTMLElement;
private _cachedSvgIcons: Set<string> = new Set<string>();
private _cachedSvgIcons = new Map<string, Map<string, string>>();
private _iconLoaded = new Subject<IgxIconLoadedEvent>();

constructor(
Expand All @@ -58,14 +57,6 @@ export class IgxIconService implements OnDestroy {
this.iconLoaded = this._iconLoaded.asObservable();
}

/**
* @hidden
* @internal
*/
public ngOnDestroy(): void {
this.cleanSvgContainer();
}

/**
* Returns the default font-family.
* ```typescript
Expand Down Expand Up @@ -162,18 +153,25 @@ export class IgxIconService implements OnDestroy {
* ```
*/
public isSvgIconCached(name: string, family: string = ''): boolean {
const iconKey = this.getSvgIconKey(name, family);
return this._cachedSvgIcons.has(iconKey);
if(this._cachedSvgIcons.has(family)) {
const familyRegistry = this._cachedSvgIcons.get(family) as Map<string, string>;
return familyRegistry.has(name);
}

return false;
}

/**
* Returns the key of a cached SVG image.
* Returns the cached SVG image as string.
* ```typescript
* const svgIconKey = this.iconService.getSvgIconKey('aruba', 'svg-flags');
* const svgIcon = this.iconService.getSvgIcon('aruba', 'svg-flags');
* ```
*/
public getSvgIconKey(name: string, family: string = '') {
return family + '_' + name;
public getSvgIcon(name: string, family: string = '') {
if(this._cachedSvgIcons.has(family)) {
const familyRegistry = this._cachedSvgIcons.get(family) as Map<string, string>;
return familyRegistry.get(name);
}
simeonoff marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -184,57 +182,23 @@ export class IgxIconService implements OnDestroy {
return req;
}

/**
* @hidden
*/
private cleanSvgContainer() {
const container = this._document.documentElement.querySelector('.igx-svg-container');

while (container.firstChild) {
container.removeChild(container.firstChild);
}
}

/**
* @hidden
*/
private cacheSvgIcon(name: string, value: string, family: string = '') {
if (name && value) {
this.ensureSvgContainerCreated();

const div = this._document.createElement('DIV');
const div = this._document.createElement('div');
pmoleri marked this conversation as resolved.
Show resolved Hide resolved
div.innerHTML = value;
const svg = div.querySelector('svg') as SVGElement;

if (svg) {
const iconKey = this.getSvgIconKey(name, family);
if (!this._cachedSvgIcons.has(family)) {
this._cachedSvgIcons.set(family, new Map<string, string>());
}

svg.setAttribute('id', iconKey);
if (svg) {
svg.setAttribute('fit', '');
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable.

if (this.isSvgIconCached(name, family)) {
const oldChild = this._svgContainer.querySelector(`svg[id='${iconKey}']`);
this._svgContainer.removeChild(oldChild);
}

this._svgContainer.appendChild(svg);
this._cachedSvgIcons.add(iconKey);
}
}
}

/**
* @hidden
*/
private ensureSvgContainerCreated() {
if (!this._svgContainer) {
this._svgContainer = this._document.documentElement.querySelector('.igx-svg-container');
if (!this._svgContainer) {
this._svgContainer = this._document.createElement('DIV');
this._svgContainer.classList.add('igx-svg-container');
this._document.body.appendChild(this._svgContainer);
this._cachedSvgIcons.get(family).set(name, svg.outerHTML);
}
}
}
Expand Down
19 changes: 1 addition & 18 deletions src/assets/svg/filtering/contains.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 2 additions & 19 deletions src/assets/svg/filtering/does_not_contain.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 1 addition & 18 deletions src/assets/svg/filtering/does_not_equal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading