Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created January 17, 2019 13:42
Show Gist options
  • Select an option

  • Save bennadel/1b11e8a790a7d3aa63fe05b8bfb72e7d to your computer and use it in GitHub Desktop.

Select an option

Save bennadel/1b11e8a790a7d3aa63fe05b8bfb72e7d to your computer and use it in GitHub Desktop.
Creating SVG Icon Components And SVG Icon Sprites In Angular 7.2.0
:host {
color: inherit ;
display: inline-block ;
// Let the app-icon naturally scale with the font-size. Since this is bound to the
// host element, it can easily be overridden by contextual styling.
height: 1em ;
width: 1em ;
}
svg {
// Let the app-icon fill color take on the same color as the contextual text.
color: inherit ;
fill: currentColor ;
display: block ;
// Scale the SVG to cover the whole app-icon container.
height: 100% ;
width: 100% ;
}
// Import the core angular services.
import { ChangeDetectionStrategy } from "@angular/core";
import { Component } from "@angular/core";
import { OnChanges } from "@angular/core";
// Import the application components and services.
import { UniqueIDService } from "./unique-id.service";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I define the collection of SVG icons that are pulled into this sprite (via Webpack
// svg-sprite-loader). I like having them listed here individually so that icons can be
// explicitly included or excluded from the build without having to remove the SVG files
// from the actual file-system. This feels more maintainable.
import "./svg/chevron-circle-left.svg";
import "./svg/chevron-circle-right.svg";
import "./svg/chevron-down.svg";
import "./svg/chevron-left.svg";
import "./svg/chevron-right.svg";
import "./svg/chevron-up.svg";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// CAUTION: I have no experience with ARIA configurations. I have tried my best to apply
// what I read in the following post; however, please do not view the ARIA implementation
// in this Icon component as "correct". I am not sure how the "app-icon" wrapper affects
// the implementation details. My approach was to treat the "app-icon" like the IMG and
// then "hide" the SVG itself from the device.
// --
// Read More: https://www.24a11y.com/2018/accessible-svg-icons-with-inline-sprites/
@Component({
selector: "app-icon",
inputs: [ "type", "title" ],
host: {
"[attr.title]": "ariaTitle",
"[attr.aria-hidden]": "ariaHidden",
"[attr.aria-labelledby]": "ariaLabelledBy",
"role": "img"
},
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: [ "./app-icon.component.less" ],
template:
`
<svg aria-hidden="true" focusable="false">
<title *ngIf="ariaTitle" [attr.id]="ariaLabelledBy">
{{ ariaTitle }}
</title>
<use [attr.xlink:href]="( '#app-icon-' + type )" />
</svg>
`
})
export class AppIconComponent implements OnChanges {
public ariaHidden: true | null;
public ariaLabelledBy: string | null;
public ariaTitle: string | null;
public title!: string;
public type!: string;
private uniqueIDService: UniqueIDService;
// I initialize the app-icon component.
constructor( uniqueIDService: UniqueIDService ) {
this.uniqueIDService = uniqueIDService;
this.ariaHidden = true;
this.ariaLabelledBy = null;
this.ariaTitle = null;
}
// ---
// PUBLIC METHODS.
// ---
// I get called when the input binding are updated.
public ngOnChanges() : void {
if ( this.title ) {
// If a title was provided, it means that this icon is more than just a
// decorative element. As such, let's try to make it more accessible to
// screen-readers.
this.ariaHidden = null;
this.ariaLabelledBy = ( this.ariaLabelledBy || this.uniqueIDService.next() );
this.ariaTitle = this.title;
} else {
// If there is no title, we want to hide this icon from screen-readers.
this.ariaHidden = true;
this.ariaLabelledBy = null;
this.ariaTitle = null;
}
}
}
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
template:
`
<div class="counter">
<button (click)="( counter = counter - 1 )" class="counter__left">
<app-icon
type="chevron-circle-left"
title="Decrement Counter"
class="counter__icon">
</app-icon>
</button>
<div class="counter__value">
{{ counter }}
</div>
<button (click)="( counter = counter + 1 )" class="counter__right">
<app-icon
type="chevron-circle-right"
title="Increment Counter"
class="counter__icon">
</app-icon>
</button>
</div>
<!-- To demonstrate what the rendered HTML looks like without [title]. -->
<div class="small-icons">
<app-icon type="chevron-left"></app-icon>
<app-icon type="chevron-up"></app-icon>
<app-icon type="chevron-right"></app-icon>
<app-icon type="chevron-down"></app-icon>
</div>
`
})
export class AppComponent {
public counter: number = 0;
}
// Import the core angular services.
import { Injectable } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Injectable({
providedIn: "root"
})
export class UniqueIDService {
private id: number = Date.now();
// ---
// PUBLIC METHODS.
// ---
// I get the next available unique ID.
public next() : string {
return( `aria-id-${ ++this.id }` );
}
}
// .... truncated for snippet ....
module: {
rules: [
// I build the SVG sprite for the Angular application. By using the
// "include" property and the "symbolId" property, I can associate the
// SVG files in one directory with a specific set of unique IDs.
// --
// NOTE: If you provide multiple instances of the "svg-sprite-loader",
// each instance can use a different "include" property and set of
// options. These SVGs will all end-up in a single "sprite"; but, they
// will each be named according to their own options.
{
test: /\.(svg)$/,
loader: "svg-sprite-loader",
include: path.join( __dirname, "app/icons/svg/" ),
options: {
symbolId: "app-icon-[name]"
}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment