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.
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.