Skip to content
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

Merged
merged 7 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions projects/igniteui-angular-elements/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,22 @@ import { registerConfig } from "../analyzer/elements.config";
import { createIgxCustomElement } from './create-custom-element';
import { IgxGridStateComponent } from '../lib/state.component';
import { ELEMENTS_TOKEN } from 'igniteui-angular/src/lib/core/utils';
import { IgxIconBroadcastService } from '../lib/icon.broadcast.service';

@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule
],
providers: [
{ provide: ELEMENTS_TOKEN, useValue: true }
{ provide: ELEMENTS_TOKEN, useValue: true },
IgxIconBroadcastService
],
// bootstrap: []
})
export class AppModule {

constructor(private injector: Injector) {}
constructor(private injector: Injector, private _iconBroadcast: IgxIconBroadcastService) {}
Copy link
Member

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.


ngDoBootstrap() {

Expand Down
31 changes: 30 additions & 1 deletion projects/igniteui-angular-elements/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ <h3 class="ig-typography__h6">Flat Grid (column groups, toolbar, paginator, row
<igc-toggle-button value="medium">Medium</igc-toggle-button>
<igc-toggle-button value="large" selected>Large</igc-toggle-button>
</igc-button-group>
<button id="toggleIcon">Change Filter Icon</button>
</div>

<h3 class="ig-typography__h6">Standalone paginator</h3>
Expand Down Expand Up @@ -112,10 +113,18 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>
import { html, nothing } from "/lit-html.js";
import { Directive, directive } from "/directive.js";

import { defineComponents, IgcSelectComponent, IgcComboComponent, IgcButtonComponent, IgcButtonGroupComponent } from "igniteui-webcomponents";
import { defineComponents, IgcSelectComponent, IgcComboComponent, IgcButtonComponent, IgcButtonGroupComponent, registerIconFromText, setIconRef } from "igniteui-webcomponents";
// import "igniteui-webcomponents/themes/light/bootstrap.css";

defineComponents(IgcSelectComponent, IgcComboComponent, IgcButtonComponent, IgcButtonGroupComponent);


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>';
const thumbUpIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z"/></svg>';
let icon = thumbUpIcon;

const grid1 = document.getElementById('grid1');
const grid2 = document.getElementById('grid2');
const buttonGroup = document.querySelector('igc-button-group');
Expand Down Expand Up @@ -196,6 +205,7 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>

document.getElementById("saveState").addEventListener("click", saveState);
document.getElementById("restoreState").addEventListener("click", restoreState);
document.getElementById("toggleIcon").addEventListener("click", toggleIcon);
const stateComponent = document.getElementById('state');
stateComponent.options = {
paging: false
Expand All @@ -220,6 +230,25 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>
stateComponent.applyState(obj);
}
}

function toggleIcon() {
if (icon !== thumbUpIcon) {
icon = thumbUpIcon;
registerIconFromText("filter_list", thumbUpIcon, "customSet");
setIconRef('filter_list', 'default', {
name: 'filter_list',
collection: 'customSet',
});

} else {
icon = buildIcon;
registerIconFromText("filter_list", buildIcon, "customSet2");
setIconRef('filter_list', 'default', {
name: 'filter_list',
collection: 'customSet2',
});
}
}
</script>

<!-- IgxTreeGridComponent -->
Expand Down
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);
Copy link
Member

Choose a reason for hiding this comment

The 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 UpdateIconReference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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".
If the other side sends the entire state of the collections, every API for change on that side will also cause the Angular side to re-register all previous icons for example, which is at least redundant if it doesn't affect performance much.

Copy link
Member

Choose a reason for hiding this comment

The 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
});
}
}
}

}
Loading