mgr/dashboard: Change deletion link to modal only

Due to CSS problems the link solution wasn't the best way, now it will
represent only the modal content. The downside of this solution is that
it put's the burden on the developer to use it the right way and import
a view things to get it working. But on the upside CSS styles will work
as expected.

The unit test example was updated accordingly this way it should be easy
to understand how it can be implemented the right way.

Signed-off-by: Stephan Müller <smueller@suse.com>
This commit is contained in:
Stephan Müller 2018-04-16 13:44:38 +02:00
parent f2096e9378
commit 63ae858582
8 changed files with 521 additions and 388 deletions

View File

@ -9,7 +9,7 @@ import { PipesModule } from '../pipes/pipes.module';
import {
DeleteConfirmationComponent
} from './delete-confirmation-modal/delete-confirmation-modal.component';
import { DeletionLinkComponent } from './deletion-link/deletion-link.component';
import { DeletionModalComponent } from './deletion-modal/deletion-modal.component';
import { HelperComponent } from './helper/helper.component';
import { ModalComponent } from './modal/modal.component';
import { SparklineComponent } from './sparkline/sparkline.component';
@ -38,7 +38,10 @@ import { ViewCacheComponent } from './view-cache/view-cache.component';
UsageBarComponent,
DeleteConfirmationComponent,
ModalComponent,
DeletionLinkComponent
DeletionModalComponent
],
entryComponents: [
DeletionModalComponent
],
providers: [],
exports: [
@ -52,7 +55,7 @@ import { ViewCacheComponent } from './view-cache/view-cache.component';
entryComponents: [
DeleteConfirmationComponent,
ModalComponent,
DeletionLinkComponent
DeletionModalComponent
]
})
export class ComponentsModule { }

View File

@ -1,88 +0,0 @@
<a (click)="showModal(deletionModal)">
<i class="fa fa-fw fa-trash-o"></i>
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
</a>
<ng-template #deletionModal>
<cd-modal #modal
[modalRef]="bsModalRef">
<ng-container class="modal-title">
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
</ng-container>
<ng-container class="modal-content">
<ng-container *ngTemplateOutlet="deletionContent"></ng-container>
</ng-container>
</cd-modal>
</ng-template>
<ng-template #deletionContent>
<form name="deletionForm"
#formDir="ngForm"
(submit)="delete()"
[formGroup]="deletionForm"
novalidate>
<div class="modal-body">
<ng-template *ngTemplateOutlet="deletionDescription"></ng-template>
<p>
<ng-container i18n>
To confirm the deletion, enter
</ng-container>
<kbd>{{ pattern }}</kbd>
<ng-container i18n>
and click on
</ng-container>
<kbd>
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
</kbd>.
</p>
<div class="form-group"
[ngClass]="{'has-error': invalidControl(formDir.submitted)}">
<input type="text"
class="form-control"
name="confirmation"
id="confirmation"
[placeholder]="pattern"
autocomplete="off"
(keyup)="updateConfirmation($event)"
formControlName="confirmation"
autofocus>
<span class="help-block"
*ngIf="invalidControl(formDir.submitted,'required')"
i18n>
This field is required.
</span>
<span class="help-block"
*ngIf="invalidControl(formDir.submitted, 'pattern')">
'{{ confirmation.value }}'
<span i18n>doesn't match</span>
'{{ pattern }}'.
</span>
</div>
</div>
<div class="modal-footer">
<cd-submit-button #submitButton
[form]="deletionForm"
(submitAction)="deletionCall()">
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
</cd-submit-button>
<button class="btn btn-link btn-sm"
(click)="hideModal()"
i18n>
Cancel
</button>
</div>
</form>
</ng-template>
<ng-template #deletionHeading>
<ng-container i18n>
Delete
</ng-container>
{{ metaType }}
</ng-template>
<ng-template #deletionDescription>
<ng-content></ng-content>
</ng-template>

