Closed elizabeth-dev closed 1 year ago
I would ask on their community as I'm not familiar with the project. Still find it amazing people still use @mdi/angular-material
instead of @mdi/js
. It's very much a legacy package we just publish it since they asked for it.
I'm in the process of coding a @mdi/angular
component at the moment that will be our recommended approach. It will function very similar to @mdi/react
does for the react community.
Most likely we'll deprecate @mdi/angular-material
once our first party component is complete.
I'm going to leave this open once you reach out to them can you link here so we can see what they say.
I solved this issue by using the single SVGs and importing them into Angular Material individually. I can quickly describe how I automated this:
@mdi/svg
instead of @mdi/angular-material
extract-svg-icons.js
NodeJS script that copies the individual SVGs into a project directory
package.json
:
{
"scripts": {
"postinstall": "node extract-svg-icons.js"
}
}
extract-svg-icons.js (don't just copy and paste this - it contains filenames and paths you might not have/want):
const fs = require('fs');
function writeSvgDefinitionFile(filePath, constName, basePath, iconNames) {
let file =
`// THIS FILE IS GENERATED AUTOMATICALLY BY extract-svg-icons.js
// DO NOT EDIT IT MANUALLY - YOU WILL LOSE YOUR CHANGES
export interface IconSet {
basePath: string;
icons: Array<string>;
}
export const ${constName}: IconSet = {
basePath: '${basePath}',
icons: [\n`;
for (let i = 0; i < iconNames.length; i++) {
file += ` '${iconNames[i]}'`;
if (i < iconNames.length - 1) {
file += `,\n`
}
}
file += `\n ]\n};\n`;
fs.writeFileSync(filePath, new Uint8Array(Buffer.from(file)));
}
function getSvgIconNamesFromDirectory(path) {
return fs.readdirSync(path)
.filter(filename => filename.endsWith('.svg'))
.map(filename => filename.slice(0, -4));
}
function extractMdiIconSet() {
const iconNames = getSvgIconNamesFromDirectory('./node_modules/@mdi/svg/svg');
fs.mkdirSync(`./src/assets/icons/mdi/`, {recursive: true});
iconNames.forEach(iconName => fs.copyFileSync(`./node_modules/@mdi/svg/svg/${iconName}.svg`, `./src/assets/icons/mdi/${iconName}.svg`));
writeSvgDefinitionFile('./src/app/configuration/mdi.ts', 'MDI_SVG_ICONS', 'assets/icons/mdi', iconNames);
}
extractMdiIconSet();
What this script does:
./node_modules/@mdi/svg/svg/
directory to ./src/assets/icons/mdi/
. If you want the icons in a different directory, you need to change the path in the script../src/app/configuration/mdi.ts
) which contains a list of all icon names and their path. If you want that file in a different location you need to change the path in the script.How to import the icons:
// somewhere in your code
// this adds the icons in the mdi namespace. you can customise this as you wish or just use matIconRegistry.addSvgIcon(...) if you don't want the icons to reside in a namespace
MDI_SVG_ICONS.icons.forEach(svgIcon => {
matIconRegistry.addSvgIconInNamespace('mdi', svgIcon, domSanitizer.bypassSecurityTrustResourceUrl(`${MDI_SVG_ICONS.basePath}/${svgIcon}.svg`));
});
Note:
You should add the generated mdi.ts
file as well as the icon directory (per default ./src/assets/icons/mdi
) to your .gitignore
because the files will be generated / copied automatically after npm install
/yarn install
.
@philmtd Creative, but one main issue with that approach is you're including all the icon names in the production build. There doesn't seem to be a tree-shaking step.
The way that Angular Material handles icons seems to be a bit flawed or less than ideal. We might want to make sure that our first party component can be used instead and deprecate other approaches.
Also, we provide @mdi/util
to do a lot of things with @mdi/svg
. It would simplify your code a bit probably. It's used in our NodeJS libs to interact with the meta.json
.
Absolutely, my solution is rather a quick workaround that makes the issue smaller, but it's far from ideal. The first party component you're developing sounds promising but would not entirely solve the issue for a project I'm currently working on as we use more than one icon set and would want to only use one icon component. Probably the Angular (Material) team would have to provide a fix for this including tree shaking at build time, but I'm not sure whether they see this as an important issue and whether this is so easy to have.
I'll take a look at @mdi/util
, thanks for the hint!
Hmmm, I see, I thought about something like what that script does, but I just thought of it as a workaround, as you said.
Well, I was looking for a more "first-party supported" way to use this with Angular Material, but if @mdi/angular-material
should be considered deprecated, I will try to find a way to make things work with @mdi/js
or @mdi/svg
, at least while the new component isn't out.
The only thing I am concerned about is how native Angular Material components use material icons, and how can them be adapted for use with mdi.
The only thing I am concerned about is how native Angular Material components use material icons, and how can them be adapted for use with mdi.
I think a proper solution that integrates best into Angular Material needs to come from the Angular Material team. I tried to suggest a possible idea here but to me it seems like they don't understand the problem or don't think of it as a problem. https://github.com/angular/components/issues/18607
The only thing I am concerned about is how native Angular Material components use material icons, and how can them be adapted for use with mdi.
I think a proper solution that integrates best into Angular Material needs to come from the Angular Material team. I tried to suggest a possible idea here but to me it seems like they don't understand the problem or don't think of it as a problem. angular/components#18607
I was trying to write a PR modifying the mat-icon component allowing it to take a SVG path string (this way @mdi/js
should work)
Another approach using an SVG sprite, in this case using the IonIcons (https://ionicons.com/) The requirements are:
npm install
and generates the spriteMatIconRegistry
For the first point just run npm i ionicons
.
For the second one, make sure to save to following script to the root of your project, I named it postinstall.js
but you are free to choose the name you want.
Also, add the svgstore
package to your dev dependencies by running npm i svgstore -D
// postinstall.js
const fs = require('fs');
const path = require('path');
const svgstore = require('svgstore');
const normalizePath = (folderPath) => path.resolve(path.normalize(folderPath));
const SVG_INPUT_FOLDER = normalizePath('node_modules/ionicons/dist/ionicons/svg');
const SPRITE_OUTPUT_FOLDER = normalizePath('src/assets/images/');
const GENERATED_SPRITE_NAME = path.join(SPRITE_OUTPUT_FOLDER, 'ionic-icons.svg');
const sprite = svgstore();
console.log('Generating SVG Sprite');
fs.readdir(SVG_INPUT_FOLDER, function (err, files) {
if (err) {
return console.log('Ups, there was an error reading the Directory: ' + err);
}
files
.filter(el => /\.svg$/.test(el))
.forEach((file) => {
sprite.add(file.replace('.svg', ''), fs.readFileSync(`${SVG_INPUT_FOLDER}/${file}`, 'utf8').replace(/<title.*>.*?<\/title>/ig, ''));
});
fs.writeFileSync(GENERATED_SPRITE_NAME, sprite);
console.log('Sprite generated successfully');
});
This script reads all the SVGs contained in the node_modules/ionicons/dist/ionicons/svg
folder and saves the sprite as src/assets/images/ionic-icons.svg
.
As I said before this script shall run during every npm install
, this can be achieved by simply editing your package.json
file and adding "postinstall": "node postinstall.js"
in the scripts
section.
Almost there: Registration of the Sprite using MatIconRegistry
.
In my case, I have decided to create a module named Material and then import it into the app.module.ts
// material.module.ts
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
@NgModule({
declarations: [],
exports: [MatIconModule],
providers: [
{
provide: APP_INITIALIZER,
useFactory: configureMatIconRegistry,
multi: true,
deps: [MatIconRegistry, DomSanitizer]
}
]
})
export class MaterialModule {}
export function configureMatIconRegistry(
matIconRegistry: MatIconRegistry,
domSanitizer: DomSanitizer
): () => Promise<void> {
return (): Promise<any> => {
matIconRegistry.addSvgIconSetInNamespace(
'ionicon',
domSanitizer.bypassSecurityTrustResourceUrl('assets/images/ionic-icons.svg')
);
return Promise.resolve();
};
}
The relevant part is the configureMatIconRegistry
but I think is self-explanatory.
Last but not least: Styles Our mat-icon is ready to be used like this:
<mat-icon svgIcon="ionicon:heart-outline"></mat-icon>
But, due to the way these icons are created (I mean the original SVGs) we need some style adjustments. Just make sure to put this in your global styles to target all mat-icons
across the app, don't worry "regular" Material mat-icons will remain unchanged.
// styles.scss
.mat-icon {
&[data-mat-icon-namespace="ionicon"] {
display: inline-block;
width: 24px;
height: 24px;
font-size: 24px;
line-height: 24px;
contain: strict;
fill: currentcolor;
box-sizing: content-box !important;
svg {
stroke: currentcolor;
.ionicon {
stroke: currentColor;
}
.ionicon-fill-none {
fill: none;
}
.ionicon-stroke-width {
stroke-width: 32px;
}
}
.ionicon,
svg {
display: block;
height: 100%;
width: 100%;
}
}
}
And that's pretty much it! Now, with every npm install
you will have all the Ionic Icons ready to be used with the <mat-icon>
@guzmanoj The point of this issue is exactly that we do not want to use an SVG sprite because all icons would always be loaded, even if you only include one in your app.
@philmtd you're right. My idea with this approach is that you can selectively have a folder with the SVGs you need and then create the sprite. 2 miles away from ideal, I know.
Hello to anyone looking at this in 2022. This is how I attacked this on Angular 14:
import * as mdi from './mdi-icons'
import { camelCase } from 'lodash-es'
export class MaterialModule {
constructor(matIconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
for (const [name, path] of Object.entries(mdi)) {
matIconRegistry.addSvgIconLiteral(
kebabCase(name),
sanitizer.bypassSecurityTrustHtml(
`<svg viewBox="0 0 24 24"><path d="${path}"></path></svg>`
)
)
}
}
then in the 'mdi-icons.ts' file only export what you need (to avoid importing the whole set):
export { mdiWrench, mdiCalendarCheck } from '@mdi/js'
Then in your component template:
<mat-icon svgIcon="mdi-wrench"></mat-icon>
The only real benefit here is avoiding the need to name everything twice on import using the addSvgIconLiteral method.
Edit: added a little helper directive to yell at me if I use an icon that's not bundled
import { Directive, Input } from '@angular/core'
import { KebabCase } from 'type-fest'
import * as mdi from './mdi-icons'
export type MdiIconName = KebabCase<keyof typeof mdi>
@Directive({ selector: 'mat-icon[svgIcon]' })
export class MdiIconDirective {
@Input() svgIcon!: MdiIconName
}
When using this library with Angular Material, through the
@mdi/angular-material
package, the getting started guide suggests copying the mdi.svg file to the assets folder, and loading the icon set through theMatIconRegistry
. This works fine, but it has a downside: the full mdi.svg file (1.8mb) is always loaded, even if only one icon is used.Is there any workaround for this? When using
@mdi/js
(with a custom component, as suggested for plain Angular stacks) only the necessary icons are loaded. Can this be adapted for use with Angular Material?