-
Notifications
You must be signed in to change notification settings - Fork 161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(elements): add broadcast channel to sync icons with wc #14498
Changes from all commits
6970a79
9734b51
3c94cb8
3fbdfbb
cf75616
54bd13e
c377a33
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
import { ActionType, BroadcastIconsChangeMessage, IgxIconBroadcastService, SvgIcon, } from './icon.broadcast.service'; | ||
import { Component, SecurityContext } from '@angular/core'; | ||
import { IconMeta, IgxIconService } from 'igniteui-angular'; | ||
import { wait } from 'igniteui-angular/src/lib/test-utils/ui-interactions.spec'; | ||
|
||
describe('Icon broadcast service', () => { | ||
let fixture: ComponentFixture<BroadcastServiceComponent>; | ||
let broadcastChannel: BroadcastChannel; | ||
let events: BroadcastIconsChangeMessage[] = []; | ||
const buildIcon = | ||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/></svg>'; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [], | ||
providers: [IgxIconBroadcastService] | ||
}) | ||
.compileComponents(); | ||
}); | ||
|
||
beforeEach(() => { | ||
broadcastChannel = new BroadcastChannel("ignite-ui-icon-channel"); | ||
broadcastChannel.onmessage = (e: MessageEvent<BroadcastIconsChangeMessage>) => { | ||
events.push(e.data); | ||
} | ||
fixture = TestBed.createComponent(BroadcastServiceComponent); | ||
}); | ||
|
||
afterEach(() => { | ||
events = []; | ||
broadcastChannel.close(); | ||
}); | ||
|
||
describe('Broadcast Events', () => { | ||
it('should correctly process event of icons registering on channel.', async() => { | ||
// simulate a new icon being registered on channel | ||
const icons: Map<string, Map<string, SvgIcon>> = new Map(); | ||
const icon: Map<string, SvgIcon> = new Map() | ||
icon.set("customIcon", { svg: buildIcon }); | ||
icons.set("customCollection", icon); | ||
const message: BroadcastIconsChangeMessage = { | ||
actionType: ActionType.RegisterIcon, | ||
collections: icons | ||
}; | ||
broadcastChannel.postMessage(message); | ||
fixture.detectChanges(); | ||
await wait(50); | ||
fixture.detectChanges(); | ||
const iconService = fixture.componentInstance.iconService; | ||
const svg = iconService.getSvgIcon("customIcon", "customCollection"); | ||
expect(svg).not.toBeUndefined(); | ||
}); | ||
|
||
it('should correctly process event of setting an icon reference on channel.', async() => { | ||
const refs: Map<string, Map<string, IconMeta>> = new Map(); | ||
const ref: Map<string, IconMeta> = new Map() | ||
ref.set("customIcon", {name: "customIcon", family: "customCollection" }); | ||
refs.set("customCollection", ref); | ||
const message: BroadcastIconsChangeMessage = { | ||
actionType: ActionType.RegisterIcon, | ||
references: refs | ||
}; | ||
broadcastChannel.postMessage(message); | ||
fixture.detectChanges(); | ||
await wait(50); | ||
fixture.detectChanges(); | ||
|
||
const iconService = fixture.componentInstance.iconService; | ||
const serviceRef = iconService.getIconRef("customIcon", "customCollection"); | ||
expect(serviceRef.family).toBe("customCollection"); | ||
expect(serviceRef.name).toBe("customIcon"); | ||
}); | ||
|
||
it('should send a request to sync state from any peer already on the channel on init.', async() => { | ||
await wait(50); | ||
expect(events.length).toBe(1); | ||
expect(events[0].actionType).toBe(ActionType.SyncState); | ||
}); | ||
|
||
it('should correctly process event of synching full state of icons on channel.', async() => { | ||
const icons: Map<string, Map<string, SvgIcon>> = new Map(); | ||
const icon: Map<string, SvgIcon> = new Map() | ||
icon.set("customIcon", { svg: buildIcon }); | ||
icons.set("customCollection", icon); | ||
const refs: Map<string, Map<string, IconMeta>> = new Map(); | ||
const ref: Map<string, IconMeta> = new Map() | ||
ref.set("customIcon", {name: "customIcon", family: "customCollection" }); | ||
refs.set("customCollection", ref); | ||
const message: BroadcastIconsChangeMessage = { | ||
actionType: ActionType.SyncState, | ||
collections: icons, | ||
references: refs | ||
}; | ||
broadcastChannel.postMessage(message); | ||
await wait(50); | ||
const iconService = fixture.componentInstance.iconService; | ||
const svg = iconService.getSvgIcon("customIcon", "customCollection"); | ||
expect(svg).not.toBeUndefined(); | ||
const serviceRef = iconService.getIconRef("customIcon", "customCollection"); | ||
expect(serviceRef.family).toBe("customCollection"); | ||
expect(serviceRef.name).toBe("customIcon"); | ||
}); | ||
}) | ||
}); | ||
|
||
@Component({ | ||
template: ` | ||
`, | ||
standalone: true, | ||
providers: [IgxIconBroadcastService, IgxIconService] | ||
}) | ||
export class BroadcastServiceComponent { | ||
constructor(public iconBroadcast: IgxIconBroadcastService, public iconService: IgxIconService) {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { Injectable, Optional } from '@angular/core'; | ||
import { PlatformUtil } from '../../../igniteui-angular/src/lib/core/utils'; | ||
import { IconMeta, IgxIconService } from '../../../igniteui-angular/src/lib/icon/icon.service'; | ||
|
||
|
||
export interface SvgIcon { | ||
svg: string; | ||
title?: string; | ||
} | ||
|
||
export type Collection<T, U> = Map<T, U>; | ||
|
||
export enum ActionType { | ||
SyncState = 0, | ||
RegisterIcon = 1, | ||
UpdateIconReference = 2, | ||
} | ||
|
||
export interface BroadcastIconsChangeMessage { | ||
actionType: ActionType; | ||
collections?: Collection<string, Map<string, SvgIcon>>; | ||
references?: Collection<string, Map<string, IconMeta>>; | ||
} | ||
|
||
/** @hidden @internal **/ | ||
@Injectable() | ||
export class IgxIconBroadcastService { | ||
private iconBroadcastChannel: BroadcastChannel; | ||
constructor( | ||
protected _iconService: IgxIconService, | ||
@Optional() private _platformUtil: PlatformUtil | ||
) { | ||
if (this._platformUtil?.isBrowser) { | ||
// open broadcast channel for sync with wc icon service. | ||
this.iconBroadcastChannel = new BroadcastChannel("ignite-ui-icon-channel"); | ||
this.iconBroadcastChannel.onmessage = (event) => { | ||
const message = event.data as BroadcastIconsChangeMessage; | ||
if (message.actionType === ActionType.SyncState || | ||
message.actionType === ActionType.RegisterIcon) { | ||
this.updateIconsFromCollection(message.collections); | ||
} | ||
|
||
if (message.actionType === ActionType.SyncState || | ||
message.actionType === ActionType.UpdateIconReference) { | ||
this.updateRefsFromCollection(message.references); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming it's the responsibility of the other side to only emit the icons that do need changing for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. The other side sends the state of the collections and this side updates its own state. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hehe, then it's not a "yes" answer :) The question was if "the other side to only emits the icons that do need changing". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nvm, checked the implementation, the other side creates a fresh collection for each change, so it is sending just the modified refs. No need to break-check me like that 😄 |
||
} | ||
}; | ||
// send message to sync state | ||
this.iconBroadcastChannel.postMessage({ | ||
actionType: ActionType.SyncState | ||
}); | ||
} | ||
} | ||
|
||
private updateIconsFromCollection(collections: Collection<string, Map<string, SvgIcon>>) { | ||
if (!collections) return; | ||
const collectionKeys = collections.keys(); | ||
for (const collectionKey of collectionKeys) { | ||
const collection = collections.get(collectionKey); | ||
for (const iconKey of collection.keys()) { | ||
const value = collection.get(iconKey).svg; | ||
this._iconService.addSvgIconFromText(iconKey, value, collectionKey); | ||
} | ||
} | ||
} | ||
|
||
private updateRefsFromCollection(collections: Collection<string, Map<string, any>>) { | ||
if (!collections) return; | ||
const collectionKeys = collections.keys(); | ||
for (const collectionKey of collectionKeys) { | ||
const collection = collections.get(collectionKey); | ||
for (const iconKey of collection.keys()) { | ||
const collectionName = collection.get(iconKey).collection; | ||
this._iconService.setIconRef(iconKey, 'default', { | ||
family: collectionName, | ||
name: iconKey | ||
}); | ||
} | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, kind of better we're keeping this in Elements for now.
A bit awkward in terms of usability as it stands having to inject just to trigger the functionality. So it either doesn't need to go through the DI system or perhaps we should consider some API on it like
start
&stop
basically - both to make sense as usage and to have a cleanup mechanism.Alternatively, since this isn't root-provided it should be able to take advantage of
ngOnDestroy
for cleanup.Both of those things technically don't matter much for Elements, since this module will never destroy until the entre app does, but still should be noted.