2
0
mirror of https://github.com/iconify/iconify.git synced 2024-12-04 18:23:17 +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,
} from './modules';
import type { IconifyIcon } from '@iconify/types';
import type {
IconifyCustomIconLoader,
IconifyCustomIconsLoader,
} from './types';
/**
* Iconify API functions
@ -41,6 +45,24 @@ export interface IconifyAPIFunctions {
provider: string,
customConfig: PartialIconifyAPIConfig
) => 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(
storage: IconStorageWithAPI,
icons: string[],
data: unknown
data: unknown,
isAPIResponse: boolean
) {
function checkMissing() {
const pending = storage.pendingIcons;
@ -119,7 +120,9 @@ function parseLoaderResponse(
}
// Cache API response
storeInBrowserStorage(storage, data as IconifyJSON);
if (isAPIResponse) {
storeInBrowserStorage(storage, data as IconifyJSON);
}
} catch (err) {
console.error(err);
}
@ -132,6 +135,28 @@ function parseLoaderResponse(
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
*/
@ -158,22 +183,34 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
return;
}
// Check for custom loader
if (storage.customLoader) {
const response = storage.customLoader(icons, prefix, provider);
if (response instanceof Promise) {
// Custom loader is async
response
.then((data) => {
parseLoaderResponse(storage, icons, data);
})
.catch(() => {
parseLoaderResponse(storage, icons, null);
});
} else {
// Sync loader
parseLoaderResponse(storage, icons, response);
}
// Check for custom loader for multiple icons
const customIconLoader = storage.loadIcon;
if (storage.loadIcons && (icons.length > 1 || !customIconLoader)) {
parsePossiblyAsyncResponse(
storage.loadIcons(icons, prefix, provider),
(data) => {
parseLoaderResponse(storage, icons, data, false);
}
);
return;
}
// Check for custom loader for one icon
if (customIconLoader) {
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;
}
@ -183,7 +220,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
if (invalid.length) {
// Invalid icons
parseLoaderResponse(storage, invalid, null);
parseLoaderResponse(storage, invalid, null, false);
}
if (!valid.length) {
// No valid icons to load
@ -196,7 +233,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
: null;
if (!api) {
// API module not found
parseLoaderResponse(storage, valid, null);
parseLoaderResponse(storage, valid, null, false);
return;
}
@ -204,7 +241,7 @@ function loadNewIcons(storage: IconStorageWithAPI, icons: string[]): void {
const params = api.prepare(provider, prefix, valid);
params.forEach((item) => {
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 type { IconifyCustomLoader, IconStorageWithAPI } from './types.js';
import { getStorage } from '../storage/storage';
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(
loader: IconifyCustomLoader,
export function setCustomIconsLoader(
loader: IconifyCustomIconsLoader,
prefix: string,
provider?: string
): void {
// 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;
}

View File

@ -4,17 +4,26 @@ import type {
} from './icons';
import type { SortedIcons } from '../icon/sort';
import type { IconStorage } from '../storage/storage';
import { IconifyJSON } from '@iconify/types';
import { IconifyIcon, IconifyJSON } from '@iconify/types';
/**
* Custom icons loader
*/
export type IconifyCustomLoader = (
export type IconifyCustomIconsLoader = (
icons: string[],
prefix: string,
provider: string
) => 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
*/
@ -36,8 +45,18 @@ export interface APICallbackItem {
* Add custom stuff to storage
*/
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

View File

@ -1,6 +1,9 @@
import { defaultIconProps } from '@iconify/utils';
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';
describe('Testing API loadIcons', () => {
@ -10,13 +13,13 @@ describe('Testing API loadIcons', () => {
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) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set loader
setCustomLoader(
setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => {
return new Promise((resolve, reject) => {
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) => {
const provider = nextPrefix();
const prefix = nextPrefix();
// Set loader
setCustomLoader(
setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => {
try {
// 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) => {
const provider = nextPrefix();
const prefix1 = nextPrefix();
const prefix2 = nextPrefix();
// Set loaders: one is sync, one is async
setCustomLoader(
setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => {
try {
// Check params
@ -161,7 +164,7 @@ describe('Testing API loadIcons', () => {
prefix1,
provider
);
setCustomLoader(
setCustomIconsLoader(
(icons, requestedPrefix, requestedProvider) => {
try {
// Check params
@ -197,7 +200,7 @@ describe('Testing API loadIcons', () => {
provider
);
// Load icon
// Load icons
loadIcons(
[
`${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);
}
);
});
});
});