Skip to content

Commit

Permalink
feat: Modal confirmation service
Browse files Browse the repository at this point in the history
  • Loading branch information
unlight committed Aug 28, 2019
1 parent dee7699 commit 57f89e6
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 82 deletions.
14 changes: 14 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ module.exports = {
// unicorn
"unicorn/import-index": 0,
"unicorn/catch-error-name": 0,
"unicorn/prevent-abbreviations": [1,
{
"replacements": {
"err": {
"error": false,
},
"args": {
"arguments": false,
},
"componentRef": false,
"viewContainerReference": false,
}
}
],
// import
"import/newline-after-import": 0,
"import/no-duplicates": 1,
Expand Down
1 change: 0 additions & 1 deletion Taskfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ dev2() {

build() {
ng-packagr
mv src/*.metadata.json dist
cp src/ngx-modal.css dist
}

Expand Down
31 changes: 28 additions & 3 deletions example/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, ViewChild } from '@angular/core';
import { ModalConfirmComponent } from '../src/index';
import { ModalConfirm2Component } from '../src/index';
import { Component, ViewChild, ComponentFactoryResolver, TemplateRef, Directive, ElementRef, ViewChildren, ViewContainerRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { ModalConfirmService, ModalConfirmComponent, ModalConfirm2Component, ModalConfirm3Component } from '../src';
import { take } from 'rxjs/operators';

require('../src/ngx-modal.css');

Expand All @@ -12,8 +12,13 @@ require('../src/ngx-modal.css');
<a [routerLink]="[{ outlets: { 'modal': 'x'} }]">Modal 1</a> <br/>
<a href="javascript:;" (click)="toggleScroll($event)">toggle scroll</a><br/>
<a [routerLink]="[{ outlets: { 'modal': 'data-routed'} }]">Modal (data routed)</a> <br/>
<a (click)="openConfirm()">Confirm</a> <br/>
<a (click)="openConfirm2()">Confirm2</a> <br/>
modalConfirmService: <button (click)="openModalConfirmService($event, 1)">Open 1</button> <br/>
modalConfirmService: <button (click)="openModalConfirmService($event, 2)">Open 2</button> <br/>
modalConfirmService: <button (click)="openModalConfirmService($event, 3)">Open 3</button> <br/>
<a routerLink="/feature">Go to Feature Module (lazy loaded)</a> <br/>
<a routerLink="/feature/123">parameter component</a> <br/>
<a routerLink="/custom_modal">custom_modal</a> <br/>
Expand Down Expand Up @@ -58,6 +63,25 @@ export class AppComponent {
confirm2Subscription: Subscription;
styleWidth: string | null;

constructor(
private modalConfirmService: ModalConfirmService,
private viewContainerRef: ViewContainerRef,
) { }

openModalConfirmService(event: Event, type: number) {
const [componentType] = [
[ModalConfirmComponent, 1],
[ModalConfirm2Component, 2],
[ModalConfirm3Component, 3],
].find(([, num]) => type === num)!;

this.modalConfirmService.open(this.viewContainerRef, <any>componentType)
.pipe(take(1))
.subscribe(result => {
console.log('confirm result', result);
});
}

openConfirm() {
this.confirm.open();
this.confirmSubscription = this.confirm.okay.subscribe(() => {
Expand Down Expand Up @@ -89,4 +113,5 @@ export class AppComponent {
this.styleWidth = '100px';
}
}

}
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Open modal window for your Angular X application",
"version": "0.0.0-dev",
"scripts": {
"dev": "webpack-dev-server --hot --progress --colors",
"dev": "webpack-dev-server --progress --colors",
"dev2": "sh Taskfile dev2",
"karma": "karma start",
"test:r": "npm run karma -- --single-run",
Expand Down Expand Up @@ -52,19 +52,19 @@
"@semantic-release/changelog": "3.0.4",
"@semantic-release/commit-analyzer": "7.0.0-beta.2",
"@semantic-release/git": "7.1.0-beta.3",
"@semantic-release/github": "5.4.2",
"@semantic-release/github": "5.4.3",
"@semantic-release/gitlab": "3.1.7",
"@semantic-release/npm": "5.2.0-beta.6",
"@semantic-release/release-notes-generator": "7.3.0",
"@types/core-js": "2.5.2",
"@types/eslint": "6.1.0",
"@types/eslint": "6.1.1",
"@types/html-webpack-plugin": "3.2.1",
"@types/jasmine": "3.4.0",
"@types/karma": "3.0.3",
"@types/karma-jasmine": "0.0.31",
"@types/karma-webpack": "2.0.7",
"@types/node": "12.7.2",
"@types/webpack": "4.39.0",
"@types/webpack": "4.39.1",
"@types/webpack-dev-server": "3.1.7",
"@typescript-eslint/eslint-plugin-tslint": "2.0.0",
"@typescript-eslint/parser": "2.0.0",
Expand All @@ -75,7 +75,7 @@
"css-loader": "3.2.0",
"css-to-string-loader": "0.1.3",
"ejs-loader": "0.3.3",
"eslint": "6.2.1",
"eslint": "6.2.2",
"eslint-import-resolver-node": "0.3.2",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-only-warn": "^1.0.1",
Expand All @@ -87,7 +87,7 @@
"html-webpack-plugin": "3.2.0",
"istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "3.4.0",
"karma": "4.2.0",
"karma": "4.3.0",
"karma-chrome-launcher": "3.1.0",
"karma-coverage-istanbul-reporter": "2.1.0",
"karma-jasmine": "2.0.1",
Expand All @@ -100,14 +100,14 @@
"semantic-release": "^16.0.0-beta.19",
"style-loader": "1.0.0",
"ts-node": "8.3.0",
"tsickle": "0.36.0",
"tsickle": "0.37.0",
"tslint": "5.19.0",
"tslint-clean-code": "0.2.9",
"tslint-microsoft-contrib": "6.2.0",
"tslint-sonarts": "1.9.0",
"typescript": "3.5.3",
"watchexec-bin": "1.0.0",
"webpack": "4.39.2",
"webpack": "4.39.3",
"webpack-cli": "3.3.7",
"webpack-dev-server": "3.8.0",
"zone.js": "0.10.2"
Expand Down
30 changes: 27 additions & 3 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/* eslint-disable @typescript-eslint/tslint/config */
import { Component } from '@angular/core';
import { Component, ViewContainerRef } from '@angular/core';
import { ModalModule } from './modal.module';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { TestBed, ComponentFixture, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ModalConfirmService } from './modal-confirm.service';
import { ModalConfirmComponent } from './modal-confirm.component';
import { tap, take } from 'rxjs/operators';
import { By } from '@angular/platform-browser';

describe('Component usage', () => {

Expand All @@ -18,7 +22,11 @@ describe('Component usage', () => {
</modal>
`,
})
class TestComponent { }
class TestComponent {
constructor(
public viewContainerRef: ViewContainerRef,
) { }
}

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -36,4 +44,20 @@ describe('Component usage', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});

it('modal confirmation service component should destroyed', async(() => {
const service: ModalConfirmService = TestBed.get(ModalConfirmService);
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
// const viewContainerRef = fixture.debugElement.injector.get(ViewContainerRef);
const viewContainerRef = component.viewContainerRef;
const observable = service.open(viewContainerRef, ModalConfirmComponent);
fixture.detectChanges();
observable
.pipe(take(1))
.subscribe();
const button = document.querySelector<HTMLButtonElement>('modal-confirm button[role=cancel]');
button!.click();
expect(document.querySelector('modal-confirm')).toBeNull();
}));
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { ModalConfirmService } from './modal-confirm.service';
export { ModalComponent } from './modal.component';
export { ModalContentComponent } from './modal-content.component';
export { ModalFooterComponent } from './modal-footer.component';
Expand Down
27 changes: 27 additions & 0 deletions src/modal-confirm.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ModalConfirmComponent } from './modal-confirm.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ModalModule } from './modal.module';
import { ModalConfirm3Component } from './modal-confirm3.component';
import { By } from '@angular/platform-browser';

describe('ModalConfirmComponent', () => {

Expand Down Expand Up @@ -65,4 +67,29 @@ describe('ModalConfirmComponent', () => {
component.ok();
});

it('emit false on close button', (done) => {
fixture = TestBed.createComponent(ModalConfirm3Component);
component = fixture.componentInstance;
component.markForOpen();
fixture.detectChanges();
const element = fixture.debugElement.query(By.css('modal-header [data-dismiss="modal"]'));
component.result.subscribe(result => {
expect(result).toEqual(false);
done();
});
element.triggerEventHandler('click', null);
});

it('escape should emit false', (done) => {
fixture = TestBed.createComponent(ModalConfirmComponent);
component = fixture.componentInstance;
component.markForOpen();
fixture.detectChanges();
component.result.subscribe(result => {
expect(result).toEqual(false);
done();
});
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
});

});
32 changes: 20 additions & 12 deletions src/modal-confirm.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* tslint:disable:no-import-side-effect */
import { Component, ViewChild, Input, Inject, OnInit, EventEmitter, Output } from '@angular/core';
import { Subject } from 'rxjs';
import { Component, ViewChild, Input, Inject, OnInit, EventEmitter, Output, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { ModalComponent } from './modal.component';
import { ModalOptions, OPTIONS } from './modal-library';
import { filter, take } from 'rxjs/operators';
Expand All @@ -24,10 +23,10 @@ import { filter, take } from 'rxjs/operators';
</modal-footer>
</modal>`,
})
export class ModalConfirmComponent implements OnInit {
export class ModalConfirmComponent implements OnInit, OnDestroy {

@Input() title: string;
@Input() content: string;
@Input() title: string = 'Confirmation';
@Input() content: string = 'Are you sure you want to do that?';
@Input() isNotification: boolean;
@Input() okayLabel = 'Okay';
@Input() cancelLabel = 'Cancel';
Expand All @@ -40,30 +39,39 @@ export class ModalConfirmComponent implements OnInit {
filter(value => value),
take(1),
);
private subscription: Subscription;

constructor(
@Inject(OPTIONS) private readonly modalOptions: ModalOptions,
) { }

ngOnInit() {
this.options = { ...this.modalOptions, ...this.settings };
this.subscription = this.modal.cancelmodal
.subscribe(() => {
this.result.next(false);
});
}

ngOnDestroy() {
this.subscription.unsubscribe();
}

open() {
this.result.next(false);
if (this.modal) {
this.modal.open();
}
this.modal.open();
}

get isOpen() {
return this.modal.isOpen;
}

markForOpen() {
this.modal.isOpen = true;
}

close() {
if (this.modal) {
this.modal.close();
}
this.modal.close();
}

ok() {
Expand Down
23 changes: 23 additions & 0 deletions src/modal-confirm.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable, ComponentFactoryResolver, Type, ViewContainerRef } from '@angular/core';
import { ModalConfirmComponent } from './modal-confirm.component';
import { finalize } from 'rxjs/operators';
import { Observable } from 'rxjs';

/**
* Create modal confirm popup dynamically.
*/
@Injectable()
export class ModalConfirmService {

constructor(
private readonly componentFactoryResolver: ComponentFactoryResolver,
) { }

open(viewContainerRef: ViewContainerRef, componentType: Type<ModalConfirmComponent>): Observable<boolean> {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType);
const componentRef = viewContainerRef.createComponent(componentFactory);
componentRef.instance.markForOpen();
return componentRef.instance.result
.pipe(finalize(() => componentRef.destroy())); // tslint:disable-line:no-void-expression
}
}
2 changes: 1 addition & 1 deletion src/modal-header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class ModalHeaderComponent {

@Input() public title: string;
@Input() public hasCloseButton: boolean;
public closeEventEmitter: EventEmitter<Event> = new EventEmitter();
public readonly closeEventEmitter: EventEmitter<Event> = new EventEmitter();

constructor(
@Inject(OPTIONS) public readonly options: ModalOptions,
Expand Down
15 changes: 11 additions & 4 deletions src/modal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ export class ModalComponent implements OnDestroy, OnInit {
@Input() settings: Partial<ModalOptions>;
@Output() closemodal: EventEmitter<void> = new EventEmitter();
@Output() openmodal: EventEmitter<void> = new EventEmitter();
options: ModalOptions;
@Output() cancelmodal: EventEmitter<void> = new EventEmitter();
@ViewChild('body', { static: true }) private readonly body: ElementRef;
@ContentChild(ModalHeaderComponent, { static: true }) private readonly header: ModalHeaderComponent;
options: ModalOptions;
private closeSubscription: Subscription;
private focusTrap: ReturnType<typeof focusTrap>;

Expand Down Expand Up @@ -85,6 +86,7 @@ export class ModalComponent implements OnDestroy, OnInit {
return;
}
if (event.key === 'Esc' || event.key === 'Escape') {
this.cancelmodal.emit();
this.close();
}
}
Expand All @@ -98,11 +100,16 @@ export class ModalComponent implements OnDestroy, OnInit {

private doOnOpen() {
if (this.header) {
this.closeSubscription = this.header.closeEventEmitter.subscribe(() => {
this.close();
});
this.closeSubscription = this.header.closeEventEmitter
.subscribe(() => {
this.cancelmodal.emit();
this.close();
});
}
setTimeout(() => {
if (!this.isOpen) {
return;
}
const element: HTMLElement | undefined = this.body.nativeElement;
if (element && typeof element.focus === 'function') {
element.focus();
Expand Down
Loading

0 comments on commit 57f89e6

Please sign in to comment.