View File

@ -1,210 +0,0 @@
import { Component, ViewChild } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormGroupDirective, ReactiveFormsModule } from '@angular/forms';
import { ModalModule } from 'ngx-bootstrap';
import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';
import { ModalComponent } from '../modal/modal.component';
import { SubmitButtonComponent } from '../submit-button/submit-button.component';
import { DeletionLinkComponent } from './deletion-link.component';
@Component({
template: `
<cd-deletion-link #ctrlDeleteButton
metaType="Controller delete handling"
pattern="ctrl-test"
(toggleDeletion)="fakeDeleteController()">
The spinner is handled by the controller if you have use the modal as ViewChild in order to
use it's functions to stop the spinner or close the dialog.
</cd-deletion-link>
<cd-deletion-link #modalDeleteButton
metaType="Modal delete handling"
[deletionObserver]="fakeDelete()"
pattern="modal-test">
The spinner is handled by the modal if your given deletion function returns a Observable.
</cd-deletion-link>
`
})
class MockComponent {
@ViewChild('ctrlDeleteButton') ctrlDeleteButton: DeletionLinkComponent;
@ViewChild('modalDeleteButton') modalDeleteButton: DeletionLinkComponent;
someData = [1, 2, 3, 4, 5];
finished: number[];
finish() {
this.finished = [6, 7, 8, 9];
}
fakeDelete() {
return (): Observable<any> => {
return new Observable((observer: Subscriber<any>) => {
Observable.timer(100).subscribe(() => {
observer.next(this.finish());
observer.complete();
});
});
};
}
fakeDeleteController() {
Observable.timer(100).subscribe(() => {
this.finish();
this.ctrlDeleteButton.hideModal();
});
}
}
describe('DeletionLinkComponent', () => {
let mockComponent: MockComponent;
let component: DeletionLinkComponent;
let fixture: ComponentFixture<MockComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MockComponent, DeletionLinkComponent, ModalComponent,
SubmitButtonComponent],
imports: [ModalModule.forRoot(), ReactiveFormsModule],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MockComponent);
mockComponent = fixture.componentInstance;
component = mockComponent.ctrlDeleteButton;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('component functions', () => {
const mockShowModal = () => {
component.showModal(null);
};
const changeValue = (value) => {
component.confirmation.setValue(value);
component.confirmation.markAsDirty();
component.confirmation.updateValueAndValidity();
};
beforeEach(() => {
spyOn(component.modalService, 'show').and.returnValue({
hide: () => true
});
});
it('should test showModal', () => {
changeValue('something');
expect(mockShowModal).toBeTruthy();
expect(component.confirmation.value).toBe('something');
expect(component.modalService.show).not.toHaveBeenCalled();
mockShowModal();
expect(component.modalService.show).toHaveBeenCalled();
expect(component.confirmation.value).toBe(null);
expect(component.confirmation.pristine).toBe(true);
});
it('should test hideModal', () => {
expect(component.bsModalRef).not.toBeTruthy();
mockShowModal();
expect(component.bsModalRef).toBeTruthy();
expect(component.hideModal).toBeTruthy();
spyOn(component.bsModalRef, 'hide').and.stub();
expect(component.bsModalRef.hide).not.toHaveBeenCalled();
component.hideModal();
expect(component.bsModalRef.hide).toHaveBeenCalled();
});
describe('invalid control', () => {
const testInvalidControl = (submitted: boolean, error: string, expected: boolean) => {
expect(component.invalidControl(submitted, error)).toBe(expected);
};
beforeEach(() => {
component.deletionForm.reset();
});
it('should test empty values', () => {
expect(component.invalidControl).toBeTruthy();
component.deletionForm.reset();
testInvalidControl(false, undefined, false);
testInvalidControl(true, 'required', true);
component.deletionForm.reset();
changeValue('let-me-pass');
changeValue('');
testInvalidControl(true, 'required', true);
});
it('should test pattern', () => {
changeValue('let-me-pass');
testInvalidControl(false, 'pattern', true);
changeValue('ctrl-test');
testInvalidControl(false, undefined, false);
testInvalidControl(true, undefined, false);
});
});
describe('deletion call', () => {
beforeEach(() => {
spyOn(component.toggleDeletion, 'emit');
spyOn(component, 'stopLoadingSpinner');
spyOn(component, 'hideModal').and.stub();
});
describe('Controller driven', () => {
beforeEach(() => {
mockShowModal();
expect(component.toggleDeletion.emit).not.toHaveBeenCalled();
expect(component.stopLoadingSpinner).not.toHaveBeenCalled();
expect(component.hideModal).not.toHaveBeenCalled();
});
it('should delete without doing anything but call emit', () => {
component.deletionCall();
expect(component.stopLoadingSpinner).not.toHaveBeenCalled();
expect(component.hideModal).not.toHaveBeenCalled();
expect(component.toggleDeletion.emit).toHaveBeenCalled();
});
it('should test fake deletion that closes modal', <any>fakeAsync(() => {
mockComponent.fakeDeleteController();
expect(component.hideModal).not.toHaveBeenCalled();
expect(mockComponent.finished).toBe(undefined);
tick(2000);
expect(component.hideModal).toHaveBeenCalled();
expect(mockComponent.finished).toEqual([6, 7, 8, 9]);
}));
});
describe('Modal driven', () => {
it('should delete and close modal', <any>fakeAsync(() => {
component = mockComponent.modalDeleteButton;
mockShowModal();
spyOn(component.toggleDeletion, 'emit');
spyOn(component, 'stopLoadingSpinner');
spyOn(component, 'hideModal').and.stub();
spyOn(mockComponent, 'fakeDelete');
component.deletionCall();
expect(mockComponent.finished).toBe(undefined);
expect(component.toggleDeletion.emit).not.toHaveBeenCalled();
expect(component.hideModal).not.toHaveBeenCalled();
tick(2000);
expect(component.toggleDeletion.emit).not.toHaveBeenCalled();
expect(component.stopLoadingSpinner).not.toHaveBeenCalled();
expect(component.hideModal).toHaveBeenCalled();
expect(mockComponent.finished).toEqual([6, 7, 8, 9]);
}));
});
});
});
});

