Skip to content

Commit

Permalink
fix(module:time-picker): input value change not work (#5770)
Browse files Browse the repository at this point in the history
Closes #5678
Closes #5741
Closes #4934
  • Loading branch information
wenqi73 authored Sep 22, 2020
1 parent fa46a79 commit 31ca2da
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 184 deletions.
1 change: 1 addition & 0 deletions components/core/time/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

export * from './candy-date';
export * from './time';
export { NgTimeParser as ɵNgTimeParser, TimeResult as ɵTimeResult } from './time-parser';
94 changes: 94 additions & 0 deletions components/core/time/time-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { registerLocaleData } from '@angular/common';
import zh from '@angular/common/locales/zh';
import { Injector, LOCALE_ID } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { NgTimeParser, TimeResult } from './time-parser';

describe('Parse time with angular format', () => {
let injector: Injector;
let localeId: string;
let parser: NgTimeParser;
let result: TimeResult | null;
let time: Date;

describe('default locale', () => {
beforeEach(() => {
injector = TestBed.configureTestingModule({});
localeId = injector.get(LOCALE_ID);
});

it('should parse hh:mm:ss a', () => {
parser = new NgTimeParser('hh:mm:ss a', localeId);
result = parser.getTimeResult('12:30:22 AM');
expect(result?.hour).toBe(12);
expect(result?.minute).toBe(30);
expect(result?.second).toBe(22);
expect(result?.period).toBe(0);

result = parser.getTimeResult('12:30:22 PM');
expect(result?.period).toBe(1);

time = parser.toDate('12:30:22 PM');
expect(time.getHours()).toBe(12);
expect(time.getMinutes()).toBe(30);
expect(time.getSeconds()).toBe(22);

time = parser.toDate('05:30:22 PM');
expect(time.getHours()).toBe(17);
});

it('should parse hh:mm:ss aaaaa', () => {
parser = new NgTimeParser('hh:mm:ss aaaaa', localeId);
result = parser.getTimeResult('12:30:22 a');
expect(result?.hour).toBe(12);
expect(result?.minute).toBe(30);
expect(result?.second).toBe(22);
expect(result?.period).toBe(0);

result = parser.getTimeResult('12:30:22 p');
expect(result?.period).toBe(1);
});

it('should parse mm(ss) HH', () => {
parser = new NgTimeParser('mm(ss) HH', localeId);
result = parser.getTimeResult('30(22) 12');
expect(result?.period).toBe(null);

time = parser.toDate('30(22) 12');
expect(time.getHours()).toBe(12);
expect(time.getMinutes()).toBe(30);
expect(time.getSeconds()).toBe(22);
});

it('should parse ss + mm', () => {
parser = new NgTimeParser('ss + mm', localeId);
time = parser.toDate('10 + 42');
const now = new Date();
expect(time.getHours()).toBe(now.getHours());
expect(time.getMinutes()).toBe(42);
expect(time.getSeconds()).toBe(10);
});
});

describe('zh locale', () => {
registerLocaleData(zh);
beforeEach(() => {
injector = TestBed.configureTestingModule({
providers: [{ provide: LOCALE_ID, useValue: 'zh-CN' }]
});
localeId = injector.get(LOCALE_ID);
});

it('should parse hh:mm:ss a', () => {
parser = new NgTimeParser('hh:mm:ss a', localeId);
result = parser.getTimeResult('04:45:22 上午');
expect(result?.hour).toBe(4);
expect(result?.minute).toBe(45);
expect(result?.second).toBe(22);
expect(result?.period).toBe(0);

time = parser.toDate('04:45:22 下午');
expect(time.getHours()).toBe(16);
});
});
});
143 changes: 143 additions & 0 deletions components/core/time/time-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
*/

// from https://github.com/hsuanxyz/ng-time-parser
import { FormStyle, getLocaleDayPeriods, TranslationWidth } from '@angular/common';
import { isNotNil } from 'ng-zorro-antd/core/util';

export interface TimeResult {
hour: number | null;
minute: number | null;
second: number | null;
period: number | null;
}

export class NgTimeParser {
regex: RegExp = null!;
matchMap: { [key: string]: null | number } = {
hour: null,
minute: null,
second: null,
periodNarrow: null,
periodWide: null,
periodAbbreviated: null
};

constructor(private format: string, private localeId: string) {
this.genRegexp();
}

toDate(str: string): Date {
const result = this.getTimeResult(str);
const time = new Date();

if (isNotNil(result?.hour)) {
time.setHours(result!.hour);
}

if (isNotNil(result?.minute)) {
time.setMinutes(result!.minute);
}

if (isNotNil(result?.second)) {
time.setSeconds(result!.second);
}

if (result?.period === 1 && time.getHours() < 12) {
time.setHours(time.getHours() + 12);
}

return time;
}

getTimeResult(str: string): TimeResult | null {
const match = this.regex.exec(str);
let period = null;
if (match) {
if (isNotNil(this.matchMap.periodNarrow)) {
period = getLocaleDayPeriods(this.localeId, FormStyle.Format, TranslationWidth.Narrow).indexOf(
match[this.matchMap.periodNarrow + 1]
);
}
if (isNotNil(this.matchMap.periodWide)) {
period = getLocaleDayPeriods(this.localeId, FormStyle.Format, TranslationWidth.Wide).indexOf(match[this.matchMap.periodWide + 1]);
}
if (isNotNil(this.matchMap.periodAbbreviated)) {
period = getLocaleDayPeriods(this.localeId, FormStyle.Format, TranslationWidth.Abbreviated).indexOf(
match[this.matchMap.periodAbbreviated + 1]
);
}
return {
hour: isNotNil(this.matchMap.hour) ? Number.parseInt(match[this.matchMap.hour + 1], 10) : null,
minute: isNotNil(this.matchMap.minute) ? Number.parseInt(match[this.matchMap.minute + 1], 10) : null,
second: isNotNil(this.matchMap.second) ? Number.parseInt(match[this.matchMap.second + 1], 10) : null,
period
};
} else {
return null;
}
}

genRegexp(): void {
let regexStr = this.format.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$&');
const hourRegex = /h{1,2}/i;
const minuteRegex = /m{1,2}/;
const secondRegex = /s{1,2}/;
const periodNarrow = /aaaaa/;
const periodWide = /aaaa/;
const periodAbbreviated = /a{1,3}/;

const hourMatch = hourRegex.exec(this.format);
const minuteMatch = minuteRegex.exec(this.format);
const secondMatch = secondRegex.exec(this.format);
const periodNarrowMatch = periodNarrow.exec(this.format);
let periodWideMatch: null | RegExpExecArray = null;
let periodAbbreviatedMatch: null | RegExpExecArray = null;
if (!periodNarrowMatch) {
periodWideMatch = periodWide.exec(this.format);
}
if (!periodWideMatch && !periodNarrowMatch) {
periodAbbreviatedMatch = periodAbbreviated.exec(this.format);
}

const matchs = [hourMatch, minuteMatch, secondMatch, periodNarrowMatch, periodWideMatch, periodAbbreviatedMatch]
.filter(m => !!m)
.sort((a, b) => a!.index - b!.index);

matchs.forEach((match, index) => {
switch (match) {
case hourMatch:
this.matchMap.hour = index;
regexStr = regexStr.replace(hourRegex, '(\\d{1,2})');
break;
case minuteMatch:
this.matchMap.minute = index;
regexStr = regexStr.replace(minuteRegex, '(\\d{1,2})');
break;
case secondMatch:
this.matchMap.second = index;
regexStr = regexStr.replace(secondRegex, '(\\d{1,2})');
break;
case periodNarrowMatch:
this.matchMap.periodNarrow = index;
const periodsNarrow = getLocaleDayPeriods(this.localeId, FormStyle.Format, TranslationWidth.Narrow).join('|');
regexStr = regexStr.replace(periodNarrow, `(${periodsNarrow})`);
break;
case periodWideMatch:
this.matchMap.periodWide = index;
const periodsWide = getLocaleDayPeriods(this.localeId, FormStyle.Format, TranslationWidth.Wide).join('|');
regexStr = regexStr.replace(periodWide, `(${periodsWide})`);
break;
case periodAbbreviatedMatch:
this.matchMap.periodAbbreviated = index;
const periodsAbbreviated = getLocaleDayPeriods(this.localeId, FormStyle.Format, TranslationWidth.Abbreviated).join('|');
regexStr = regexStr.replace(periodAbbreviated, `(${periodsAbbreviated})`);
break;
}
});

this.regex = new RegExp(regexStr);
}
}
3 changes: 2 additions & 1 deletion components/date-picker/picker.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '@angular/core';
import { slideMotion } from 'ng-zorro-antd/core/animation';

import { ESCAPE } from '@angular/cdk/keycodes';
import { CandyDate, CompatibleValue } from 'ng-zorro-antd/core/time';
import { NgStyleInterface, NzSafeAny } from 'ng-zorro-antd/core/types';
import { DateHelperService } from 'ng-zorro-antd/i18n';
Expand Down Expand Up @@ -372,7 +373,7 @@ export class NzPickerComponent implements OnInit, AfterViewInit, OnChanges, OnDe
}

onOverlayKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
if (event.keyCode === ESCAPE) {
this.datePickerService.setValue(this.datePickerService.initialValue!);
}
}
Expand Down
4 changes: 2 additions & 2 deletions components/i18n/date-helper.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ describe('DateHelperService', () => {
});

it('should do parseTime correctly', () => {
expect(dateHelper.parseTime('14:00')?.toTimeString().substr(0, 8)).toBe('14:00:00');
expect(dateHelper.parseTime('4:00')?.toTimeString().substr(0, 8)).toBe('04:00:00');
expect(dateHelper.parseTime('14:00', 'HH:mm')?.toTimeString().substr(0, 5)).toBe('14:00');
expect(dateHelper.parseTime('4:00', 'H:mm')?.toTimeString().substr(0, 5)).toBe('04:00');
});
});

Expand Down
12 changes: 5 additions & 7 deletions components/i18n/date-helper.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import fnsFormat from 'date-fns/format';
import fnsGetISOWeek from 'date-fns/getISOWeek';
import fnsParse from 'date-fns/parse';

import { WeekDayIndex } from 'ng-zorro-antd/core/time';
import { WeekDayIndex, ɵNgTimeParser } from 'ng-zorro-antd/core/time';
import { mergeDateConfig, NzDateConfig, NZ_DATE_CONFIG } from './date-config';
import { NzI18nService } from './nz-i18n.service';

