Skip to content

Commit

Permalink
feat(module:cron-expression): add cron-expression component (#7677)
Browse files Browse the repository at this point in the history
* feat(module:cron-expression): add cron-expression component

* feat(module:cron-expression): add cron-expression component i18n

* feat(module:cron-expression): modify cron-expression i18.interface

* feat(module:cron-expression): detail modification

* feat(module:cron-expression): cron-expression support spring

* feat(module:cron-expression): adjust the layout
  • Loading branch information
OriginRing committed Nov 7, 2022
1 parent e3103f0 commit 3a638af
Show file tree
Hide file tree
Showing 30 changed files with 954 additions and 0 deletions.
1 change: 1 addition & 0 deletions components/components.less
Expand Up @@ -62,3 +62,4 @@
@import './result/style/entry.less';
@import './space/style/entry.less';
@import './image/style/entry.less';
@import './cron-expression/style/entry.less';
49 changes: 49 additions & 0 deletions components/cron-expression/cron-expression-input.component.ts
@@ -0,0 +1,49 @@
/**
* 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
*/

import { Component, ViewEncapsulation, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core';

import { CronChangeType, TimeType } from './typings';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
selector: 'nz-cron-expression-input',
exportAs: 'nzCronExpression',
template: `
<div class="ant-cron-expression-input">
<input
nz-input
[(ngModel)]="value"
[name]="label"
(focus)="focusInputEffect($event)"
(blur)="blurInputEffect()"
(ngModelChange)="setValue()"
/>
</div>
`
})
export class NzCronExpressionInputComponent {
@Input() value: string = '0';
@Input() label: TimeType = 'second';
@Output() readonly focusEffect = new EventEmitter<TimeType>();
@Output() readonly blurEffect = new EventEmitter<void>();
@Output() readonly getValue = new EventEmitter<CronChangeType>();

constructor() {}

focusInputEffect(event: FocusEvent): void {
this.focusEffect.emit(this.label);
(event.target as HTMLInputElement).select();
}

blurInputEffect(): void {
this.blurEffect.emit();
}

setValue(): void {
this.getValue.emit({ label: this.label, value: this.value });
}
}
44 changes: 44 additions & 0 deletions components/cron-expression/cron-expression-label.component.ts
@@ -0,0 +1,44 @@
/**
* 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
*/

import { Component, ViewEncapsulation, ChangeDetectionStrategy, Input, OnInit } from '@angular/core';

import { NzCronExpressionLabelI18n } from 'ng-zorro-antd/i18n';

import { TimeType, TimeTypeError } from './typings';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
selector: 'nz-cron-expression-label',
exportAs: 'nzCronExpression',
template: `
<div
class="ant-cron-expression-label"
[class.ant-cron-expression-label-foucs]="labelFocus === type"
[class.ant-cron-expression-error]="!valid"
>
<label nz-tooltip [nzTooltipTitle]="error" [nzTooltipVisible]="!valid" nzTooltipPlacement="bottom">
{{ locale[type] }}
</label>
</div>
<ng-template #error>
<div class="ant-cron-expression-hint" [innerHTML]="locale[labelError]"></div>
</ng-template>
`
})
export class NzCronExpressionLabelComponent implements OnInit {
@Input() type: TimeType = 'second';
@Input() valid: boolean = true;
@Input() locale!: NzCronExpressionLabelI18n;
@Input() labelFocus: string | null = null;
labelError: TimeTypeError = 'secondError';

constructor() {}

ngOnInit(): void {
this.labelError = `${this.type}Error`;
}
}
267 changes: 267 additions & 0 deletions components/cron-expression/cron-expression.component.ts
@@ -0,0 +1,267 @@
/**
* 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
*/