View File

@ -1,87 +0,0 @@
import {
Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild
} from '@angular/core';
import { FormControl, FormGroup, FormGroupDirective, Validators } from '@angular/forms';
import { BsModalRef, BsModalService } from 'ngx-bootstrap';
import { Observable } from 'rxjs/Observable';
import { SubmitButtonComponent } from '../submit-button/submit-button.component';
@Component({
selector: 'cd-deletion-link',
templateUrl: './deletion-link.component.html',
styleUrls: ['./deletion-link.component.scss']
})
export class DeletionLinkComponent implements OnInit {
@ViewChild(SubmitButtonComponent) submitButton: SubmitButtonComponent;
@Input() metaType: string;
@Input() pattern = 'yes';
@Input() deletionObserver: () => Observable<any>;
@Output() toggleDeletion = new EventEmitter();
bsModalRef: BsModalRef;
deletionForm: FormGroup;
confirmation: FormControl;
delete: Function;
constructor(public modalService: BsModalService) {}
ngOnInit() {
this.confirmation = new FormControl('', {
validators: [
Validators.required,
Validators.pattern(this.pattern)
],
updateOn: 'blur'
});
this.deletionForm = new FormGroup({
confirmation: this.confirmation
});
}
showModal(template: TemplateRef<any>) {
this.deletionForm.reset();
this.bsModalRef = this.modalService.show(template);
this.delete = () => {
this.submitButton.submit();
};
}
invalidControl(submitted: boolean, error?: string): boolean {
const control = this.confirmation;
return !!(
(submitted || control.dirty) &&
control.invalid &&
(error ? control.errors[error] : true)
);
}
updateConfirmation($e) {
if ($e.key !== 'Enter') {
return;
}
this.confirmation.setValue($e.target.value);
this.confirmation.markAsDirty();
this.confirmation.updateValueAndValidity();
}
deletionCall() {
if (this.deletionObserver) {
this.deletionObserver().subscribe(
undefined,
() => this.stopLoadingSpinner(),
() => this.hideModal()
);
} else {
this.toggleDeletion.emit();
}
}
hideModal() {
this.bsModalRef.hide();
}
stopLoadingSpinner() {
this.submitButton.loading = false;
}
}

