diff --git a/packages/react/src/iconify.ts b/packages/react/src/iconify.ts index 81f7246..5e555e6 100644 --- a/packages/react/src/iconify.ts +++ b/packages/react/src/iconify.ts @@ -2,7 +2,7 @@ import React from 'react'; import type { IconifyJSON } from '@iconify/types'; // Core -import type { IconifyIconName } from '@iconify/core/lib/icon/name'; +import { IconifyIconName, stringToIcon } from '@iconify/core/lib/icon/name'; import type { IconifyIconSize, IconifyHorizontalIconAlignment, @@ -310,10 +310,13 @@ interface InternalIconProps extends IconProps { _inline: boolean; } -type IconComponentData = Required | null; +interface IconComponentData { + data: Required; + classes?: string[]; +} interface IconComponentState { - data: IconComponentData; + icon: IconComponentData | null; } interface ComponentAbortData { @@ -332,7 +335,7 @@ class IconComponent extends React.Component< super(props); this.state = { // Render placeholder before component is mounted - data: null, + icon: null, }; } @@ -349,10 +352,10 @@ class IconComponent extends React.Component< /** * Update state */ - _setData(data: IconComponentData) { - if (this.state.data !== data) { + _setData(icon: IconComponentData | null) { + if (this.state.icon !== icon) { this.setState({ - data, + icon, }); } } @@ -374,22 +377,28 @@ class IconComponent extends React.Component< this._icon = ''; this._abortLoading(); - if (changed || state.data === null) { + if (changed || state.icon === null) { // Set data if it was changed - this._setData(fullIcon(icon)); + this._setData({ + data: fullIcon(icon), + }); } return; } // Invalid icon? - if (typeof icon !== 'string') { + let iconName: IconifyIconName | null; + if ( + typeof icon !== 'string' || + (iconName = stringToIcon(icon, false, true)) === null + ) { this._abortLoading(); this._setData(null); return; } // Load icon - const data = getIconData(icon); + const data = getIconData(iconName); if (data === null) { // Icon needs to be loaded if (!this._loading || this._loading.name !== icon) { @@ -400,7 +409,7 @@ class IconComponent extends React.Component< this._loading = { name: icon, abort: API.loadIcons( - [icon], + [iconName], this._checkIcon.bind(this, false) ), }; @@ -409,11 +418,25 @@ class IconComponent extends React.Component< } // Icon data is available - if (this._icon !== icon || state.data === null) { + if (this._icon !== icon || state.icon === null) { // New icon or icon has been loaded this._abortLoading(); this._icon = icon; - this._setData(data); + + // Add classes + const classes: string[] = ['iconify']; + if (iconName.prefix !== '') { + classes.push('iconify--' + iconName.prefix); + } + if (iconName.provider !== '') { + classes.push('iconify--' + iconName.provider); + } + + // Set data + this._setData({ + data, + classes, + }); if (this.props.onLoad) { this.props.onLoad(icon); } @@ -448,17 +471,28 @@ class IconComponent extends React.Component< */ render() { const props = this.props; - const data = this.state.data; + const icon = this.state.icon; - if (data === null) { + if (icon === null) { // Render placeholder return props.children ? (props.children as JSX.Element) : React.createElement('span', {}); } + // Add classes + let newProps = props; + if (icon.classes) { + newProps = merge(props, { + className: + (typeof props.className === 'string' + ? props.className + ' ' + : '') + icon.classes.join(' '), + } as typeof props); + } + // Render icon - return render(data, props, props._inline, props._ref); + return render(icon.data, newProps, props._inline, props._ref); } } diff --git a/packages/react/tests/api/20-rendering-from-api.test.js b/packages/react/tests/api/20-rendering-from-api.test.js index 7daba26..78c92b4 100644 --- a/packages/react/tests/api/20-rendering-from-api.test.js +++ b/packages/react/tests/api/20-rendering-from-api.test.js @@ -15,6 +15,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'render-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -73,6 +74,7 @@ describe('Rendering icon', () => { 'height': '1em', 'preserveAspectRatio': 'xMidYMid meet', 'viewBox': '0 0 ' + iconData.width + ' ' + iconData.height, + className, }, children: null, }); @@ -88,6 +90,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'mock-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -137,6 +140,7 @@ describe('Rendering icon', () => { iconData.width + ' ' + iconData.height, + 'className': 'test ' + className, }, children: null, }); @@ -157,6 +161,7 @@ describe('Rendering icon', () => { const component = renderer.create( { expect(name).toEqual(iconName); expect(onLoadCalled).toEqual(false); diff --git a/packages/react/tests/api/30-changing-props.test.js b/packages/react/tests/api/30-changing-props.test.js index f742b48..0f970c9 100644 --- a/packages/react/tests/api/30-changing-props.test.js +++ b/packages/react/tests/api/30-changing-props.test.js @@ -23,6 +23,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = ''; // Name of icon from last onLoad call const onLoad = (name) => { @@ -91,6 +92,7 @@ describe('Rendering icon', () => { iconData.width + ' ' + iconData.height, + className, }, children: null, }); @@ -151,6 +153,7 @@ describe('Rendering icon', () => { iconData2.width + ' ' + iconData2.height, + className, }, children: null, }); @@ -190,6 +193,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let isSync = true; mockAPIData({ @@ -257,6 +261,7 @@ describe('Rendering icon', () => { iconData2.width + ' ' + iconData2.height, + className, }, children: null, }); @@ -292,6 +297,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'multiple-props'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; mockAPIData({ provider, @@ -336,6 +342,7 @@ describe('Rendering icon', () => { iconData.width + ' ' + iconData.height, + className, }, children: null, }); @@ -371,6 +378,7 @@ describe('Rendering icon', () => { iconData.width + ' ' + iconData.height, + className, }, children: null, }); diff --git a/packages/react/tests/api/30-ref.test.js b/packages/react/tests/api/30-ref.test.js index 81024f5..92c636a 100644 --- a/packages/react/tests/api/30-ref.test.js +++ b/packages/react/tests/api/30-ref.test.js @@ -5,8 +5,7 @@ import { mockAPIData } from '@iconify/core/lib/api/modules/mock'; import { provider, nextPrefix } from './load'; const iconData = { - body: - '', + body: '', width: 24, height: 24, }; @@ -16,6 +15,7 @@ describe('Testing references', () => { const prefix = nextPrefix(); const name = 'render-test'; const iconName = `@${provider}:${prefix}:${name}`; + mockAPIData({ provider, prefix, diff --git a/packages/svelte/src/Icon.svelte b/packages/svelte/src/Icon.svelte index 9d2e513..fcc9446 100644 --- a/packages/svelte/src/Icon.svelte +++ b/packages/svelte/src/Icon.svelte @@ -26,7 +26,12 @@ // Generate data $: { counter; - data = mounted ? generateIcon(checkIconState($$props.icon, state, loaded, $$props.onLoad), $$props) : null; + const iconData = checkIconState($$props.icon, state, loaded, $$props.onLoad); + data = mounted && iconData ? generateIcon(iconData.data, $$props) : null; + if (data && iconData.classes) { + // Add classes + data.attributes['class'] = (typeof $$props['class'] === 'string' ? $$props['class'] + ' ' : '') + iconData.classes.join(' '); + } } // Increase counter when loaded to force re-calculation of data diff --git a/packages/svelte/src/functions.ts b/packages/svelte/src/functions.ts index 007f27a..071d03d 100644 --- a/packages/svelte/src/functions.ts +++ b/packages/svelte/src/functions.ts @@ -1,7 +1,7 @@ import type { IconifyJSON } from '@iconify/types'; // Core -import type { IconifyIconName } from '@iconify/core/lib/icon/name'; +import { IconifyIconName, stringToIcon } from '@iconify/core/lib/icon/name'; import type { IconifyIconSize, IconifyHorizontalIconAlignment, @@ -326,6 +326,14 @@ type IconStateCallback = () => void; */ export type IconifyIconOnLoad = (name: string) => void; +/** + * checkIconState result + */ +export interface CheckIconStateResult { + data: IconComponentData; + classes?: string[]; +} + /** * Check if component needs to be updated */ @@ -334,7 +342,7 @@ export function checkIconState( state: IconState, callback: IconStateCallback, onload?: IconifyIconOnLoad -): IconComponentData { +): CheckIconStateResult | null { // Abort loading icon function abortLoading() { if (state.loading) { @@ -352,17 +360,21 @@ export function checkIconState( // Stop loading state.name = ''; abortLoading(); - return fullIcon(icon); + return { data: fullIcon(icon) }; } - // Invalid icon - if (typeof icon !== 'string') { + // Invalid icon? + let iconName: IconifyIconName | null; + if ( + typeof icon !== 'string' || + (iconName = stringToIcon(icon, false, true)) === null + ) { abortLoading(); return null; } // Load icon - const data = getIconData(icon); + const data = getIconData(iconName); if (data === null) { // Icon needs to be loaded if (!state.loading || state.loading.name !== icon) { @@ -371,7 +383,7 @@ export function checkIconState( state.name = ''; state.loading = { name: icon, - abort: API.loadIcons([icon], callback), + abort: API.loadIcons([iconName], callback), }; } return null; @@ -385,7 +397,17 @@ export function checkIconState( onload(icon); } } - return data; + + // Add classes + const classes: string[] = ['iconify']; + if (iconName.prefix !== '') { + classes.push('iconify--' + iconName.prefix); + } + if (iconName.provider !== '') { + classes.push('iconify--' + iconName.provider); + } + + return { data, classes }; } /** diff --git a/packages/svelte/tests/api/20-rendering-from-api.test.js b/packages/svelte/tests/api/20-rendering-from-api.test.js index 069d20e..92d59e0 100644 --- a/packages/svelte/tests/api/20-rendering-from-api.test.js +++ b/packages/svelte/tests/api/20-rendering-from-api.test.js @@ -4,8 +4,7 @@ import { mockAPIData } from '@iconify/core/lib/api/modules/mock'; import { provider, nextPrefix } from './load'; const iconData = { - body: - '', + body: '', width: 24, height: 24, }; @@ -15,6 +14,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'render-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -59,7 +59,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // Make sure onLoad has been called @@ -73,6 +75,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'mock-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -106,7 +109,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // onLoad should have been called @@ -124,6 +129,8 @@ describe('Rendering icon', () => { // Render component const component = render(Icon, { icon: iconName, + // Also testing simple class + class: 'test', onLoad: (name) => { expect(name).toEqual(iconName); expect(onLoadCalled).toEqual(false); diff --git a/packages/svelte/tests/api/30-changing-props.test.js b/packages/svelte/tests/api/30-changing-props.test.js index 258f5c3..41bcb75 100644 --- a/packages/svelte/tests/api/30-changing-props.test.js +++ b/packages/svelte/tests/api/30-changing-props.test.js @@ -6,15 +6,13 @@ import ChangeIcon from './fixtures/ChangeIcon.svelte'; import ChangeProps from './fixtures/ChangeProps.svelte'; const iconData = { - body: - '', + body: '', width: 24, height: 24, }; const iconData2 = { - body: - '', + body: '', width: 32, height: 32, }; @@ -26,6 +24,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = ''; // Name of icon from last onLoad call let triggerSwap; @@ -63,7 +62,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // onLoad should have been called @@ -107,7 +108,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // onLoad should have been called for second icon @@ -163,6 +166,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = ''; // Name of icon from last onLoad call let isSync = true; let triggerSwap; @@ -219,7 +223,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // onLoad should have been called for second icon @@ -269,6 +275,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'multiple-props'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; let triggerSwap; @@ -303,7 +310,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // onLoad should have been called @@ -316,13 +325,14 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { // Check HTML again - const node = component.container.querySelector( - 'svg' - ); + const node = + component.container.querySelector('svg'); const html = node.parentNode.innerHTML; expect(html).toEqual( - '' + '' ); done(); diff --git a/packages/vue/src/iconify.ts b/packages/vue/src/iconify.ts index 0c9a9ca..1cf70df 100644 --- a/packages/vue/src/iconify.ts +++ b/packages/vue/src/iconify.ts @@ -11,7 +11,7 @@ import { import { IconifyJSON } from '@iconify/types'; // Core -import { IconifyIconName } from '@iconify/core/lib/icon/name'; +import { IconifyIconName, stringToIcon } from '@iconify/core/lib/icon/name'; import { IconifyIconSize, IconifyHorizontalIconAlignment, @@ -27,8 +27,9 @@ import { IconifyBuilderFunctions, builderFunctions, } from '@iconify/core/lib/builder/functions'; -import type { IconifyIconBuildResult } from '@iconify/core/lib/builder'; +import { IconifyIconBuildResult } from '@iconify/core/lib/builder'; import { fullIcon, IconifyIcon } from '@iconify/core/lib/icon'; +import { merge } from '@iconify/core/lib/misc/merge'; // Modules import { coreModules } from '@iconify/core/lib/modules'; @@ -312,6 +313,11 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { /** * Component */ +interface IconComponentData { + data: Required; + classes?: string[]; +} + export const Icon = defineComponent({ // Do not inherit other attributes: it is handled by render() inheritAttrs: false, @@ -350,7 +356,10 @@ export const Icon = defineComponent({ } }, // Get data for icon to render or null - getIcon(icon: IconifyIcon | string, onload?: IconifyIconOnLoad) { + getIcon( + icon: IconifyIcon | string, + onload?: IconifyIconOnLoad + ): IconComponentData | null { // Icon is an object if ( typeof icon === 'object' && @@ -360,17 +369,23 @@ export const Icon = defineComponent({ // Stop loading this._name = ''; this.abortLoading(); - return fullIcon(icon); + return { + data: fullIcon(icon), + }; } // Invalid icon? - if (typeof icon !== 'string') { + let iconName: IconifyIconName | null; + if ( + typeof icon !== 'string' || + (iconName = stringToIcon(icon, false, true)) === null + ) { this.abortLoading(); return null; } // Load icon - const data = getIconData(icon); + const data = getIconData(iconName); if (data === null) { // Icon needs to be loaded if (!this._loadingIcon || this._loadingIcon.name !== icon) { @@ -379,7 +394,7 @@ export const Icon = defineComponent({ this._name = ''; this._loadingIcon = { name: icon, - abort: API.loadIcons([icon], () => { + abort: API.loadIcons([iconName], () => { this.counter++; }), }; @@ -395,7 +410,17 @@ export const Icon = defineComponent({ onload(icon); } } - return data; + + // Add classes + const classes: string[] = ['iconify']; + if (iconName.prefix !== '') { + classes.push('iconify--' + iconName.prefix); + } + if (iconName.provider !== '') { + classes.push('iconify--' + iconName.provider); + } + + return { data, classes }; }, }, @@ -410,14 +435,28 @@ export const Icon = defineComponent({ // Get icon data const props = this.$attrs; - const icon = this.getIcon(props.icon, props.onLoad); + const icon: IconComponentData | null = this.getIcon( + props.icon, + props.onLoad + ); // Validate icon object if (!icon) { return this.$slots.default ? this.$slots.default() : null; } - // Valid icon: render it - return render(icon, props); + // Add classes + let newProps = props; + if (icon.classes) { + newProps = merge(props, { + class: + (typeof props['class'] === 'string' + ? props['class'] + ' ' + : '') + icon.classes.join(' '), + }); + } + + // Render icon + return render(icon.data, newProps); }, }); diff --git a/packages/vue/tests/api/20-rendering-from-api.test.js b/packages/vue/tests/api/20-rendering-from-api.test.js index 0639a52..896d4bd 100644 --- a/packages/vue/tests/api/20-rendering-from-api.test.js +++ b/packages/vue/tests/api/20-rendering-from-api.test.js @@ -15,6 +15,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'render-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -48,7 +49,8 @@ describe('Rendering icon', () => { // Render component const Wrapper = { components: { Icon }, - template: ``, + // Also test class string + template: ``, methods: { onLoad(name) { expect(name).toEqual(iconName); @@ -62,7 +64,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // Make sure onLoad has been called @@ -76,6 +80,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'mock-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -106,7 +111,9 @@ describe('Rendering icon', () => { setTimeout(() => { // Check HTML expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // onLoad should have been called @@ -124,7 +131,7 @@ describe('Rendering icon', () => { // Render component const Wrapper = { components: { Icon }, - template: ``, + template: ``, methods: { onLoad(name) { expect(name).toEqual(iconName); @@ -132,6 +139,15 @@ describe('Rendering icon', () => { onLoadCalled = true; }, }, + data() { + // Test dynamic class + return { + testClass: { + foo: true, + bar: false, + }, + }; + }, }; const wrapper = mount(Wrapper, {}); diff --git a/packages/vue/tests/api/30-changing-props.test.js b/packages/vue/tests/api/30-changing-props.test.js index 061310d..8605ef3 100644 --- a/packages/vue/tests/api/30-changing-props.test.js +++ b/packages/vue/tests/api/30-changing-props.test.js @@ -24,6 +24,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = ''; // Name of icon from last onLoad call const onLoad = name => { @@ -72,7 +73,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // onLoad should have been called @@ -113,7 +116,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // onLoad should have been called for second icon @@ -151,6 +156,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let isSync = true; mockAPIData({ @@ -198,7 +204,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); done(); @@ -234,6 +242,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'multiple-props'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; mockAPIData({ provider, @@ -259,7 +268,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // Add horizontal flip and style @@ -276,7 +287,9 @@ describe('Rendering icon', () => { expect( wrapper.html().replace(/\s*\n\s*/g, '') ).toEqual( - '' + '' ); done(); diff --git a/packages/vue2/src/iconify.ts b/packages/vue2/src/iconify.ts index 5f0b968..75a77a9 100644 --- a/packages/vue2/src/iconify.ts +++ b/packages/vue2/src/iconify.ts @@ -3,7 +3,7 @@ import { ExtendedVue } from 'vue/types/vue'; import { IconifyJSON } from '@iconify/types'; // Core -import { IconifyIconName } from '@iconify/core/lib/icon/name'; +import { IconifyIconName, stringToIcon } from '@iconify/core/lib/icon/name'; import { IconifyIconSize, IconifyHorizontalIconAlignment, @@ -19,8 +19,9 @@ import { IconifyBuilderFunctions, builderFunctions, } from '@iconify/core/lib/builder/functions'; -import type { IconifyIconBuildResult } from '@iconify/core/lib/builder'; +import { IconifyIconBuildResult } from '@iconify/core/lib/builder'; import { fullIcon, IconifyIcon } from '@iconify/core/lib/icon'; +import { merge } from '@iconify/core/lib/misc/merge'; // Modules import { coreModules } from '@iconify/core/lib/modules'; @@ -304,6 +305,11 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { /** * Component */ +interface IconComponentData { + data: Required; + classes?: string[]; +} + export const Icon = Vue.extend({ // Do not inherit other attributes: it is handled by render() // In Vue 2 style is still passed! @@ -340,7 +346,10 @@ export const Icon = Vue.extend({ } }, // Get data for icon to render or null - getIcon(icon: IconifyIcon | string, onload?: IconifyIconOnLoad) { + getIcon( + icon: IconifyIcon | string, + onload?: IconifyIconOnLoad + ): IconComponentData | null { // Icon is an object if ( typeof icon === 'object' && @@ -350,17 +359,23 @@ export const Icon = Vue.extend({ // Stop loading this._name = ''; this.abortLoading(); - return fullIcon(icon); + return { + data: fullIcon(icon), + }; } // Invalid icon? - if (typeof icon !== 'string') { + let iconName: IconifyIconName | null; + if ( + typeof icon !== 'string' || + (iconName = stringToIcon(icon, false, true)) === null + ) { this.abortLoading(); return null; } // Load icon - const data = getIconData(icon); + const data = getIconData(iconName); if (data === null) { // Icon needs to be loaded if (!this._loadingIcon || this._loadingIcon.name !== icon) { @@ -369,7 +384,7 @@ export const Icon = Vue.extend({ this._name = ''; this._loadingIcon = { name: icon, - abort: API.loadIcons([icon], () => { + abort: API.loadIcons([iconName], () => { this.$forceUpdate(); }), }; @@ -385,7 +400,17 @@ export const Icon = Vue.extend({ onload(icon); } } - return data; + + // Add classes + const classes: string[] = ['iconify']; + if (iconName.prefix !== '') { + classes.push('iconify--' + iconName.prefix); + } + if (iconName.provider !== '') { + classes.push('iconify--' + iconName.provider); + } + + return { data, classes }; }, }, @@ -410,7 +435,10 @@ export const Icon = Vue.extend({ // Get icon data const props = this.$attrs; - const icon = this.getIcon(props.icon, props.onLoad); + const icon: IconComponentData | null = this.getIcon( + props.icon, + props.onLoad + ); // Validate icon object if (!icon) { @@ -418,7 +446,18 @@ export const Icon = Vue.extend({ return placeholder(this.$slots); } - // Valid icon: render it - return render(createElement, props, this.$data, icon); + // Add classes + let context = this.$data; + if (icon.classes) { + context = merge(context, { + class: + (typeof context['class'] === 'string' + ? context['class'] + ' ' + : '') + icon.classes.join(' '), + }); + } + + // Render icon + return render(createElement, props, context, icon.data); }, }); diff --git a/packages/vue2/tests/api/20-rendering-from-api.test.js b/packages/vue2/tests/api/20-rendering-from-api.test.js index 08287fe..308e1bd 100644 --- a/packages/vue2/tests/api/20-rendering-from-api.test.js +++ b/packages/vue2/tests/api/20-rendering-from-api.test.js @@ -14,6 +14,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'render-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -47,7 +48,8 @@ describe('Rendering icon', () => { // Render component const Wrapper = { components: { Icon }, - template: ``, + // Also test class string + template: ``, methods: { onLoad(name) { expect(name).toEqual(iconName); @@ -61,7 +63,9 @@ describe('Rendering icon', () => { // Check HTML expect(html).toEqual( - '' + '' ); // Make sure onLoad has been called @@ -75,6 +79,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'mock-test'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = false; mockAPIData({ @@ -105,7 +110,10 @@ describe('Rendering icon', () => { setTimeout(() => { // Check HTML expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // onLoad should have been called @@ -123,7 +131,7 @@ describe('Rendering icon', () => { // Render component const Wrapper = { components: { Icon }, - template: ``, + template: ``, methods: { onLoad(name) { expect(name).toEqual(iconName); @@ -131,6 +139,15 @@ describe('Rendering icon', () => { onLoadCalled = true; }, }, + data() { + // Test dynamic class + return { + testClass: { + foo: true, + bar: false, + }, + }; + }, }; const wrapper = mount(Wrapper, {}); @@ -177,7 +194,7 @@ describe('Rendering icon', () => { // Render component const Wrapper = { components: { Icon }, - template: ``, + template: ``, methods: { onLoad() { throw new Error('onLoad called for empty icon!'); diff --git a/packages/vue2/tests/api/30-changing-props.test.js b/packages/vue2/tests/api/30-changing-props.test.js index 8b6b8e8..cc98020 100644 --- a/packages/vue2/tests/api/30-changing-props.test.js +++ b/packages/vue2/tests/api/30-changing-props.test.js @@ -22,6 +22,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let onLoadCalled = ''; // Name of icon from last onLoad call const onLoad = (name) => { @@ -70,7 +71,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // onLoad should have been called @@ -111,7 +114,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // onLoad should have been called for second icon @@ -147,6 +152,7 @@ describe('Rendering icon', () => { const name2 = 'changing-prop2'; const iconName = `@${provider}:${prefix}:${name}`; const iconName2 = `@${provider}:${prefix}:${name2}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; let isSync = true; mockAPIData({ @@ -194,7 +200,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); done(); @@ -229,6 +237,7 @@ describe('Rendering icon', () => { const prefix = nextPrefix(); const name = 'multiple-props'; const iconName = `@${provider}:${prefix}:${name}`; + const className = `iconify iconify--${prefix} iconify--${provider}`; mockAPIData({ provider, @@ -254,7 +263,9 @@ describe('Rendering icon', () => { setTimeout(() => { setTimeout(() => { expect(wrapper.html().replace(/\s*\n\s*/g, '')).toEqual( - '' + '' ); // Add horizontal flip and style @@ -271,7 +282,9 @@ describe('Rendering icon', () => { expect( wrapper.html().replace(/\s*\n\s*/g, '') ).toEqual( - '' + '' ); done();