Os vários formulários em nossos aplicativos da Web geralmente são criados com os mesmos elementos de bloco. As estruturas de componentes nos ajudam a nos livrar do código repetível e agora quero considerar uma dessas abordagens. Então, como é habitual em Angular.Tarefa técnica:
- você precisa criar o componente "elemento de formulário para inserir SNILS";
- O componente deve formatar os valores inseridos por máscara;
- O componente deve validar a entrada.
- o componente deve funcionar como parte da forma reativa;
- um formulário incompleto deve manter seu estado entre as reinicializações;
- ao carregar a página, uma vez que o formulário editado deve mostrar imediatamente erros;
- usado pelo material angular.
Criando um projeto e instalando dependências
Crie um projeto de teste
ng new input-snils
Instale bibliotecas de terceiros
Primeiro de tudo, o próprio material angularng add @angular/material
Então, precisamos mascarar e verificar os próprios SNILS de acordo com as regras para o cálculo da soma de verificação .Instale as bibliotecas:npm install ngx-mask ru-validation-codes
Salvando dados do formulário
Preparando um Serviço para Trabalhar com LocalStorage
Os dados do formulário serão armazenados no localStorage.Você pode usar imediatamente os métodos do navegador para trabalhar com o LS, mas no Angular é comum tentar escrever código universal e manter todas as dependências externas sob controle. Também simplifica o teste.Portanto, estará correto quando a classe obtiver todas as suas dependências do contêiner de DI. Lembrando o gato Matroskin - para comprar algo desnecessário do injetor, você deve primeiro vender algo desnecessário ao injetor.Crie um provedorwindow.provider.tsimport { InjectionToken } from '@angular/core';
export function getWindow() {
return window;
}
export const WINDOW = new InjectionToken('Window', {
providedIn: 'root',
factory: getWindow,
});
O que está acontecendo aqui? O Injetor DI da Angular se lembra de tokens e distribui entidades que estão associadas a eles. Token - pode ser um objeto InjectionToken, uma string ou uma classe. Isso cria um novo InjectionToken no nível da raiz e se comunica com a fábrica, que retorna o navegador window
.Agora que temos uma janela, vamos criar um serviço simples para trabalhar com LocalStoragestorage.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);
}
}
O StorageService utiliza uma janela de um injetor e fornece seus próprios invólucros para salvar e ler dados. Não tornei o prefixo configurável para não sobrecarregar o artigo com uma descrição de como criar módulos com uma configuração.FormPersistModule
Criamos um serviço simples para salvar dados de formulário.form-persist.service.ts@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 pode registrar formulários com a chave de cadeia passada. O registro significa que os dados do formulário serão salvos no LS a cada alteração.Ao se registrar, o valor extraído do LS também é retornado para que seja possível entender que o formulário já foi salvo anteriormente.Unregister ( unregisterForm
) finaliza o processo de salvamento e exclui a entrada no LS.Gostaria de descrever a funcionalidade de armazenamento declarativamente e não fazê-lo sempre no código do componente. Angular permite fazer milagres com a ajuda de diretivas, e agora é esse o caso.Crie a diretivaform-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();
}
}
}
Quando sobreposto ao formulário, o FormPersistDirective extrai outra diretiva do injetor local - FormGroupDirective e pega o objeto de formulário reativo de lá para se registrar no FormPersistService.A chave de registro deve ser retirada do modelo, o próprio formulário não possui nenhum identificador exclusivo inerente.Ao enviar um formulário, o registro deve ser cancelado. Para fazer isso, ouça o evento de envio usando o HostListener.A diretiva também precisa ser entregue aos componentes onde pode ser usada. É uma boa prática criar pequenos módulos separados para cada entidade reutilizada.form-persist.module.ts@NgModule({
declarations: [FormPersistDirective],
exports: [FormPersistDirective]
})
export class FormPersistModule { }
Elemento do formulário SNILS
Que tarefas são atribuídas a ele?Primeiro de tudo, ele deve validar os dados.snilsValidator
Angular permite que você anexe seus validadores para formar controles, e é hora de criar seus próprios. Para verificar o SNILS, eu uso a biblioteca externa de códigos de validação de ru e o validador será bastante simples.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' };
}
Component InputSnilsComponent
O modelo do componente consiste em um campo de entrada agrupado, uma versão clássica da biblioteca de material angular.Com uma pequena adição, uma máscara de entrada será sobreposta na entrada usando a biblioteca externa ngx-mask, a partir daqui os parâmetros de entrada mask - define a máscara e dropSpecialCharacters - desativa a remoção de caracteres especiais da máscara do valor.Consulte a documentação da ngx-mask para obter mais detalhes.Aquiestá o modelo do componenteinput-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>
A questão é: qual é esse formControl | snilsErrors? Este é um canal personalizado para exibir erros, agora vamos criá-lo.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;
}
}
}
O tubo não está limpo, o que significa que será executado sempre que forem detectadas alterações.O pipe aceita um parâmetro do tipo de erro e detecta erros de três tipos:- “Obrigatório” - esse erro é da diretiva Angular RequiredValidator interno
- "Snils" - esse erro é do nosso validador snilsValidator
- "Erro de máscara" - esse erro da diretiva MaskDirective da biblioteca ngx-mask
E ele retorna um valor booleano - existe um erro ou não.Agora, vejamos o próprio código do componenteinput-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 });
}
}
Há muitas coisas e não descreverei como trabalhar com o ControlValueAccessor. Você pode ler sobre isso na documentação do Angular ou, por exemplo, aqui: tyapk.ru/blog/post/angular-custom-form-field-controlO que precisa de explicação aqui?Primeiro, usamos o controle interno do formulário formControl, anexamos às suas alterações para enviar a alteração de valor, através dos métodos onChange e onTouched.E vice-versa, as alterações no formulário externo chegam até nós através dos métodos writeValue e setDisabledState e são refletidas no formControl.Em segundo lugar, há um token STATE_VALUE_ACCESSOR desconhecido, uma interface StateValueAccessor desconhecida e alguns métodos extras setPristineState e setTouchedState. Eles serão explicados mais tarde.Enquanto isso, crie um módulo pessoal para o componenteinput-snils.module.ts@NgModule({
declarations: [InputSnilsComponent, SnilsErrorsPipe],
imports: [
CommonModule,
MatFormFieldModule,
MatInputModule,
NgxMaskModule.forChild(),
ReactiveFormsModule,
],
exports: [InputSnilsComponent],
})
export class InputSnilsModule { }
Passando status para um item
Ao usar o ControlValueAccessor, há as seguintes nuances: Aforma reativa tocou nos estados primitivos (a seguir, simplesmente "estados").- pristine é true inicialmente e muda para false quando o valor de controle é alterado do modelo
- tocou inicialmente false e muda para true quando o controle perdeu o foco
Eles também podem ser definidos à força, mas isso não afetará o controle dentro do ControlValueAccessor, pois nosso componente é formControl.E erros de erro de esteira são renderizados somente quando o controle atual é tocado. Temos o requisito de que o formulário restaurado exiba imediatamente erros de validação; portanto, FormPersistDirective executa markAllAsTouched se o valor do formulário tiver sido lido em localStorage. Mas erros mat não serão exibidos, pois estão dentro do nosso componente ControlValueAccessor, eles dependem do controle formControl e, nesse controle independente, o estado tocado ainda é falso.Precisamos de um mecanismo para lançar esses estados. Para fazer isso, você pode criar seu próprio análogo do ControlValueAccessor, vamos chamá-lo StateValueAccessor.Primeiro, você precisa criar um token e uma interface.state-value-accessor.token.tsexport const STATE_VALUE_ACCESSOR = new InjectionToken<StateValueAccessor>('STATE_VALUE_ACCESSOR');
state-value-accessor.interface.tsexport interface StateValueAccessor {
setTouchedState?(touched: boolean): void;
setPristineState?(pristine: boolean): void;
}
A interface descreve os requisitos para a classe que a implementa ter (opcionalmente) dois métodos especificados. Esses métodos são implementados no InputSnilsComponent e forçam esses estados no controle interno formControl.Então você precisa de uma diretiva para vincular NgControl e nosso componente que implementa StateValueAccessor. É impossível identificar o momento em que o estado de um formulário é alterado, mas sabemos que sempre que um formulário é alterado, o Angular marca o componente como aguardando um ciclo de detecção de alterações. O componente testado e seus descendentes executam o gancho de ciclo de vida ngDoCheck, que nossa diretiva usará.FormStatusesDirective
Crie a diretivaform-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 é sobreposto a todos os controles e verificações possíveis quanto à presença de StateValueAccessor. Para fazer isso, uma dependência opcional no token STATE_VALUE_ACCESSOR é solicitada ao injetor, que o componente que implementa StateValueAccessor deve ter verificado.Se nada for encontrado pelo token, nada acontecerá, os métodos setSVATouched e setSVAPristine serão apenas funções vazias.Se StateValueAccessor for encontrado, seus métodos setTouchedState e setPristineState serão chamados para cada alteração de estado detectada.Resta fornecer à diretiva um módulo para exportarform-statuses.module.ts@NgModule({
declarations: [FormStatusesDirective],
exports: [FormStatusesDirective]
})
export class FormStatusesModule { }
Página principal
Agora você precisa criar o próprio formulário. Para não fazer barulho, coloque-o na página principal do AppComponent. Obviamente, em um aplicativo real, é melhor criar um componente separado para o formulário. App.component.htmlmodelo<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>
A diretiva FormPersistDirective trava no formulário, Angular descobre através do seletor de formulário [formPersist].O modelo precisa ser fornecido com variáveis, vamos fazê-loapp.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])
});
}
O código do componente com o formulário saiu extremamente simples e não contém nada de supérfluo.É assim:
O código-fonte pode ser obtido no GitHubA demonstração no stackblitzO código no stackblitz é um pouco diferente, devido ao fato de a versão datilografada ainda não suportar o operador elvis.Além disso, ainda existem nuances que não estão refletidas no artigo, se alguém precisar, eu o complementarei.