View File

@ -0,0 +1,73 @@
<cd-modal #modal
[modalRef]="modalRef">
<ng-container class="modal-title">
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
</ng-container>
<ng-container class="modal-content">
<form name="deletionForm"
#formDir="ngForm"
(submit)="delete()"
[formGroup]="deletionForm"
novalidate>
<div class="modal-body">
<ng-container *ngTemplateOutlet="description"></ng-container>
<p>
<ng-container i18n>
To confirm the deletion, enter
</ng-container>
<kbd>{{ pattern }}</kbd>
<ng-container i18n>
and click on
</ng-container>
<kbd>
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
</kbd>.
</p>
<div class="form-group"
[ngClass]="{'has-error': invalidControl(formDir.submitted)}">
<input type="text"
class="form-control"
name="confirmation"
id="confirmation"
[placeholder]="pattern"
[pattern]="pattern"
autocomplete="off"
(keyup)="updateConfirmation($event)"
formControlName="confirmation"
autofocus>
<span class="help-block"
*ngIf="invalidControl(formDir.submitted,'required')"
i18n>
This field is required.
</span>
<span class="help-block"
*ngIf="invalidControl(formDir.submitted, 'pattern')">
'{{ confirmation.value }}'
<span i18n>doesn't match</span>
'{{ pattern }}'.
</span>
</div>
</div>
<div class="modal-footer">
<cd-submit-button #submitButton
[form]="deletionForm"
(submitAction)="deletionCall()">
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
</cd-submit-button>
<button class="btn btn-link btn-sm"
(click)="hideModal()"
i18n>
Cancel
</button>
</div>
</form>
</ng-container>
</cd-modal>
<ng-template #deletionHeading>
<ng-container i18n>
Delete
</ng-container>
{{ metaType }}
</ng-template>

View File