Expand All @@ -34,7 +34,7 @@ export abstract class DateHelperService {

abstract getISOWeek(date: Date): number;
abstract getFirstDayOfWeek(): WeekDayIndex;
abstract format(date: Date, formatStr: string): string;
abstract format(date: Date | null, formatStr: string): string;
abstract parseDate(text: string, formatStr?: string): Date;
abstract parseTime(text: string, formatStr?: string): Date | undefined;
}
Expand Down Expand Up @@ -108,10 +108,8 @@ export class DateHelperByDatePipe extends DateHelperService {
return new Date(text);
}

parseTime(text: string): Date | undefined {
if (!text) {
return;
}
return new Date(Date.parse(`1970-01-01 ${text}`));
parseTime(text: string, formatStr: string): Date {
const parser = new ɵNgTimeParser(formatStr, this.i18n.getLocaleId());
return parser.toDate(text);
}
}
8 changes: 6 additions & 2 deletions components/time-picker/demo/use12-hours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Component } from '@angular/core';
@Component({
selector: 'nz-demo-time-picker-use12-hours',
template: `
<nz-time-picker [(ngModel)]="time" [nzUse12Hours]="true"></nz-time-picker>
<nz-time-picker [(ngModel)]="time" [nzUse12Hours]="true" nzFormat="h:mm a"></nz-time-picker>
<nz-time-picker [(ngModel)]="time" [nzUse12Hours]="true" (ngModelChange)="log($event)"></nz-time-picker>
<nz-time-picker [(ngModel)]="time" [nzUse12Hours]="true" (ngModelChange)="log($event)" nzFormat="h:mm a"></nz-time-picker>
`,
styles: [
`
Expand All @@ -16,4 +16,8 @@ import { Component } from '@angular/core';
})
export class NzDemoTimePickerUse12HoursComponent {
time: Date | null = null;

log(value: Date): void {
console.log(value);
}
}
1 change: 0 additions & 1 deletion components/time-picker/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@
export * from './time-picker.component';
export * from './time-picker.module';
export * from './time-picker-panel.component';
export * from './time-value-accessor.directive';
23 changes: 10 additions & 13 deletions components/time-picker/time-picker-panel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ import { InputBoolean, isNotNil } from 'ng-zorro-antd/core/util';
import { DateHelperService } from 'ng-zorro-antd/i18n';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { TimeHolder } from './time-holder';
import { NzTimeValueAccessorDirective } from './time-value-accessor.directive';

function makeRange(length: number, step: number = 1, start: number = 0): number[] {
return new Array(Math.ceil(length / step)).fill(0).map((_, i) => (i + start) * step);
Expand Down Expand Up @@ -109,6 +107,11 @@ export type NzTimePickerUnit = 'hour' | 'minute' | 'second' | '12-hour';
{{ 'Calendar.lang.now' | nzI18n }}
</a>
</li>
<li class="ant-picker-ok">
<button nz-button type="button" nzSize="small" nzType="primary" (click)="onClickOk()">
{{ 'Calendar.lang.ok' | nzI18n }}
</button>
</li>
</ul>
</div>
`,
Expand Down Expand Up @@ -148,8 +151,6 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit,
secondRange!: ReadonlyArray<{ index: number; disabled: boolean }>;
use12HoursRange!: ReadonlyArray<{ index: number; value: string }>;

@ViewChild(NzTimeValueAccessorDirective, { static: false })
nzTimeValueAccessorDirective?: NzTimeValueAccessorDirective;
@ViewChild('hourListElement', { static: false })
hourListElement?: DebugElement;
@ViewChild('minuteListElement', { static: false }) minuteListElement?: DebugElement;
Expand Down Expand Up @@ -277,14 +278,6 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit,
return this._nzSecondStep;
}

selectInputRange(): void {
setTimeout(() => {
if (this.nzTimeValueAccessorDirective) {
this.nzTimeValueAccessorDirective.setRange();
}
});
}

buildHours(): void {
let hourRanges = 24;
let disabledHours = this.nzDisabledHours?.();
Expand Down Expand Up @@ -501,6 +494,10 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit,
this.closePanel.emit();
}

onClickOk(): void {
this.closePanel.emit();
}

isSelectedHour(hour: { index: number; disabled: boolean }): boolean {
return hour.index === this.time.viewHours;
}
Expand All @@ -523,9 +520,9 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit,
this.time.changes.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
this.changed();
this.touched();
this.scrollToTime(120);
});
this.buildTimes();
this.selectInputRange();
setTimeout(() => {
this.scrollToTime();
this.firstScrolled = true;
Expand Down
Loading

0 comments on commit 31ca2da

Please sign in to comment.