Skip to content

Commit

Permalink
feat(angular/modal): support modal content by component class (#256)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielleroux authored Dec 19, 2022
1 parent f3b5372 commit d7479d9
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 17 deletions.
5 changes: 0 additions & 5 deletions packages/angular-test-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,6 @@ import WorkflowVertical from 'src/preview-examples/workflow-vertical';
import { NavigationTestComponent } from './components/navigation-test.component';

const routes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: 'preview/buttons',
},
{
path: 'testing',
children: [
Expand Down
1 change: 1 addition & 0 deletions packages/angular/src/modal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
* LICENSE file in the root directory of this source tree.
*/

export { IxActiveModal } from './modal-ref';
export * from './modal.service';
46 changes: 46 additions & 0 deletions packages/angular/src/modal/modal-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2022 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { closeModal, dismissModal } from '@siemens/ix';

export class IxActiveModal<TData = any, TReason = any> {
modalElement: HTMLElement;

constructor(private readonly modalData?: TData) {}

public get data() {
return this.modalData;
}

/**
* Close the active modal
*
* @param reason
*/
public close(reason: TReason) {
closeModal(this.modalElement, reason);
}

/**
* Dismiss the active modal
*
* @param reason
*/
public dismiss(reason?: TReason) {
dismissModal(this.modalElement, reason);
}
}

export class InternalIxActiveModal<
TData = any,
TReason = any
> extends IxActiveModal<TData, TReason> {
setModalElement(element: HTMLElement) {
this.modalElement = element;
}
}
3 changes: 1 addition & 2 deletions packages/angular/src/modal/modal.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
* LICENSE file in the root directory of this source tree.
*/

import { TemplateRef } from '@angular/core';
import { ModalConfig as IxModalConfig } from '@siemens/ix';

export type ModalConfig<TDATA = any> = Omit<IxModalConfig, 'content'> & {
content: TemplateRef<any>;
content: any;
data?: TDATA;
};
51 changes: 49 additions & 2 deletions packages/angular/src/modal/modal.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { expect } from '@jest/globals';
import { IxActiveModal } from './modal-ref';
import { ModalService } from './modal.service';

jest.mock('@siemens/ix', () => ({
Expand All @@ -18,16 +19,17 @@ jest.mock('@siemens/ix', () => ({
onDismiss: {
once: jest.fn(),
},
htmlElement: { type: 'html-element' },
})
),
}));

test('test', () => {
test('should create modal by templateRef', () => {
const createEmbeddedViewMock = jest.fn((_: { $implicit: any }) => ({
rootNodes: [{}],
detectChanges: jest.fn(),
}));
const modalService = new ModalService();
const modalService = new ModalService({} as any, {} as any, {} as any);
modalService.open({
content: {
createEmbeddedView: createEmbeddedViewMock,
Expand All @@ -46,3 +48,48 @@ test('test', () => {
},
});
});

test('should create modal by component typ', async () => {
const appRefMock = {
attachView: jest.fn(),
};
const factory = {
create: jest.fn(() => ({
hostView: {
rootNodes: [jest.fn()],
detectChanges: jest.fn(),
},
})),
};
const componentFactoryMock = {
resolveComponentFactory: jest.fn(() => factory),
};
const injectorMock = jest.fn();

const modalService = new ModalService(
appRefMock as any,
componentFactoryMock as any,
injectorMock as any
);

class TestComponent {
foo = 'bar';
}

await modalService.open({
content: TestComponent,
title: '',
data: {
foo: 'bar',
},
});

const [[{ records }]] = factory.create.mock.calls as any;

const injectorRecords = records as Map<Function, any>;

expect(injectorRecords.get(IxActiveModal).value).toEqual({
modalData: { foo: 'bar' },
modalElement: { type: 'html-element' },
});
});
100 changes: 92 additions & 8 deletions packages/angular/src/modal/modal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,115 @@
* LICENSE file in the root directory of this source tree.
*/

import { Injectable } from '@angular/core';
import {
ApplicationRef,
ComponentFactoryResolver,
EmbeddedViewRef,
Injectable,
Injector,
Type,
} from '@angular/core';
import { closeModal, dismissModal, modal } from '@siemens/ix';
import { InternalIxActiveModal, IxActiveModal } from './modal-ref';
import { ModalConfig } from './modal.config';

type ModalContext<T> = {
close: ((result: any) => void) | null;
dismiss: ((result?: any) => void) | null;
data?: T;
};

@Injectable({
providedIn: 'root',
})
export class ModalService {
constructor() {}
constructor(
private appRef: ApplicationRef,
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {}

async open<TData = any, TReason = any>(config: ModalConfig<TData>) {
const context: {
close: ((result: any) => void) | null;
dismiss: ((result?: any) => void) | null;
data?: TData;
} = {
const context: ModalContext<TData> = {
close: null,
dismiss: null,
data: config.data,
};

if (config.content instanceof Type) {
return this.createContentByComponentType<TData, TReason>(config, context);
}

const modalInstance = await this.createContentByTemplateRef<TData, TReason>(
config,
context
);

return modalInstance;
}

private async createContentByComponentType<TData = any, TReason = any>(
config: ModalConfig<TData>,
context: ModalContext<TData>
) {
const activeModal = new InternalIxActiveModal<TData>(context.data);

const modalFactory = this.componentFactoryResolver.resolveComponentFactory(
config.content
);

const modalInjector = Injector.create({
providers: [
{
provide: IxActiveModal,
useValue: activeModal,
},
],
parent: this.injector,
});

const instance = modalFactory.create(modalInjector);
this.appRef.attachView(instance.hostView);

const embeddedView = instance.hostView as EmbeddedViewRef<any>;
const modalInstance = await this.createModalInstance<TData, TReason>(
context,
embeddedView,
config
);

activeModal.setModalElement(modalInstance.htmlElement);

return modalInstance;
}

private async createContentByTemplateRef<TData = any, TReason = any>(
config: ModalConfig<TData>,
context: {
close: ((result: any) => void) | null;
dismiss: ((result?: any) => void) | null;
data?: TData | undefined;
}
) {
const embeddedView = config.content.createEmbeddedView({
$implicit: context,
});
return await this.createModalInstance<TData, TReason>(
context,
embeddedView,
config
);
}

private async createModalInstance<TData = any, TReason = any>(
context: {
close: ((result: any) => void) | null;
dismiss: ((result?: any) => void) | null;
data?: TData | undefined;
},
embeddedView: EmbeddedViewRef<any>,
config: ModalConfig<TData>
) {
const node = embeddedView.rootNodes[0];

context.close = (result: any) => {
Expand All @@ -56,7 +141,6 @@ export class ModalService {
modalInstance.onDismiss.once(() => {
embeddedView.destroy();
});

return modalInstance;
}
}

0 comments on commit d7479d9

Please sign in to comment.