Skip to content
This repository has been archived by the owner on Nov 6, 2024. It is now read-only.

Commit

Permalink
Fix Date/Timepicker validation for ChangeDetectionStrategy.OnPush (#300)
Browse files Browse the repository at this point in the history
  • Loading branch information
jfcere authored and scote committed Mar 7, 2018
1 parent 4f4e541 commit 7368eee
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 8 deletions.
7 changes: 6 additions & 1 deletion src/app/datepicker/datepicker.directive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Directive, ElementRef, HostBinding, Input, OnDestroy, OnInit, Optional, Renderer } from '@angular/core';
import { ChangeDetectorRef, Directive, ElementRef, HostBinding, Input, OnDestroy, OnInit, Optional, Renderer } from '@angular/core';
import { NgControl } from '@angular/forms';
import { Subscription } from 'rxjs/Subscription';

Expand Down Expand Up @@ -47,6 +47,7 @@ export class MzDatepickerDirective extends HandlePropChanges implements OnInit,

constructor(
@Optional() private ngControl: NgControl,
private changeDetectorRef: ChangeDetectorRef,
private elementRef: ElementRef,
private renderer: Renderer,
) {
Expand Down Expand Up @@ -131,6 +132,10 @@ export class MzDatepickerDirective extends HandlePropChanges implements OnInit,

// set label active status
this.setLabelActive();

// mark for change detection
// fix form validation with ChangeDetectionStrategy.OnPush
this.changeDetectorRef.markForCheck();
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/datepicker/datepicker.directive.unit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('MzDatepickerDirective:unit', () => {
let directive: MzDatepickerDirective;

beforeEach(() => {
directive = new MzDatepickerDirective(null, null, null);
directive = new MzDatepickerDirective(null, null, null, null);
});

describe('ngOnDestroy', () => {
Expand Down
98 changes: 96 additions & 2 deletions src/app/datepicker/datepicker.directive.view.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
import { async, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { MzValidationModule } from 'app';
import { buildComponent, MzTestWrapperComponent } from '../shared/test-wrapper';
import { MzDatepickerContainerComponent, MzDatepickerDirective } from './';

@Component({
selector: `mz-test-datepicker`,
template: `
<form [formGroup]="form">
<mz-datepicker-container>
<input mz-datepicker mz-validation
id="datepicker-id"
type="text"
[errorMessageResource]="errorMessages.datepicker"
[formControlName]="'datepicker'"
[options]="{ format: 'yyyy-mm-dd' }">
</mz-datepicker-container>
<button id="submit" mz-button [disabled]="!form.valid">submit</button>
</form>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class MzTestDatepickerComponent {
errorMessages = {
datepicker: {
required: 'This field is required',
},
};
form: FormGroup;

constructor(formBuilder: FormBuilder) {
this.form = formBuilder.group({
datepicker: [null, Validators.required],
});
}
}

describe('MzDatepickerDirective:view', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
MzValidationModule,
NoopAnimationsModule,
],
declarations: [
MzDatepickerContainerComponent,
MzDatepickerDirective,
MzTestDatepickerComponent,
MzTestWrapperComponent,
],
});
Expand Down Expand Up @@ -544,4 +583,59 @@ describe('MzDatepickerDirective:view', () => {
}));
});
});

describe('validation', () => {
let component: MzTestDatepickerComponent;
let fixture: ComponentFixture<MzTestDatepickerComponent>;
let nativeElement: HTMLElement;

function input(): HTMLInputElement {
return nativeElement.querySelector('input.datepicker') as HTMLInputElement;
}

function datepicker(): Pickadate.DatePicker {
return $(input()).pickadate('picker');
}

function errorMessage(): HTMLElement {
return nativeElement.querySelector('mz-error-message') as HTMLElement;
}

function submitButton(): HTMLButtonElement {
return nativeElement.querySelector('button#submit') as HTMLButtonElement;
}

beforeEach(() => {
fixture = TestBed.createComponent(MzTestDatepickerComponent);
component = fixture.componentInstance;
nativeElement = fixture.nativeElement;
fixture.detectChanges();
});

it('should be reflected correctly when used with ChangeStrategy.OnPush', fakeAsync(() => {

// initial state
expect(errorMessage().innerText.trim()).toBe('');
expect(component.form.valid).toBeFalsy();
expect(submitButton().hasAttribute('disabled')).toBeTruthy();

// invalid
datepicker().clear();
component.form.get('datepicker').markAsDirty();
fixture.detectChanges();

expect(errorMessage().innerText.trim()).toBe(component.errorMessages.datepicker.required);
expect(component.form.valid).toBeFalsy();
expect(submitButton().hasAttribute('disabled')).toBeTruthy();

// valid
datepicker().set('select', '2017-02-03');
fixture.detectChanges();
tick();

expect(errorMessage().innerText.trim()).toBe('');
expect(component.form.valid).toBeTruthy();
expect(submitButton().hasAttribute('disabled')).toBeFalsy();
}));
});
});
7 changes: 6 additions & 1 deletion src/app/timepicker/timepicker.directive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Directive, ElementRef, HostBinding, Input, NgZone, OnDestroy, OnInit, Optional, Renderer } from '@angular/core';
import { ChangeDetectorRef, Directive, ElementRef, HostBinding, Input, NgZone, OnDestroy, OnInit, Optional, Renderer } from '@angular/core';
import { NgControl } from '@angular/forms';
import { Subscription } from 'rxjs/Subscription';

Expand Down Expand Up @@ -33,6 +33,7 @@ export class MzTimepickerDirective extends HandlePropChanges implements OnInit,

constructor(
@Optional() private ngControl: NgControl,
private changeDetectorRef: ChangeDetectorRef,
private elementRef: ElementRef,
private renderer: Renderer,
private zone: NgZone,
Expand Down Expand Up @@ -88,6 +89,10 @@ export class MzTimepickerDirective extends HandlePropChanges implements OnInit,
// set ngControl value according to selected time in timepicker
this.inputElement.on('change', (event: JQuery.Event<HTMLInputElement>) => {
this.ngControl.control.setValue(event.target.value);

// mark for change detection
// fix form validation with ChangeDetectionStrategy.OnPush
this.changeDetectorRef.markForCheck();
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/timepicker/timepicker.directive.unit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('MzTimepickerDirective:unit', () => {
}

beforeEach(() => {
directive = new MzTimepickerDirective(null, null, null, null);
directive = new MzTimepickerDirective(null, null, null, null, null);
directive.inputElement = createInputElement();
});

Expand Down
99 changes: 97 additions & 2 deletions src/app/timepicker/timepicker.directive.view.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,58 @@
import { async, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { MzValidationModule } from 'app';
import { buildComponent, MzTestWrapperComponent } from '../shared/test-wrapper';
import { MzTimepickerContainerComponent, MzTimepickerDirective } from './';

@Component({
selector: `mz-test-timepicker`,
template: `
<form [formGroup]="form">
<mz-timepicker-container>
<input mz-timepicker mz-validation
id="timepicker-id"
type="text"
[errorMessageResource]="errorMessages.timepicker"
[formControlName]="'timepicker'">
</mz-timepicker-container>
<button id="submit" mz-button [disabled]="!form.valid">submit</button>
</form>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class MzTestTimepickerComponent {
errorMessages = {
timepicker: {
required: 'This field is required',
},
};
form: FormGroup;

constructor(formBuilder: FormBuilder) {
this.form = formBuilder.group({
timepicker: [null, Validators.required],
});
}
}

describe('MzTimepickerDirective:view', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
MzValidationModule,
NoopAnimationsModule,
],
declarations: [
MzTimepickerContainerComponent,
MzTimepickerDirective,
MzTestTimepickerComponent,
MzTestWrapperComponent,
],
});
Expand Down Expand Up @@ -386,4 +424,61 @@ describe('MzTimepickerDirective:view', () => {
});
}));
});

describe('validation', () => {
let component: MzTestTimepickerComponent;
let fixture: ComponentFixture<MzTestTimepickerComponent>;
let nativeElement: HTMLElement;

function input(): HTMLInputElement {
return nativeElement.querySelector('input.timepicker') as HTMLInputElement;
}

function timepicker(): Pickadate.DatePicker {
return $(input()).pickadate('picker');
}

function errorMessage(): HTMLElement {
return nativeElement.querySelector('mz-error-message') as HTMLElement;
}

function submitButton(): HTMLButtonElement {
return nativeElement.querySelector('button#submit') as HTMLButtonElement;
}

beforeEach(() => {
fixture = TestBed.createComponent(MzTestTimepickerComponent);
component = fixture.componentInstance;
nativeElement = fixture.nativeElement;
fixture.detectChanges();
});

it('should be reflected correctly when used with ChangeStrategy.OnPush', fakeAsync(() => {

// initial state
expect(errorMessage().innerText.trim()).toBe('');
expect(component.form.valid).toBeFalsy();
expect(submitButton().hasAttribute('disabled')).toBeTruthy();

// invalid
$(input()).val(null);
$(input()).change();
component.form.get('timepicker').markAsDirty();
fixture.detectChanges();

expect(errorMessage().innerText.trim()).toBe(component.errorMessages.timepicker.required);
expect(component.form.valid).toBeFalsy();
expect(submitButton().hasAttribute('disabled')).toBeTruthy();

// valid
$(input()).val('05:45PM');
$(input()).change();
fixture.detectChanges();
tick();

expect(errorMessage().innerText.trim()).toBe('');
expect(component.form.valid).toBeTruthy();
expect(submitButton().hasAttribute('disabled')).toBeFalsy();
}));
});
});

0 comments on commit 7368eee

Please sign in to comment.