@ -0,0 +1,342 @@
import { Component, NgModule, TemplateRef, ViewChild } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap';
import { Observable } from 'rxjs/Observable';
import { Subscriber } from 'rxjs/Subscriber';
import { ModalComponent } from '../modal/modal.component';
import { SubmitButtonComponent } from '../submit-button/submit-button.component';
import { DeletionModalComponent } from './deletion-modal.component';
@NgModule({
entryComponents: [DeletionModalComponent]
})
export class MockModule {}
@Component({
template: `
<button type="button"
class="btn btn-sm btn-primary"
(click)="openCtrlDriven()">
<i class="fa fa-fw fa-trash"></i>Deletion Ctrl-Test
<ng-template #ctrlDescription>
The spinner is handled by the controller if you have use the modal as ViewChild in order to
use it's functions to stop the spinner or close the dialog.
</ng-template>
</button>
<button type="button"
class="btn btn-sm btn-primary"
(click)="openModalDriven()">
<i class="fa fa-fw fa-trash"></i>Deletion Modal-Test
<ng-template #modalDescription>
The spinner is handled by the modal if your given deletion function returns a Observable.
</ng-template>
</button>
`
})
class MockComponent {
@ViewChild('ctrlDescription') ctrlDescription: TemplateRef<any>;
@ViewChild('modalDescription') modalDescription: TemplateRef<any>;
someData = [1, 2, 3, 4, 5];
finished: number[];
ctrlRef: BsModalRef;
modalRef: BsModalRef;
// Normally private - public was needed for the tests
constructor(public modalService: BsModalService) {}
openCtrlDriven() {
this.ctrlRef = this.modalService.show(DeletionModalComponent);
this.ctrlRef.content.setUp({
metaType: 'Controller delete handling',
pattern: 'ctrl-test',
deletionMethod: this.fakeDeleteController.bind(this),
description: this.ctrlDescription,
modalRef: this.ctrlRef
});
}
openModalDriven() {
this.modalRef = this.modalService.show(DeletionModalComponent);
this.modalRef.content.setUp({
metaType: 'Modal delete handling',
pattern: 'modal-test',
deletionObserver: this.fakeDelete(),
description: this.modalDescription,
modalRef: this.modalRef
});
}
finish() {
this.finished = [6, 7, 8, 9];
}
fakeDelete() {
return (): Observable<any> => {
return new Observable((observer: Subscriber<any>) => {
Observable.timer(100).subscribe(() => {
observer.next(this.finish());
observer.complete();
});
});
};
}
fakeDeleteController() {
Observable.timer(100).subscribe(() => {
this.finish();
this.ctrlRef.hide();
});
}
}
describe('DeletionModalComponent', () => {
let mockComponent: MockComponent;
let component: DeletionModalComponent;
let mockFixture: ComponentFixture<MockComponent>;
let fixture: ComponentFixture<DeletionModalComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MockComponent, DeletionModalComponent, ModalComponent,
SubmitButtonComponent],
imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule],
})
.compileComponents();
}));
beforeEach(() => {
mockFixture = TestBed.createComponent(MockComponent);
mockComponent = mockFixture.componentInstance;
// Mocking the modals as a lot would be left over
spyOn(mockComponent.modalService, 'show').and.callFake(() => {
const ref = new BsModalRef();
fixture = TestBed.createComponent(DeletionModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
ref.content = component;
return ref;
});
mockComponent.openCtrlDriven();
mockFixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('setUp', () => {
const clearSetup = () => {
component.metaType = undefined;
component.pattern = 'yes';
component.deletionObserver = undefined;
component.description = undefined;
component.modalRef = undefined;
};
const expectSetup = (metaType, observer: boolean, method: boolean, pattern,
template: boolean) => {
expect(component.modalRef).toBeTruthy();
expect(component.metaType).toBe(metaType);
expect(!!component.deletionObserver).toBe(observer);
expect(!!component.deletionMethod).toBe(method);
expect(component.pattern).toBe(pattern);
expect(!!component.description).toBe(template);
};
beforeEach(() => {
clearSetup();
});
it('should throw error if no modal reference is given', () => {
expect(() => component.setUp({
metaType: undefined,
modalRef: undefined
})).toThrowError('No modal reference');
});
it('should throw error if no meta type is given', () => {
expect(() => component.setUp({
metaType: undefined,
modalRef: mockComponent.ctrlRef
})).toThrowError('No meta type');
});
it('should throw error if no deletion method is given', () => {
expect(() => component.setUp({
metaType: 'Sth',
modalRef: mockComponent.ctrlRef
})).toThrowError('No deletion method');
});
it('should throw no errors if metaType, modalRef and a deletion method were given',
() => {
component.setUp({
metaType: 'Observer',
modalRef: mockComponent.ctrlRef,
deletionObserver: mockComponent.fakeDelete()
});
expectSetup('Observer', true, false, 'yes', false);
clearSetup();
component.setUp({
metaType: 'Controller',
modalRef: mockComponent.ctrlRef,
deletionMethod: mockComponent.fakeDeleteController
});
expectSetup('Controller', false, true, 'yes', false);
});
it('should test optional parameters - pattern and description',
() => {
component.setUp({
metaType: 'Pattern only',
modalRef: mockComponent.ctrlRef,
deletionObserver: mockComponent.fakeDelete(),
pattern: '{sth/!$_8()'
});
expectSetup('Pattern only', true, false, '{sth/!$_8()', false);
clearSetup();
component.setUp({
metaType: 'Description only',
modalRef: mockComponent.ctrlRef,
deletionObserver: mockComponent.fakeDelete(),
description: mockComponent.modalDescription
});
expectSetup('Description only', true, false, 'yes', true);
clearSetup();
component.setUp({
metaType: 'Description and pattern',
modalRef: mockComponent.ctrlRef,
deletionObserver: mockComponent.fakeDelete(),
description: mockComponent.modalDescription,
pattern: '{sth/!$_8()'
});
expectSetup('Description and pattern', true, false, '{sth/!$_8()', true);
});
});
it('should test if the ctrl driven mock is set correctly through mock component', () => {
expect(component.metaType).toBe('Controller delete handling');
expect(component.pattern).toBe('ctrl-test');
expect(component.description).toBeTruthy();
expect(component.modalRef).toBeTruthy();
expect(component.deletionMethod).toBeTruthy();
expect(component.deletionObserver).not.toBeTruthy();
});
it('should test if the modal driven mock is set correctly through mock component', () => {
mockComponent.openModalDriven();
expect(component.metaType).toBe('Modal delete handling');
expect(component.pattern).toBe('modal-test');
expect(component.description).toBeTruthy();
expect(component.modalRef).toBeTruthy();
expect(component.deletionObserver).toBeTruthy();
expect(component.deletionMethod).not.toBeTruthy();
});
describe('component functions', () => {
const changeValue = (value) => {
component.confirmation.setValue(value);
component.confirmation.markAsDirty();
component.confirmation.updateValueAndValidity();
fixture.detectChanges();
};
it('should test hideModal', () => {
expect(component.modalRef).toBeTruthy();
expect(component.hideModal).toBeTruthy();
spyOn(component.modalRef, 'hide').and.callThrough();
expect(component.modalRef.hide).not.toHaveBeenCalled();
component.hideModal();
expect(component.modalRef.hide).toHaveBeenCalled();
});
describe('invalid control', () => {
const testInvalidControl = (submitted: boolean, error: string, expected: boolean) => {
expect(component.invalidControl(submitted, error)).toBe(expected);
};
beforeEach(() => {
component.deletionForm.reset();
});
it('should test empty values', () => {
expect(component.invalidControl).toBeTruthy();
component.deletionForm.reset();
testInvalidControl(false, undefined, false);
testInvalidControl(true, 'required', true);
component.deletionForm.reset();
changeValue('let-me-pass');
changeValue('');
testInvalidControl(true, 'required', true);
});
it('should test pattern', () => {
changeValue('let-me-pass');
testInvalidControl(false, 'pattern', true);
changeValue('ctrl-test');
testInvalidControl(false, undefined, false);
testInvalidControl(true, undefined, false);
});
});
describe('deletion call', () => {
beforeEach(() => {
spyOn(component, 'stopLoadingSpinner').and.callThrough();
spyOn(component, 'hideModal').and.callThrough();
});
describe('Controller driven', () => {
beforeEach(() => {
spyOn(component, 'deletionMethod').and.callThrough();
spyOn(mockComponent.ctrlRef, 'hide').and.callThrough();
});
it('should test fake deletion that closes modal', <any>fakeAsync(() => {
// Before deletionCall
expect(component.deletionMethod).not.toHaveBeenCalled();
// During deletionCall
component.deletionCall();
expect(component.stopLoadingSpinner).not.toHaveBeenCalled();
expect(component.hideModal).not.toHaveBeenCalled();
expect(mockComponent.ctrlRef.hide).not.toHaveBeenCalled();
expect(component.deletionMethod).toHaveBeenCalled();
expect(mockComponent.finished).toBe(undefined);
// After deletionCall
tick(2000);
expect(component.hideModal).not.toHaveBeenCalled();
expect(mockComponent.ctrlRef.hide).toHaveBeenCalled();
expect(mockComponent.finished).toEqual([6, 7, 8, 9]);
}));
});
describe('Modal driven', () => {
beforeEach(() => {
mockComponent.openModalDriven();
spyOn(mockComponent.modalRef, 'hide').and.callThrough();
spyOn(component, 'stopLoadingSpinner').and.callThrough();
spyOn(component, 'hideModal').and.callThrough();
spyOn(mockComponent, 'fakeDelete').and.callThrough();
});
it('should delete and close modal', <any>fakeAsync(() => {
// During deletionCall
component.deletionCall();
expect(mockComponent.finished).toBe(undefined);
expect(component.hideModal).not.toHaveBeenCalled();
expect(mockComponent.modalRef.hide).not.toHaveBeenCalled();
// After deletionCall
tick(2000);
expect(mockComponent.finished).toEqual([6, 7, 8, 9]);
expect(mockComponent.modalRef.hide).toHaveBeenCalled();
expect(component.stopLoadingSpinner).not.toHaveBeenCalled();
expect(component.hideModal).toHaveBeenCalled();
}));
});
});
});
});

View File

@ -0,0 +1,100 @@
import {
Component, OnInit, TemplateRef, ViewChild
} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { BsModalRef, BsModalService } from 'ngx-bootstrap';
import { Observable } from 'rxjs/Observable';
import { SubmitButtonComponent } from '../submit-button/submit-button.component';
@Component({
selector: 'cd-deletion-modal',
templateUrl: './deletion-modal.component.html',
styleUrls: ['./deletion-modal.component.scss']
})
export class DeletionModalComponent implements OnInit {
@ViewChild(SubmitButtonComponent) submitButton: SubmitButtonComponent;
description: TemplateRef<any>;
metaType: string;
pattern = 'yes';
deletionObserver: () => Observable<any>;
deletionMethod: Function;
modalRef: BsModalRef;
deletionForm: FormGroup;
confirmation: FormControl;
// Parameters are destructed here than assigned to specific types and marked as optional
setUp({modalRef, metaType, deletionMethod, pattern, deletionObserver, description}:
{ modalRef: BsModalRef, metaType: string, deletionMethod?: Function, pattern?: string,
deletionObserver?: () => Observable<any>, description?: TemplateRef<any>}) {
if (!modalRef) {
throw new Error('No modal reference');
} else if (!metaType) {
throw new Error('No meta type');
} else if (!(deletionMethod || deletionObserver)) {
throw new Error('No deletion method');
}
this.metaType = metaType;
this.modalRef = modalRef;
this.deletionMethod = deletionMethod;
this.pattern = pattern || this.pattern;
this.deletionObserver = deletionObserver;
this.description = description;
}
ngOnInit() {
this.confirmation = new FormControl('', {
validators: [
Validators.required
],
updateOn: 'blur'
});
this.deletionForm = new FormGroup({
confirmation: this.confirmation
});
}
invalidControl(submitted: boolean, error?: string): boolean {
const control = this.confirmation;
return !!(
(submitted || control.dirty) &&
control.invalid &&
(error ? control.errors[error] : true)
);
}
updateConfirmation($e) {
if ($e.key !== 'Enter') {
return;
}
this.confirmation.setValue($e.target.value);
this.confirmation.markAsDirty();
this.confirmation.updateValueAndValidity();
}
delete () {
this.submitButton.submit();
}
deletionCall() {
if (this.deletionObserver) {
this.deletionObserver().subscribe(
undefined,
this.stopLoadingSpinner.bind(this),
this.hideModal.bind(this)
);
} else {
this.deletionMethod();
}
}
hideModal() {
this.modalRef.hide();
}
stopLoadingSpinner() {
this.submitButton.loading = false;
}
}