Boa tarde
Hoje eu quero falar sobre um novo pacote para validações assíncronas em projetos baseados em
React ,
Mobx e escritos em
Typescript .
O desenvolvimento do Frontend moderno envolve uma grande quantidade de lógica ao preencher páginas com documentos, questionários e documentos para um empréstimo, ordens de pagamento, páginas de registro no site. O principal ônus lógico recai sobre as verificações de validação.
Os desenvolvedores
angulares pensaram sobre esse ponto e ofereceram aos desenvolvedores o uso do mecanismo
FormControl interno para esses fins, que, embora tenha várias desvantagens, ainda é melhor do que a ausência completa de uma solução no
React . A situação é complicada pelo fato de que a tendência atual do desenvolvimento do
React envolve o uso de mobx para organizar a lógica de negócios.
Diante desses problemas, resolvemos todos eles escrevendo um pacote:
@ quantumart / mobx-form-validation-kitVantagens do pacote:
- Inteiramente no TypeScript
- Compatível com Mobx (versão 4, que suporta todos os favoritos, IE10)
- Projetado para funcionar em React (pode ser usado em projetos sem reagir)
- Projetado para validação assíncrona
- Fácil de incorporar em um projeto existente.
Instruções para trabalhar com a embalagem sob o corte.
No início, descreveremos a funcionalidade do
pacote @ quantumart / mobx-form-validation-kit , no final do artigo, escreveremos uma página totalmente funcional com um exemplo do formulário de inscrição no site.
Formcontrol
O @ quantumart / mobx-form-validation-kit permite criar uma camada entre os dados de origem e o formulário a ser exibido. O que, por sua vez, permite validá-los e, se necessário, alterar os dados antes que eles cheguem ao objeto original.

