我们的Web应用程序中的各种形式通常是使用相同的Brick元素构建的。组件框架可帮助我们摆脱可重复的代码,现在我想考虑这些方法之一。因此,按照Angular的惯例。技术任务:
- 您需要创建组件“用于输入SNILS的表单元素”;
- 该组件应通过掩码格式化输入的值;
- 组件必须验证输入。
- 该组件应作为反应形式的一部分;
- 不完整的表格必须在两次重启之间保持其状态;
- 加载页面时,一旦编辑后的表单应立即显示错误;
- 由Angular Material使用。
创建项目并安装依赖项
创建一个测试项目
ng new input-snils
安装第三方库
首先,角材料本身ng add @angular/material
然后我们需要根据计算校验和的规则对 SNILS本身进行屏蔽和检查。安装库:npm install ngx-mask ru-validation-codes
保存表格数据
准备与localStorage一起使用的服务
表单数据将存储在localStorage中。您可以立即采用浏览器方法来处理LS,但是在Angular中,习惯于尝试编写通用代码,并使所有外部依赖项都受到控制。它还简化了测试。因此,当类从DI容器获取所有依赖项时,它将是正确的。记住猫Matroskin-为了从喷油器购买不必要的东西,您必须首先向喷油器出售不必要的东西。创建一个window.provider.ts提供者import { InjectionToken } from '@angular/core';
export function getWindow() {
return window;
}
export const WINDOW = new InjectionToken('Window', {
providedIn: 'root',
factory: getWindow,
});
这里发生了什么?Angular的DI注入器会记住令牌并赠送与它们关联的实体。令牌-它可以是InjectionToken对象,字符串或类。这将创建一个新的根级别的InjectionToken并与工厂通信,然后返回浏览器window
。现在我们有了一个窗口,让我们创建一个用于使用LocalStorage storage.service.ts的简单服务@Injectable({
providedIn: 'root'
})
export class StorageService {
readonly prefix = 'snils-input__';
constructor(
@Inject(WINDOW) private window: Window,
) {}
public set<T>(key: string, data: T): void {
this.window.localStorage.setItem(this.prefix + key, JSON.stringify(data));
}
public get<T>(key: string): T {
try {
return JSON.parse(this.window.localStorage.getItem(this.prefix + key));
} catch (e) { }
}
public remove(key: string): void {
this.window.localStorage.removeItem(this.prefix + key);
}
}
StorageService从注入器获取一个窗口,并提供自己的包装器以保存和读取数据。我没有将前缀配置为可配置,以免使本文过多地介绍如何使用配置创建模块。表单持久性模块
我们创建了一个用于保存表单数据的简单服务。形式持续服务@Injectable({
providedIn: 'root'
})
export class FormPersistService {
private subscriptions: Record<string, Subscription> = {};
constructor(
private storageService: StorageService,
) { }
public registerForm<T>(formName: string, form: AbstractControl): T {
this.subscriptions[formName]?.unsubscribe();
this.subscriptions[formName] = this.createFormSubscription(formName, form);
return this.restoreData(formName, form);
}
public unregisterForm(formName: string): void {
this.storageService.remove(formName);
this.subscriptions[formName]?.unsubscribe();
delete this.subscriptions[formName];
}
public restoreData<T>(formName: string, form: AbstractControl): T {
const data = this.storageService.get(formName) as T;
if (data) {
form.patchValue(data, { emitEvent: false });
}
return data;
}
private createFormSubscription(formName: string, form: AbstractControl): Subscription {
return form.valueChanges.pipe(
debounceTime(500),
)
.subscribe(value => {
this.storageService.set(formName, value);
});
}
}
FormPersistService能够使用传递的字符串键注册表单。注册意味着表格数据每次更改都会保存在LS中。注册时,还将返回从LS提取的值,以便可以理解该表单已被更早保存。取消注册(unregisterForm
)会终止保存过程并删除LS中的条目。我想声明性地描述存储功能,而不是每次在组件代码中都这样做。 Angular让您借助指令来创造奇迹,现在就是这种情况。创建form-persist.directive.ts指令@Directive({
selector: 'form[formPersist]',
})
export class FormPersistDirective implements OnInit {
@Input() formPersist: string;
constructor(
private formPersistService: FormPersistService,
@Self() private formGroup: FormGroupDirective,
) { }
@HostListener('submit')
onSubmit() {
this.formPersistService.unregisterForm(this.formPersist);
}
ngOnInit() {
const savedValue = this.formPersistService.registerForm(this.formPersist, this.formGroup.control);
if (savedValue) {
this.formGroup.control.markAllAsTouched();
}
}
}
当叠加在表单上时,FormPersistDirective从本地注入器中提取另一个指令-FormGroupDirective,并从那里获取反应性表单对象以向FormPersistService注册。注册密钥必须从模板中获取,表单本身没有固有的唯一标识符。提交表格时,应取消注册。为此,请使用HostListener监听Submit事件。该指令还需要传递到可以使用它的组件。优良作法是为每个重用实体创建单独的小模块。form-persist.module.ts@NgModule({
declarations: [FormPersistDirective],
exports: [FormPersistDirective]
})
export class FormPersistModule { }
SNILS表单的元素
分配了哪些任务?首先,它必须验证数据。snilsValidator
Angular允许您将验证器附加到表单控件,是时候制作自己的了。要检查SNILS,我使用外部ru-validation-codes库,验证器将非常简单。snils.validator.tsimport { checkSnils } from 'ru-validation-codes';
export function snilsValidator(control: AbstractControl): ValidationErrors | null {
if (control.value === '' || control.value === null) {
return null;
}
return checkSnils(control.value)
? null
: { snils: 'error' };
}
组件InputSnilsComponent
组件模板包含一个包装的输入字段,这是Angular Material库中的经典版本。加上一点点,就可以使用外部库ngx-mask将输入掩码叠加在输入上,在这里,输入参数mask-设置mask和dropSpecialCharacters-关闭从值中除去特殊掩码字符的功能。有关更多详细信息,请参见ngx-mask文档。这是input-snils.component.html组件模板<mat-form-field appearance="outline">
<input
matInput
autocomplete="snils"
[formControl]="formControl"
[mask]="mask"
[dropSpecialCharacters]="false"
[placeholder]="placeholder"
[readonly]="readonly"
[required]="required"
[tabIndex]="tabIndex"
>
<mat-error [hidden]="formControl | snilsErrors: 'required'"> </mat-error>
<mat-error [hidden]="formControl | snilsErrors: 'format'"> </mat-error>
<mat-error [hidden]="formControl | snilsErrors: 'snils'"> </mat-error>
</mat-form-field>
问题是,这个formControl是什么?snilsErrors?这是一个用于显示错误的自定义管道,现在我们将创建它。snils-errors.pipe.tstype ErrorType = 'required' | 'format' | 'snils';
@Pipe({
name: 'snilsErrors',
pure: false,
})
export class SnilsErrorsPipe implements PipeTransform {
transform(control: AbstractControl, errorrType: ErrorType): boolean {
switch (errorrType) {
case 'required': return !control.hasError('required');
case 'format': return !control.hasError('Mask error');
case 'snils': return control.hasError('Mask error') || !control.hasError('snils');
default: return false;
}
}
}
管道不干净,这意味着它将在每次检测到更改时执行。管道接受错误类型参数,并检测三种类型的错误:- “必需”-此错误来自内置的Angular指令RequiredValidator
- “ Snils”-此错误来自我们的验证器snilsValidator
- “掩码错误”-来自ngx-mask库的MaskDirective指令的此错误
它返回一个布尔值-是否存在这样的错误。现在,让我们看一下input-snils.component.ts组件代码本身@Component({
selector: 'app-input-snils',
templateUrl: './input-snils.component.html',
styleUrls: ['./input-snils.component.css'],
encapsulation: ViewEncapsulation.None,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputSnilsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => InputSnilsComponent),
multi: true,
},
{
provide: STATE_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputSnilsComponent),
},
]
})
export class InputSnilsComponent implements OnInit, ControlValueAccessor, StateValueAccessor, OnDestroy {
public mask = '000-000-000 00';
public formControl = new FormControl('', [snilsValidator]);
private sub = new Subscription();
@Input() readonly: boolean;
@Input() placeholder = '';
@Input() tabIndex = 0;
@Input() required: boolean;
private onChange = (value: any) => { };
private onTouched = () => { };
registerOnChange = (fn: (value: any) => {}) => this.onChange = fn;
registerOnTouched = (fn: () => {}) => this.onTouched = fn;
ngOnInit() {
this.sub = this.linkForm();
}
ngOnDestroy() {
this.sub.unsubscribe();
}
private linkForm(): Subscription {
return this.formControl.valueChanges.subscribe(value => {
this.onTouched();
this.onChange(value);
});
}
writeValue(outsideValue: string): void {
if (outsideValue) {
this.onTouched();
}
this.formControl.setValue(outsideValue, { emitEvent: false });
}
setDisabledState(disabled: boolean) {
disabled
? this.formControl.disable()
: this.formControl.enable();
}
validate(): ValidationErrors | null {
return this.formControl.errors;
}
setPristineState(pristine: boolean) {
pristine
? this.formControl.markAsPristine()
: this.formControl.markAsDirty();
this.formControl.updateValueAndValidity({ emitEvent: false });
}
setTouchedState(touched: boolean) {
touched
? this.formControl.markAsTouched()
: this.formControl.markAsUntouched();
this.formControl.updateValueAndValidity({ emitEvent: false });
}
}
有很多东西,我将不介绍如何使用ControlValueAccessor,您可以在Angular文档中阅读有关此内容的信息,例如,在这里tyapk.ru/blog/post/angular-custom-form-field-control这里需要解释什么?首先,我们使用formControl形式的内部控件,将其附加到其更改,以通过onChange和onTouched方法将值更改发送到顶部。反之亦然,外部形式的更改通过writeValue和setDisabledState方法传递给我们,并反映在formControl中。其次,有一个未知的STATE_VALUE_ACCESSOR令牌,一个未知的StateValueAccessor接口以及两个额外的setPristineState和setTouchedState方法。它们将在后面说明。同时,为组件创建一个个人模块input-snils.module.ts@NgModule({
declarations: [InputSnilsComponent, SnilsErrorsPipe],
imports: [
CommonModule,
MatFormFieldModule,
MatInputModule,
NgxMaskModule.forChild(),
ReactiveFormsModule,
],
exports: [InputSnilsComponent],
})
export class InputSnilsModule { }
将状态传递给项目
使用ControlValueAccessor时,存在以下细微差别:反应形式具有触摸状态和原始状态(以下简称为“状态”)。- pristine最初为true,从模板更改控制值后变为false
- 当控件失去焦点时,最初变为false,然后变为true
也可以强制设置它们,但这不会影响ControlValueAccessor内部的控件,因为我们的组件是formControl。而且只有在触摸当前控件时才会出现垫错误错误。我们要求还原的表单立即显示验证错误,因此,如果已从localStorage读取表单值,则FormPersistDirective将执行markAllAsTouched。但是不会显示垫错误,因为它们在我们的ControlValueAccessor组件内部,它们依赖于formControl控件,并且在此独立控件上,触摸状态仍然为false。我们需要一种引发这些状态的机制。为此,您可以创建自己的ControlValueAccessor类似物,我们将其称为StateValueAccessor。首先,您需要创建一个令牌和接口。状态值访问器。令牌export const STATE_VALUE_ACCESSOR = new InjectionToken<StateValueAccessor>('STATE_VALUE_ACCESSOR');
状态值访问器.interface.tsexport interface StateValueAccessor {
setTouchedState?(touched: boolean): void;
setPristineState?(pristine: boolean): void;
}
该接口描述了实现它的类具有(可选)两个指定方法的要求。这些方法在InputSnilsComponent中实现,并在内部控件formControl上强制这些状态。然后,您需要一个指令来绑定NgControl和实现StateValueAccessor的组件。查明表单状态变化的时刻是不可能的,但是我们知道,只要表单发生变化,Angular就会将组件标记为等待更改检测周期。被测试的组件及其后代执行ngDoCheck生命周期挂钩,我们的指令将使用该挂钩。FormStatuses指令
创建指令form-statuses.directive.tsconst noop: (v?: boolean) => void = () => { };
@Directive({
selector: '[formControlName],[ngModel],[formControl]'
})
export class FormStatusesDirective implements DoCheck, OnInit {
private setSVATouched = noop;
private setSVAPristine = noop;
constructor(
@Self() private control: NgControl,
@Self() @Optional() @Inject(STATE_VALUE_ACCESSOR) private stateValueAccessor: StateValueAccessor,
) { }
ngOnInit() {
if (this.stateValueAccessor?.setTouchedState) {
this.setSVATouched = wrapIfChanges(touched => this.stateValueAccessor.setTouchedState(touched));
}
if (this.stateValueAccessor?.setPristineState) {
this.setSVAPristine = wrapIfChanges(pristine => this.stateValueAccessor.setPristineState(pristine));
}
}
ngDoCheck() {
this.setSVAPristine(this.control.pristine);
this.setSVATouched(this.control.touched);
}
}
FormStatusesDirective叠加在所有可能的控件上,并检查StateValueAccessor的存在。为此,从注入器请求对STATE_VALUE_ACCESSOR令牌的可选依赖关系,实现StateValueAccessor的组件应该已经检查了依赖关系。如果令牌没有发现任何东西,那么什么也不会发生,则setSVATouched和setSVAPristine方法将只是空函数。如果找到了StateValueAccessor,则将为每个检测到的状态更改调用其setTouchedState和setPristineState方法。仍然需要为该指令提供用于导出form-statuses.module.ts的模块@NgModule({
declarations: [FormStatusesDirective],
exports: [FormStatusesDirective]
})
export class FormStatusesModule { }
主页
现在,您需要创建表单本身。为了避免大惊小怪,请将其放在AppComponent的主页上。当然,在实际应用中,最好为表单创建一个单独的组件。App.component.html模板<section class="form-wrapper">
<form
class="form"
[formGroup]="form"
formPersist="inputSnils"
>
<app-input-snils
class="input-snils"
formControlName="snils"
[required]="true"
></app-input-snils>
<button
class="ready-button"
mat-raised-button
[disabled]="form.invalid"
type="submit"
>
Submit
</button>
</form>
</section>
FormPersistDirective指令挂在表单上,Angular通过表单[formPersist]选择器了解它。模板需要提供变量,让我们执行app.component.ts@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public form = new FormGroup({
snils: new FormControl('', [Validators.required])
});
}
带有形式的组件代码非常简单,并且不包含任何多余的内容。看起来像这样:
源代码可以在GitHub上获取stackblitz 上的演示stackblitz上的代码略有不同,因为那里的打字稿版本尚不支持elvis运算符。此外,本文中仍然存在一些细微差别,如果有人需要,我将对其进行补充。