import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnDestroy,
OnInit,
TemplateRef,
ViewEncapsulation
} from '@angular/core';
import {
AsyncValidator,
ControlValueAccessor,
FormControl,
NG_ASYNC_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validators
} from '@angular/forms';
import { Observable, of, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { CronExpression, parseExpression } from 'cron-parser';

import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { InputBoolean } from 'ng-zorro-antd/core/util';
import { NzCronExpressionI18nInterface, NzI18nService } from 'ng-zorro-antd/i18n';

import { CronChangeType, CronType, NzCronExpressionSize, TimeType } from './typings';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
selector: 'nz-cron-expression',
exportAs: 'nzCronExpression',
template: `
<div class="ant-cron-expression">
<div class="ant-cron-expression-content">
<div
class="ant-cron-expression-input-group"
[class.ant-cron-expression-input-group-lg]="nzSize === 'large'"
[class.ant-cron-expression-input-group-sm]="nzSize === 'small'"
[class.ant-cron-expression-input-group-focus]="focus"
[class.ant-cron-expression-input-group-error]="!validateForm.valid"
[class.ant-cron-expression-input-group-error-focus]="!validateForm.valid && focus"
>
<ng-container *ngFor="let label of labels">
<nz-cron-expression-input
[value]="this.validateForm.controls[label].value"
[label]="label"
(focusEffect)="focusEffect($event)"
(blurEffect)="blurEffect()"
(getValue)="getValue($event)"
></nz-cron-expression-input>
</ng-container>
</div>
<div class="ant-cron-expression-label-group">
<ng-container *ngFor="let label of labels">
<nz-cron-expression-label
[type]="label"
[valid]="this.validateForm.controls[label].valid"
[labelFocus]="labelFocus"
[locale]="locale"
></nz-cron-expression-label>
</ng-container>
</div>
<nz-collapse *ngIf="!nzCollapseDisable" [nzBordered]="false">
<nz-collapse-panel [nzHeader]="nextDate">
<ng-container *ngIf="validateForm.valid">
<ul class="ant-cron-expression-preview-date">
<li style="margin: 0" *ngFor="let dateItem of nextTimeList">
{{ dateItem | date: 'YYYY-MM-dd HH:mm:ss' }}
</li>
<li><a (click)="loadMorePreview()">···</a></li>
</ul>
</ng-container>
<ng-container *ngIf="!validateForm.valid">{{ locale.cronError }}</ng-container>
</nz-collapse-panel>
</nz-collapse>
</div>
<div class="ant-cron-expression-map" *ngIf="nzExtra">
<ng-template [ngTemplateOutlet]="nzExtra"></ng-template>
</div>
<ng-template #nextDate>
<ng-container *ngIf="validateForm.valid">
{{ dateTime | date: 'YYYY-MM-dd HH:mm:ss' }}
</ng-container>
<ng-container *ngIf="!validateForm.valid">{{ locale.cronError }}</ng-container>
</ng-template>
</div>
`,
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => NzCronExpressionComponent),
multi: true
},
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NzCronExpressionComponent),
multi: true
}
]
})
export class NzCronExpressionComponent implements OnInit, ControlValueAccessor, AsyncValidator, OnDestroy {
@Input() nzSize: NzCronExpressionSize = 'default';
@Input() nzType: 'linux' | 'spring' = 'linux';
@Input() @InputBoolean() nzCollapseDisable: boolean = false;
@Input() nzExtra?: TemplateRef<void> | null = null;

locale!: NzCronExpressionI18nInterface;
focus: boolean = false;
labelFocus: TimeType | null = null;
validLabel: string | null = null;
labels: TimeType[] = [];
interval!: CronExpression<false>;
nextTimeList: Date[] = [];
dateTime: Date = new Date();
private destroy$ = new Subject<void>();

validateForm!: UntypedFormGroup;

onChange: NzSafeAny = () => {};
onTouch: () => void = () => null;

convertFormat(value: string): void {
const values = value.split(' ');
const valueObject: CronType = {};
this.labels.map((a, b) => {
valueObject[a] = values[b];
});
this.validateForm.patchValue(valueObject);
}

writeValue(value: string | null): void {
if (value) {
this.convertFormat(value);
}
}

registerOnChange(fn: NzSafeAny): void {
this.onChange = fn;
}

registerOnTouched(fn: NzSafeAny): void {
this.onTouch = fn;
}

validate(): Observable<ValidationErrors | null> {
if (this.validateForm.valid) {
return of(null);
} else {
return of({ error: true });
}
}

constructor(private formBuilder: UntypedFormBuilder, private cdr: ChangeDetectorRef, private i18n: NzI18nService) {}

ngOnInit(): void {
if (this.nzType === 'spring') {
this.labels = ['second', 'minute', 'hour', 'day', 'month', 'week'];
this.validateForm = this.formBuilder.group({
second: ['0', Validators.required, this.checkValid],
minute: ['*', Validators.required, this.checkValid],
hour: ['*', Validators.required, this.checkValid],
day: ['*', Validators.required, this.checkValid],
month: ['*', Validators.required, this.checkValid],
week: ['*', Validators.required, this.checkValid]
});
} else {
this.labels = ['minute', 'hour', 'day', 'month', 'week'];
this.validateForm = this.formBuilder.group({
minute: ['*', Validators.required, this.checkValid],
hour: ['*', Validators.required, this.checkValid],
day: ['*', Validators.required, this.checkValid],
month: ['*', Validators.required, this.checkValid],
week: ['*', Validators.required, this.checkValid]
});
}
this.i18n.localeChange.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.locale = this.i18n.getLocaleData('CronExpression');
this.cdr.markForCheck();
});

this.previewDate(this.validateForm.value);

this.validateForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
this.onChange(Object.values(value).join(' '));
this.previewDate(value);
this.cdr.markForCheck();
});
}

previewDate(value: CronType): void {
try {
this.interval = parseExpression(Object.values(value).join(' '));
this.dateTime = this.interval.next().toDate();
this.nextTimeList = [
this.interval.next().toDate(),
this.interval.next().toDate(),
this.interval.next().toDate(),
this.interval.next().toDate(),
this.interval.next().toDate()
];
} catch (err: NzSafeAny) {
return;
}
}

loadMorePreview(): void {
this.nextTimeList = [
...this.nextTimeList,
this.interval.next().toDate(),
this.interval.next().toDate(),
this.interval.next().toDate(),
this.interval.next().toDate(),
this.interval.next().toDate()
];
this.cdr.markForCheck();
}

focusEffect(value: TimeType): void {
this.focus = true;
this.labelFocus = value;
this.cdr.markForCheck();
}

blurEffect(): void {
this.focus = false;
this.labelFocus = null;
this.cdr.markForCheck();
}

getValue(item: CronChangeType): void {
this.validLabel = item.label;
this.validateForm.controls[item.label].patchValue(item.value);
this.cdr.markForCheck();
}

checkValid = (control: FormControl): Observable<ValidationErrors | null> => {
if (control.value) {
try {
const cron: string[] = [];
this.labels.forEach(label => {
label === this.validLabel ? cron.push(control.value) : cron.push('*');
});
parseExpression(cron.join(' '));
} catch (err: unknown) {
return of({ error: true });
}
}
return of(null);
};

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

0 comments on commit 3a638af

Please sign in to comment.