2
0
mirror of https://github.com/iconify/iconify.git synced 2024-12-05 02:33:16 +00:00

feat: custom loader for one icon

This commit is contained in:
Vjacheslav Trushkin 2024-11-03 19:32:50 +02:00
parent 1fad38b291
commit 84f87bacb3
5 changed files with 466 additions and 40 deletions

View File

@ -14,6 +14,10 @@ import type {
IconifyAPICustomQueryParams, IconifyAPICustomQueryParams,
} from './modules'; } from './modules';
import type { IconifyIcon } from '@iconify/types'; import type { IconifyIcon } from '@iconify/types';
import type {
IconifyCustomIconLoader,
IconifyCustomIconsLoader,
} from './types';
/** /**
* Iconify API functions * Iconify API functions
@ -41,6 +45,24 @@ export interface IconifyAPIFunctions {
provider: string, provider: string,
customConfig: PartialIconifyAPIConfig customConfig: PartialIconifyAPIConfig
) => boolean; ) => boolean;
/**
* Set custom loader for multple icons
*/
setCustomIconsLoader: (
callback: IconifyCustomIconsLoader,
prefix: string,
provider?: string
) => void;
/**
* Set custom loader for one icon
*/
setCustomIconLoader: (
callback: IconifyCustomIconLoader,
prefix: string,
provider?: string
) => void;
} }
/** /**

View File

@ -91,7 +91,8 @@ function checkIconNamesForAPI(icons: string[]): CheckIconNames {
function parseLoaderResponse( function parseLoaderResponse(
storage: IconStorageWithAPI, storage: IconStorageWithAPI,
icons: string[], icons: string[],
data: unknown data: unknown,
isAPIResponse: boolean
) { ) {
function checkMissing() { function checkMissing() {
const pending = storage.pendingIcons; const pending = storage.pendingIcons;
@ -119,7 +120,9 @@ function parseLoaderResponse(
} }
// Cache API response // Cache API response
storeInBrowserStorage(storage, data as IconifyJSON); if (isAPIResponse) {
storeInBrowserStorage(storage, data as IconifyJSON);
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -132,6 +135,28 @@ function parseLoaderResponse(
loadedNewIcons(storage); loadedNewIcons(storage);
} }
/**
* Handle response that can be async
*/
function parsePossiblyAsyncResponse<T>(
response: T | null | Promise<T | null>,
callback: (data: T | null) => void
): void {
if (response instanceof Promise) {
// Custom loader is async
response
.then((data) => {
callback(data);
})
.catch(() => {
callback(null);
});
} else {
// Sync loader
callback(response);
}
}
/** /**
* Load icons * Load icons
*/ */
@ -158,22 +183,34 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
return; return;
} }
// Check for custom loader // Check for custom loader for multiple icons
if (storage.customLoader) { const customIconLoader = storage.loadIcon;
const response = storage.customLoader(icons, prefix, provider); if (storage.loadIcons && (icons.length > 1 || !customIconLoader)) {
if (response instanceof Promise) { parsePossiblyAsyncResponse(
// Custom loader is async storage.loadIcons(icons, prefix, provider),
response (data) => {
.then((data) => { parseLoaderResponse(storage, icons, data, false);
parseLoaderResponse(storage, icons, data); }
}) );
.catch(() => { return;
parseLoaderResponse(storage, icons, null); }
});
} else { // Check for custom loader for one icon
// Sync loader if (customIconLoader) {
parseLoaderResponse(storage, icons, response); icons.forEach((name) => {
} const response = customIconLoader(name, prefix, provider);
parsePossiblyAsyncResponse(response, (data) => {
const iconSet: IconifyJSON | null = data
? {
prefix,
icons: {
[name]: data,
},
}
: null;
parseLoaderResponse(storage, [name], iconSet, false);
});
});
return; return;
} }
@ -183,7 +220,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
if (invalid.length) { if (invalid.length) {
// Invalid icons // Invalid icons
parseLoaderResponse(storage, invalid, null); parseLoaderResponse(storage, invalid, null, false);
} }
if (!valid.length) { if (!valid.length) {
// No valid icons to load // No valid icons to load
@ -196,7 +233,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
: null; : null;
if (!api) { if (!api) {
// API module not found // API module not found
parseLoaderResponse(storage, valid, null); parseLoaderResponse(storage, valid, null, false);
return; return;
} }
@ -204,7 +241,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
const params = api.prepare(provider, prefix, valid); const params = api.prepare(provider, prefix, valid);
params.forEach((item) => { params.forEach((item) => {
sendAPIQuery(provider, item, (data) => { sendAPIQuery(provider, item, (data) => {
parseLoaderResponse(storage, item.icons, data); parseLoaderResponse(storage, item.icons, data, true);
}); });
}); });
}); });