A
biblioteca @ quantumart / mobx-form-validation-kit contém três classes principais (componentes de validação) para gerenciar o formulário:
- FormGroup - permite combinar componentes de validação. A classe é digitada e permite refazer a interface com a lista de campos como um parâmetro genérico. Qualquer um é registrado por padrão, é altamente recomendável não usá-lo sem digitar, mas existe uma possibilidade.
- FormControl - usado para validar um campo específico, a classe mais usada. A classe é digitada e assume como parâmetro genérico o tipo da variável que deve armazenar. Por padrão, a string é registrada, porque o padrão é string, como a opção mais privada para formulários.
- FormArray - permite criar e gerenciar uma matriz de componentes de validação.
Além disso, existem classes abstratas básicas
- AbstractControl - a classe base para todas as classes de validação listadas, não digitadas.
- FormAbstractControl - classe base para FormGroup e FormArray , não digitado.
- FormAbstractGroup - não uma classe base digitada para FormControl, contém um link para o elemento html que está sendo renderizado.
A melhor prática para criar um formulário de validação seria a seguinte idéia.
Um objeto do tipo um
FormGroup é criado no formulário e os campos já estão listados nele
this.form = new FormGroup<IUserInfo>({ name: new FormControl( this.userInfo.name, [], v => (this.userInfo.name = v) ), surname: new FormControl( this.userInfo.surname, [], v => (this.userInfo.surname = v) )
O FormGroup suporta aninhamento, ou seja,
this.form = new FormGroup<IUserInfo>({ name: new FormControl( this.userInfo.name, [], v => (this.userInfo.name = v) ), surname: new FormControl( this.userInfo.surname, [], v => (this.userInfo.surname = v) ) passport: new FormGroup<IPassport >({ number: new FormControl( this.userInfo.passport.number, [], v => (this.userInfo.passport.number = v) ),
Você pode adicionar o
FormArray , que, por sua vez, pode passar o tipo
FormControl e / ou o
FormGroup inteiro, criando objetos de qualquer complexidade e estrutura.
- FormArray <FormControl>
FormArray <FormGroup>
O próprio FormControl leva o seguinte conjunto de parâmetros para o construtor
- value : TEntity é o valor inicial digitado.
- validators : ValidatorFunctionFormControlHandler [] - um conjunto de validadores.
- callbackValidValue : UpdateValidValueHandler | null - função de retorno de chamada para a qual o último valor válido é passado. É chamado toda vez que um valor no FormControl é alterado e esse valor passa nas validações descritas.
- ativar : (() => booleano) | null - a função ativará / desativará as validações por condição (sempre ativada por padrão). Por exemplo, a validade da data de término do serviço não precisa ser verificada se a caixa de seleção "Ilimitado" não estiver marcada. Como resultado, basta digitar aqui uma função que, ao marcar o estado do campo observável responsável pela caixa de seleção "Ilimitado", você pode desativar automaticamente todas as validações associadas ao campo para verificar a data, em vez de registrar essa lógica em cada uma das validações de campo de data.
- additionalData : TAdditionalData | null - um bloco com informações adicionais permite adicionar informações adicionais a um FormControl específico e usá-las posteriormente, por exemplo, para visualização. Isso é conveniente se houver construtores para o FormControl nos quais você precise analisar determinadas informações e não passar essas informações por meio de um pacote de dados complexo para os controles de visualização. Embora eu não possa fornecer um cenário de aplicativo exato e inegável, é melhor ter essa oportunidade do que sofrer sem ela.
Há uma limitação que o Angular FormControl também possui: não há necessidade de reutilizar objetos em diferentes formas. I.e. Você pode criar um construtor FormGroup e criar seu próprio objeto em cada página. Mas usar um objeto por grupo de páginas é uma prática ruim.
Além disso, o FormControl é inicializado com um único valor e, se esse valor for alterado, o novo valor não entrará no FormControl . Isso é feito de propósito, porque, como a prática mostrou, por algum motivo, todo mundo está tentando teimosamente editar inicialmente o objeto original ignorando validações, e não o valor no FormControl . Basta atribuir um novo valor ao campo de valor do FormControl para modificar o objeto original.
O FormGroup aceita o seguinte conjunto de parâmetros no construtor:
- Controles : TControls - um objeto herdado de AbstractControls . De fato, basta criar uma interface herdada de AbstractControls na qual você enumera campos do tipo FormGroup , FormControl , FormArray . É claro que você pode definir o tipo como any , mas todas as vantagens do TypeScript serão perdidas.
- validators : ValidatorFunctionFormGroupHandler [] - um conjunto de validadores para valores de grupo. Por exemplo, você pode criar um FormGroup contendo dois valores - a data mínima e máxima, para o controle de seleção de período. É nesses validadores que você precisará passar a função / funções de verificar o período. Por exemplo, que a data de início não seja maior que a data de término
- ativar : (() => booleano) | null - a função ativará / desativará as validações por condição (sempre ativada por padrão). Deve-se entender que a aplicação da função de validação a um grupo desativa a validação no nível de todo o grupo. Por exemplo, temos uma caixa de seleção para um documento de identidade. Você pode criar vários Grupos de Formulários com um conjunto diferente de campos para documentos: passaporte, carteira de motorista, passaporte de marinheiro, etc. ... Nesta função, verifique os valores suspensos e, se o valor selecionado não corresponder a esse grupo, todas as verificações de validação serão desativadas. Mais precisamente, o grupo será considerado válido, independentemente dos valores nele contidos.
Vamos falar sobre os campos FormControl , incluindo a presença do FormGroup e do FormArray .
Esses eram campos comuns para todos os controles, mas cada controle também possui campos exclusivos para seu tipo.
Formcontrol
- valor - contém o valor atual do campo. Você também pode atribuir um novo valor a este campo.
FormGroup e FormArray contêm
Validações
Obviamente, além dos controles que permitem trabalhar com dados, precisamos de validações. O pacote @ quantumart / mobx-form-validation-kit contém naturalmente várias validações predefinidas e também suporta a criação de validações personalizadas personalizadas.
Um exemplo de configuração de validações para FormControl para um campo de idade.
new FormControl<number>( this.userInfo.age, [required(), minValue(18, " 18 .", ValidationEventTypes.Warning)], v => (this.userInfo.age = v) )
Cada validação com os parâmetros mais recentes leva:
- Mensagem - uma mensagem de validação.
- eventType - nível da mensagem. 4 níveis de mensagem são suportados.
- Erro - erros
- Aviso - avisos
- Info - Mensagens informativas
- Sucesso - mensagens sobre validade. Por exemplo, você pode verificar se a senha é realmente complexa.
O pacote contém o seguinte conjunto de validações:
- obrigatório (... - campo obrigatório
- notEmptyOrSpaces (... - o campo não está vazio e não contém apenas espaços. De fato, obrigatório, levando em consideração a proibição de espaços.
- padrão (regExp: RegExp, ... - o primeiro parâmetro é a expressão regular que o campo deve corresponder. Um erro será gerado se não houver correspondência de padrão.
- invertPattern (regExp: RegExp, ... - o primeiro parâmetro é a expressão regular que o campo não deve corresponder. Um erro será gerado se houver uma correspondência de padrão.
- minLength (minlength: number, .... - o primeiro parâmetro é o comprimento mínimo do texto inclusive. É gerado um erro se o comprimento for menor que o transmitido.
- maxLength (maxlength: number, .... - o primeiro parâmetro é o comprimento máximo do texto inclusive. É emitido um erro se o comprimento for maior que o transmitido.
- absoluteLength (length: number, .... - o primeiro parâmetro é o tamanho exato do texto. Um erro será emitido se o comprimento não corresponder ao especificado.
- minValue (min: TEntity | (() => TEntity), ... - esta validação destina-se apenas a números e datas. Um erro é definido se o valor for menor que o especificado. A peculiaridade da validação é a capacidade de aceitar não apenas um valor específico, mas também function, o que significa que se você ler o valor nessa função no campo @observable do objeto, a validação em si será reiniciada não apenas quando o campo no qual a validação está pendurada for alterado, mas também quando o "campo vinculado" for alterado. primeiro, exceto tag campo que é lido como o valor @observable.
- maxValue (max: TEntity | (() => TEntity), ... - esta validação destina-se apenas a números e datas. Um erro é definido se o valor for maior que o especificado. O recurso de validação é a capacidade de aceitar não apenas um valor específico, mas também function, o que significa que se você ler o valor nessa função no campo @observable do objeto, a validação em si será reiniciada não apenas quando o campo no qual a validação está pendurada for alterado, mas também quando o "campo vinculado" for alterado. primeiro, exceto campo tag que é lido como o valor @observable
- notContainSpaces (... - diferente de notEmptyOrSpaces, um erro será emitido se o valor contiver pelo menos um espaço.
- compare (expression: (value: TEntity) => boolean (... - escrever sua própria função de validação gera muito código de copiar e colar, esse wrapper foi desenvolvido para se livrar desse problema. Essa função de validação aceita uma função como o primeiro parâmetro, que por sua vez passa o valor atual campos, que permitem que você faça uma verificação complexa, por exemplo, calculando o hash para o número de NIF ou passaporte e retorne verdadeiro / falso.Um erro será exibido se a verificação retornar falso.
- isEqual (value: string ... - uma verificação simples de correspondência de string.
A seguir, são descritas as funções do wrapper que controlam o fluxo de gatilhos de validação.
Deve-se observar que o conjunto de validações passadas para FormControl , FormGroup , FormArray é iniciado em uma única matriz e, de fato, não possui sequência de execução. Como resultado do trabalho, teremos nos campos erros , avisos , informações , mensagens, matrizes de sucessos que consistem em erros, avisos combinados em uma única matriz, etc ...
Muitas vezes, o cliente quer ver apenas um erro, e não todos de uma vez. Além disso, o TOR pode ser projetado para que uma verificação seja realizada somente após a aprovação da anterior.
Para resolver esse problema, wrapperSequentialCheck é usado. Sua chamada e sua aplicação não são diferentes do validador de função usual, mas na entrada ele recebe uma matriz de validadores que serão iniciados sequencialmente, ou seja, A próxima validação é iniciada somente após a aprovação anterior sem erros.
A segunda função do wrapper é a função de controle de fluxo das validações. wrapperActivateValidation como o primeiro parâmetro assume uma função na qual as condições para a ativação da validação devem ser gravadas. Diferentemente da função de ativação que é passada para o FormControl, essa verificação é projetada para uma lógica mais complexa. Suponha que tenhamos um construtor comum para toda a forma de pagamento do FormGroup e, além disso, existe apenas um método no servidor que aceita um conjunto comum de campos. Mas o problema é que, embora o formulário seja um, dependendo do "tipo de pagamento", mostramos um conjunto diferente de campos para o usuário. Portanto, wrapperActivateValidation permite que você escreva uma lógica na qual várias verificações serão realizadas, dependendo do tipo de pagamento.
O uso de wrappers será parecido com funções comuns.
new FormControl( this.userInfo.megapole, [wrapperActivateValidation(() => this.info.A === 10, [ required(), pattern(/\^d{10}$/) ]), wrapperActivateValidation(() => this.info.A === 20, [ wrapperSequentialCheck([ notContainSpaces(), pattern(/\^d{20}$/) ]) ])], v => (this.userInfo.megapole = v) )
Este exemplo mostra que as verificações obrigatórias (), padrão (/ \ ^ d {10} $ /) serão executadas apenas com this.info.A === 10 e se this.info.A === 20 , as validações notContainSpaces (), pattern (/ \ ^ d {20} $ /) funcionarão. Além disso, essas validações funcionarão sequencialmente, ao contrário do primeiro caso.
Naturalmente, chegará o momento em que o conjunto padrão de validações não será mais suficiente.
Então você deve escrever suas próprias funções assíncronas. Felizmente, isso é feito sem dificuldades.
O FormControl foi originalmente aprimorado pelas funções de validação assíncrona, que podem querer acessar dados do servidor, e essa resposta precisa aguardar. E, como resultado, todas as validações são assíncronas.
async function checkValueOnServer(control: FormControl): Promise<ValidationEvent[]> { if (control.value == null) { return []; } const result = await sendToServer(control.value); if (result.errorMessage) { return [ { message: result.errorMessage, type: ValidationEventTypes.Error, }, ]; } return []; }
Aqui você precisa prestar atenção em dois objetos.
Primeiro, sempre nos importamos com a matriz. I.e. de fato, você pode retornar várias mensagens de erro de uma vez, se desejar.
O segundo ponto é o objeto retornado, com o seguinte conjunto de campos.
- key ?: string - um campo opcional, permite especificar uma "chave" para uma validação específica. Para todas as chaves de base, a chave é única e corresponde ao nome. Você pode usar a tecla para renderizar a lista como reagir, mas, como a prática demonstrou, é uma má idéia. No futuro, no exemplo, mostrarei que é melhor usar a mensagem e não toque em nenhuma tecla. De qualquer forma, é como em Angunar, mas sua necessidade foi reduzida, de fato, a 0.
- message : string - uma mensagem de validação. Campo obrigatório.
- tipo : ValidationEventTypes - tipo de mensagem.
- Erro - erros
- Aviso - avisos
- Info - Mensagens informativas
- Sucesso - mensagens sobre validade. Por exemplo, você pode verificar se a senha é realmente complexa.
- additionalData ?: any - informações adicionais que podem ser transmitidas juntamente com a validação, se necessário. Pode ser alguma marcação html adicional ou um estilo específico. Em geral, você pode colocar tudo em qualquer.
Extensões
Qualquer mágica é baseada em coisas triviais. E, nesse caso, para que a configuração do foco funcione, obter alterações nos campos requer vincular o FormControl em um campo de entrada específico.
Porque O FormControl não limita o desenvolvedor no tipo de dados validados, devido à sua versatilidade, era necessário sacrificar um pouco de aplicabilidade nos elementos de reação.
Ao mesmo tempo, para entrada e área de texto, foi possível criar funções simples de ligação de dados em um elemento; para outros componentes, o processador ainda precisará fazer esforços mínimos para substituir os dados.
Para entrada, a ligação do elemento ao FormControl (nome) ficará assim.
<input type = "text" {... InputFormControl.bindActions (controls.name)} />
Para a área de texto, a ligação será assim
<textarea {... TextAreaFormControl.bindActions (controls.name)} />
InputFormControl.bindActions e TextAreaFormControl.bindActions aceitam dois parâmetros:
- formControl : FormControl - na verdade FormControl que será usado para ligação. Obrigatório.
- eventos ? - Um parâmetro opcional que contém uma lista de funções que podem ser chamadas se você precisar personalizá-las. , bindActions - Element, , element FormControl -, . . event. .
, FormControl - .
this.form = new FormGroup<IUserInfo>({ name: new FormControl( this.userInfo.name, [], v => (this.userInfo.name = v) ) });
this.userInfo.name , FormControl . FormControl.for
this.form = new FormGroup<IUserInfo>({ name: FormControl.for(this.userInfo, 'name', []) });
, name name . , TypeScript, name , . userInfo — .
— . :)
Exemplo
React TypeScript mobx.
.
npm install @quantumart/mobx-form-validation-kit
,:
,
npm init –y
npm install --save-dev webpack webpack-cli
npm install --save react react-dom
npm install --save-dev @types/react @types/react-dom
npm install --save-dev typescript ts-loader source-map-loadertsconfig.json { "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true, "module": "commonjs", "target": "es6", "jsx": "react", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
- src\components\Hello.tsx
- src\index.tsx
- index.html
- src\assets\global.scss
src\components\
Hello.tsx import * as React from "react"; export class Hello extends React.Component { render() { return ( <h1> Hello from TypeScript and React! </h1> ); } }
src\
index.tsx import * as React from "react"; import * as ReactDOM from "react-dom"; import { Hello } from "./components/Hello"; ReactDOM.render( <Hello />, document.getElementById("example") );
index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello React!</title> </head> <body> <div id="example"></div> <script src="./node_modules/react/umd/react.development.js"></script> <script src="./node_modules/react-dom/umd/react-dom.development.js"></script> <script src="./dist/main.js"></script> </body> </html> src\assets\global.scss .row { display: inline; }
webpack-dev-server
npm install --save-dev webpack-dev-server
npm install --save-dev awesome-typescript-loader
npm install --save-dev html-webpack-plugin
webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.tsx', resolve: { extensions: ['.ts', '.tsx', '.js'] }, output: { path: path.join(__dirname, '/dist'), filename: 'bundle.min.js' }, module: { rules: [ { test: /\.tsx?$/, loader: 'awesome-typescript-loader' }, { test: /\.(scss|css)?$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { importLoaders: 1, }, }, { loader: 'sass-loader', options: { sourceMap: true } }, ], }, ] }, plugins: [ new HtmlWebpackPlugin({ template: './index.html' }) ] }
, mobx React.
"mobx": "4", "mobx-react": "^6.1.1",
C IE10 mobx, 4 , package.json.
"style-loader": "^0.23.1", "css-loader": "^3.1.0", "node-sass": "^4.12.0", "sass-loader": "^7.1.0"
npm install, .
npm install @quantumart/mobx-form-validation-kit
package.json { "name": "ttt", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "webpack-dev-server --mode development --open", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "MIT", "devDependencies": { "@types/react": "^16.9.5", "@types/react-dom": "^16.9.1", "awesome-typescript-loader": "^5.2.1", "html-webpack-plugin": "^3.2.0", "source-map-loader": "^0.2.4", "ts-loader": "^6.2.0", "typescript": "^3.6.3", "webpack": "^4.41.0", "webpack-cli": "^3.3.9" }, "dependencies": { "@quantumart/mobx-form-validation-kit": "^1.0.8", "mobx": "4", "mobx-react": "^6.1.1", "react": "^16.10.2", "react-dom": "^16.10.2", "webpack-dev-server": "^3.8.2", "style-loader": "^0.23.1", "css-loader": "^3.1.0", "node-sass": "^4.12.0", "sass-loader": "^7.1.0" } }
npm run startHello from TypeScript and React!
Hello . RegistrationStore RegistrationStore.ts
src\RegistrationStore.ts
import { observable } from "mobx"; export class RegistrationStore { @observable public userInfo = { name: "" }; } export const registrationStore = new RegistrationStore();
Hello.ts, .
import * as React from "react"; import { observer } from "mobx-react"; import { registrationStore } from "../RegistrationStore"; @observer export class Hello extends React.Component { private changeName = (event: React.ChangeEvent<HTMLInputElement>) => { registrationStore.userInfo.name = event.target.value; }; render() { return ( <React.Fragment> <h1>, {registrationStore.userInfo.name}</h1> <div className="row"> <span>:</span> <input type="text" value={registrationStore.userInfo.name} onChange={this.changeName} /> </div> </React.Fragment> ); } }
, Store Mobx. input.
, . , . «» . , .
@quantumart/mobx-form-validation-kit
- .
stc/ErrorWraper.tsx
import * as React from "react"; import { observer } from "mobx-react"; import { FormControl } from "@quantumart/mobx-form-validation-kit"; interface Props { formControl: FormControl; } @observer export class ErrorWraper extends React.Component<Props> { render() { return ( <div> {this.props.children} {this.props.formControl.errors.map(error => ( <span key={error.message} className="error"> {error.message} </span> ))} </div> ); } }
, -, .
Hello.tsx .
- — changeName. {...InputFormControl.bindActions(controls.name)} . .
- – input, input , , , .
- – form store , , , componentWillUnmount registrationStore.form.dispose() . mobx FromControl .
import * as React from "react"; import { observer } from "mobx-react"; import { registrationStore } from "../RegistrationStore"; import { ErrorWraper } from "../ErrorWraper"; import { InputFormControl } from "@quantumart/mobx-form-validation-kit"; @observer export class Hello extends React.Component { constructor(props: any) { super(props); registrationStore.initForm(); } componentWillUnmount() { registrationStore.form.dispose(); } render() { const controls = registrationStore.form.controls; return ( <React.Fragment> <h1>, {registrationStore.userInfo.name}</h1> <div className="row"> <span>:</span> <ErrorWraper formControl={controls.name}> <input type="text" {...InputFormControl.bindActions(controls.name)} /> </ErrorWraper> </div> </React.Fragment> ); } }
RegistrationStore.ts.
.
( ) userInfo, form. - userInfo.
import { observable } from "mobx"; import { FormControl, FormGroup, AbstractControls } from "@quantumart/mobx-form-validation-kit"; interface IUserInfo extends AbstractControls { name: FormControl; } export class RegistrationStore { @observable public userInfo = { name: "" }; @observable public form: FormGroup<IUserInfo>; public initForm(): void { this.form = new FormGroup<IUserInfo>({ name: new FormControl( this.userInfo.name, [], v => (this.userInfo.name = v) ) }); } } export const registrationStore = new RegistrationStore();