View File

@ -1,15 +1,35 @@
import { getStorage } from '../storage/storage.js'; import { getStorage } from '../storage/storage';
import type { IconifyCustomLoader, IconStorageWithAPI } from './types.js'; import type {
IconifyCustomIconLoader,
IconifyCustomIconsLoader,
IconStorageWithAPI,
} from './types';
// Custom loaders
// You can set only one of these loaders, whichever is more suitable for your use case.
/** /**
* Set custom loader * Set custom loader for multiple icons
*/ */
export function setCustomLoader( export function setCustomIconsLoader(
loader: IconifyCustomLoader, loader: IconifyCustomIconsLoader,
prefix: string, prefix: string,
provider?: string provider?: string
): void { ): void {
// Assign loader directly to storage // Assign loader directly to storage
(getStorage(provider || '', prefix) as IconStorageWithAPI).customLoader = (getStorage(provider || '', prefix) as IconStorageWithAPI).loadIcons =
loader;
}
/**
* Set custom loader for one icon
*/
export function setCustomIconLoader(
loader: IconifyCustomIconLoader,
prefix: string,
provider?: string
): void {
// Assign loader directly to storage
(getStorage(provider || '', prefix) as IconStorageWithAPI).loadIcon =
loader; loader;
} }

View File

@ -4,17 +4,26 @@ import type {
} from './icons'; } from './icons';
import type { SortedIcons } from '../icon/sort'; import type { SortedIcons } from '../icon/sort';
import type { IconStorage } from '../storage/storage'; import type { IconStorage } from '../storage/storage';
import { IconifyJSON } from '@iconify/types'; import { IconifyIcon, IconifyJSON } from '@iconify/types';
/** /**
* Custom icons loader * Custom icons loader
*/ */
export type IconifyCustomLoader = ( export type IconifyCustomIconsLoader = (
icons: string[], icons: string[],
prefix: string, prefix: string,
provider: string provider: string
) => Promise<IconifyJSON | null> | IconifyJSON | null; ) => Promise<IconifyJSON | null> | IconifyJSON | null;
/**
* Custom loader for one icon
*/
export type IconifyCustomIconLoader = (
name: string,
prefix: string,
provider: string
) => Promise<IconifyIcon | null> | IconifyIcon | null;
/** /**
* Storage for callbacks * Storage for callbacks
*/ */
@ -36,8 +45,18 @@ export interface APICallbackItem {
* Add custom stuff to storage * Add custom stuff to storage
*/ */
export interface IconStorageWithAPI extends IconStorage { export interface IconStorageWithAPI extends IconStorage {
// Custom loader /**
customLoader?: IconifyCustomLoader; * Custom loaders
*
* If custom loader is set, API module will not be used to load icons.
*
* You can set only one of these loaders.
*
* If both loaders are set, loader for one icon will be used when requesting only once icon,
* loader for multiple icons will be used when requesting multiple icons.
*/
loadIcons?: IconifyCustomIconsLoader;
loadIcon?: IconifyCustomIconLoader;
/** /**
* List of icons that are being loaded, added to storage * List of icons that are being loaded, added to storage

View File

@ -1,6 +1,9 @@
import { defaultIconProps } from '@iconify/utils'; import { defaultIconProps } from '@iconify/utils';
import { loadIcons, loadIcon } from '../../lib/api/icons'; import { loadIcons, loadIcon } from '../../lib/api/icons';
import { setCustomLoader } from '../../lib/api/loaders'; import {
setCustomIconsLoader,
setCustomIconLoader,
} from '../../lib/api/loaders';
import { listIcons } from '../../lib/storage/storage'; import { listIcons } from '../../lib/storage/storage';
describe('Testing API loadIcons', () => { describe('Testing API loadIcons', () => {
@ -10,13 +13,13 @@ describe('Testing API loadIcons', () => {
return `loader-test-${prefixCounter < 10 ? '0' : ''}${prefixCounter}`; return `loader-test-${prefixCounter < 10 ? '0' : ''}${prefixCounter}`;
} }
it('Custom async loader with loadIcon', () => { it('Custom async loader for multiple icons with loadIcon', () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const provider = nextPrefix(); const provider = nextPrefix();
const prefix = nextPrefix(); const prefix = nextPrefix();
// Set loader // Set loader
setCustomLoader( setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => { (icons, requestedPrefix, requestedProvider) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
@ -72,13 +75,13 @@ describe('Testing API loadIcons', () => {
}); });
}); });
it('Custom sync loader with loadIcon', () => { it('Custom sync loader for multiple icons with loadIcon', () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const provider = nextPrefix(); const provider = nextPrefix();
const prefix = nextPrefix(); const prefix = nextPrefix();
// Set loader // Set loader
setCustomLoader( setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => { (icons, requestedPrefix, requestedProvider) => {
try { try {
// Check params // Check params
@ -129,14 +132,14 @@ describe('Testing API loadIcons', () => {
}); });
}); });
it('Missing icons', () => { it('Loader multiple icons with missing icons', () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const provider = nextPrefix(); const provider = nextPrefix();
const prefix1 = nextPrefix(); const prefix1 = nextPrefix();
const prefix2 = nextPrefix(); const prefix2 = nextPrefix();
// Set loaders: one is sync, one is async // Set loaders: one is sync, one is async
setCustomLoader( setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => { (icons, requestedPrefix, requestedProvider) => {
try { try {
// Check params // Check params
@ -161,7 +164,7 @@ describe('Testing API loadIcons', () => {
prefix1, prefix1,
provider provider
); );
setCustomLoader( setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => { (icons, requestedPrefix, requestedProvider) => {
try { try {
// Check params // Check params
@ -197,7 +200,7 @@ describe('Testing API loadIcons', () => {
provider provider
); );
// Load icon // Load icons
loadIcons( loadIcons(
[ [
`${provider}:${prefix1}:icon1`, `${provider}:${prefix1}:icon1`,
@ -250,4 +253,329 @@ describe('Testing API loadIcons', () => {
); );
}); });
}); });
it('Custom async loader for one icon with loadIcon', () => {
return new Promise((resolve, reject) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set loader
setCustomIconLoader(
(name, requestedPrefix, requestedProvider) => {
return new Promise((resolve, reject) => {
try {
// Check params
expect(name).toBe('icon1');
expect(requestedPrefix).toBe(prefix);
expect(requestedProvider).toBe(provider);
} catch (err) {
reject(err);
return;
}
// Send data
resolve({
body: '<path d="" />',
});
});
},
prefix,
provider
);
// Load icon
loadIcon(provider + ':' + prefix + ':icon1')
.then((data) => {
try {
// Test response
expect(data).toEqual({
...defaultIconProps,
body: '<path d="" />',
});
// Check storage
expect(listIcons(provider, prefix)).toEqual([
`@${provider}:${prefix}:icon1`,
]);
} catch (err) {
reject(err);
return;
}
resolve(true);
})
.catch(reject);
});
});
it('Loader multiple icons with loader for one icon', () => {
return new Promise((resolve, reject) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
const iconsToTest: Record<string, Set<string>> = {
[prefix1]: new Set(['icon1']),
[prefix2]: new Set(['Icon_2', 'BadIcon']),
};
// Set loaders: one is sync, one is async
setCustomIconLoader(
(requestedName, requestedPrefix, requestedProvider) => {
try {
// Check params
expect(requestedPrefix).toBe(prefix1);
expect(requestedProvider).toBe(provider);
expect(
iconsToTest[prefix1].has(requestedName)
).toBeTruthy();
} catch (err) {
reject(err);
return null;
}
// Send data
return {
body: `<g data-name="${requestedName}" />`,
};
},
prefix1,
provider
);
setCustomIconLoader(
(requestedName, requestedPrefix, requestedProvider) => {
try {
// Check params
expect(requestedPrefix).toBe(prefix2);
expect(requestedProvider).toBe(provider);
expect(
iconsToTest[prefix2].has(requestedName)
).toBeTruthy();
} catch (err) {
reject(err);
return null;
}
// Send data asynchronously, without 'BadIcon'
return new Promise((resolve) => {
setTimeout(() => {
resolve(
requestedName === 'BadIcon'
? null
: {
body: `<g data-name="${requestedName}" />`,
}
);
}, 150);
});
},
prefix2,
provider
);
// Load icons
loadIcons(
[
`${provider}:${prefix1}:icon1`,
`${provider}:${prefix2}:Icon_2`,
`${provider}:${prefix2}:BadIcon`,
],
(loaded, missing, pending) => {
if (pending.length) {
// Could be called before all icons are loaded because multiple async requests
return;
}
try {
// Test response
expect(loaded).toEqual([
{
provider,
prefix: prefix1,
name: 'icon1',
},
{
provider,
prefix: prefix2,
name: 'Icon_2',
},
]);
expect(missing).toEqual([
{
provider,
prefix: prefix2,
name: 'BadIcon',
},
]);
// Check storage
expect(listIcons(provider, prefix1)).toEqual([
`@${provider}:${prefix1}:icon1`,
]);
expect(listIcons(provider, prefix2)).toEqual([
`@${provider}:${prefix2}:Icon_2`,
]);
} catch (err) {
reject(err);
return;
}
resolve(true);
}
);
});
});
it('Loaders for one and multiple icons, requesting one icon', () => {
return new Promise((resolve, reject) => {
const provider = nextPrefix();
const prefix = nextPrefix();
const name = 'TestIcon';
// Set loaders: one is sync, one is async
setCustomIconLoader(
(requestedName, requestedPrefix, requestedProvider) => {
try {
// Check params
expect(requestedPrefix).toBe(prefix);
expect(requestedProvider).toBe(provider);
expect(requestedName).toBe(name);
} catch (err) {
reject(err);
return null;
}
// Send data
return {
body: `<g data-name="${requestedName}" />`,
};
},
prefix,
provider
);
setCustomIconsLoader(
() => {
reject(
new Error(
'Loader for multple icons should not be called'
)
);
return null;
},
prefix,
provider
);
// Load icon
loadIcons(
[`${provider}:${prefix}:${name}`],
(loaded, missing, pending) => {
try {
// Test response
expect(loaded).toEqual([
{
provider,
prefix,
name,
},
]);
expect(missing).toEqual([]);
expect(pending).toEqual([]);
// Check storage
expect(listIcons(provider, prefix)).toEqual([
`@${provider}:${prefix}:${name}`,
]);
} catch (err) {
reject(err);
return;
}
resolve(true);
}
);
});
});
it('Loaders for one and multiple icons, requesting multiple icons', () => {
return new Promise((resolve, reject) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set loaders: one is sync, one is async
setCustomIconsLoader(
(requestedNames, requestedPrefix, requestedProvider) => {
try {
// Check params
expect(requestedPrefix).toBe(prefix);
expect(requestedProvider).toBe(provider);
expect(requestedNames).toEqual(['Icon_1', 'Icon_2']);
} catch (err) {
reject(err);
return null;
}
// Send data
return {
prefix,
icons: {
Icon_1: {
body: `<g data-name="Icon_1" />`,
},
Icon_3: {
body: `<g data-name="Icon_3" />`,
},
},
};
},
prefix,
provider
);
setCustomIconLoader(
() => {
reject(
new Error('Loader for one icon should not be called')
);
return null;
},
prefix,
provider
);
// Load icon
loadIcons(
[
`${provider}:${prefix}:Icon_1`,
`${provider}:${prefix}:Icon_2`,
],
(loaded, missing, pending) => {
try {
// Test response
expect(loaded).toEqual([
{
provider,
prefix,
name: 'Icon_1',
},
]);
expect(missing).toEqual([
{
provider,
prefix,
name: 'Icon_2',
},
]);
expect(pending).toEqual([]);
// Check storage
expect(listIcons(provider, prefix)).toEqual([
`@${provider}:${prefix}:Icon_1`,
`@${provider}:${prefix}:Icon_3`,
]);
} catch (err) {
reject(err);
return;
}
resolve(true);
}
);
});
});
}); });