[cpf-field.tsx](snippets/cpf-field.tsx ':include :type=code tsx')
-With react-hook-form:
+A controlled component: the parent holds the CPF and passes `value` and `onChange`. It also takes the input's own props (`name`, `onBlur`, `ref`), so react-hook-form's `field` spreads into it as is, with focus on error and touched state:
[signup-form.tsx](snippets/usage/signup-form.tsx ':include :type=code tsx')
@@ -22,7 +22,7 @@ With react-hook-form:
[cpf-field.ts](snippets/cpf-field.ts ':include :type=code ts')
-With Reactive Forms:
+A `ControlValueAccessor`, so it takes `formControlName` (or `formControl`, or `ngModel`) like a native input, touched on blur and disabled with its control. With Reactive Forms:
[signup.ts](snippets/usage/signup.ts ':include :type=code ts')
@@ -32,7 +32,7 @@ With Reactive Forms:
[cpf-field.vue](snippets/cpf-field.vue ':include :type=code vue')
-With VeeValidate:
+The CPF is the component's `v-model` (`defineModel`), which is what VeeValidate's `defineField` binds to:
[signup-form.vue](snippets/usage/signup-form.vue ':include :type=code vue')
@@ -40,6 +40,8 @@ With VeeValidate:
+No build step: save it as an `.html` file and open it. It imports the package from a CDN and calls `setCustomValidity`, so a form around the field refuses to submit an invalid CPF.
+
[cpf-field.html](snippets/cpf-field.html ':include :type=code html')
diff --git a/docs/pt-br/examples.md b/docs/pt-br/examples.md
index eadb17f2b..e9a82c9e1 100644
--- a/docs/pt-br/examples.md
+++ b/docs/pt-br/examples.md
@@ -6,13 +6,13 @@ keywords: ["exemplos", "máscara de CPF", "CPF React", "CPF Angular", "CPF Vue",
Um campo de CPF que formata enquanto você digita, com `formatCpf`, e valida quando completo, com `isValidCpf`. Cada aba roda o código logo abaixo dela, que pode ser copiado do jeito que está.
-`formatCpf` formata o que já foi digitado (`"9438"` vira `"943.8"`), descarta o que não é dígito e corta os dígitos além do décimo primeiro. Só que trocar o valor do campo joga o cursor para o fim, então `maskCpf` devolve o cursor para perto do dígito que está sendo editado, e transforma um `.` ou `-` apagado num dígito apagado, que o `formatCpf` recolocaria na hora. O campo recebe `aria-invalid` quando um CPF completo não é válido, e entrega o CPF formatado ao componente pai do jeito que cada framework espera, então encaixa na biblioteca de formulários que a maioria dos projetos usa: um componente controlado no React (`value`, `onChange` e as props do próprio input, para o `field` do react-hook-form), um `ControlValueAccessor` no Angular (`formControlName`), `v-model` no Vue (o `defineField` do VeeValidate).
+`formatCpf` formata o que já foi digitado (`"9438"` vira `"943.8"`), descarta o que não é dígito e corta os dígitos além do décimo primeiro. Só que trocar o valor do campo joga o cursor para o fim, então `maskCpf` devolve o cursor para perto do dígito que está sendo editado, e transforma um `.` ou `-` apagado num dígito apagado, que o `formatCpf` recolocaria na hora. O campo recebe `aria-invalid` quando um CPF completo não é válido, e entrega o CPF formatado ao componente pai do jeito que o seu framework espera, então encaixa na biblioteca de formulários que a maioria dos projetos usa.
[cpf-field.tsx](../snippets/cpf-field.tsx ':include :type=code tsx')
-Com react-hook-form:
+Um componente controlado: o pai guarda o CPF e passa `value` e `onChange`. Ele também aceita as props do próprio input (`name`, `onBlur`, `ref`), então o `field` do react-hook-form entra inteiro, com foco no erro e estado de touched:
[signup-form.tsx](../snippets/usage/signup-form.tsx ':include :type=code tsx')
@@ -22,7 +22,7 @@ Com react-hook-form:
[cpf-field.ts](../snippets/cpf-field.ts ':include :type=code ts')
-Com Reactive Forms:
+Um `ControlValueAccessor`, então aceita `formControlName` (ou `formControl`, ou `ngModel`) como um input nativo, fica touched no blur e desabilitado junto com o seu controle. Com Reactive Forms:
[signup.ts](../snippets/usage/signup.ts ':include :type=code ts')
@@ -32,7 +32,7 @@ Com Reactive Forms:
[cpf-field.vue](../snippets/cpf-field.vue ':include :type=code vue')
-Com VeeValidate:
+O CPF é o `v-model` do componente (`defineModel`), que é onde o `defineField` do VeeValidate se liga:
[signup-form.vue](../snippets/usage/signup-form.vue ':include :type=code vue')
@@ -40,6 +40,8 @@ Com VeeValidate:
+Sem build: salve como um arquivo `.html` e abra. Ele importa o pacote de um CDN e chama `setCustomValidity`, então um formulário em volta do campo se recusa a enviar um CPF inválido.
+
[cpf-field.html](../snippets/cpf-field.html ':include :type=code html')
From e6ac5524c16bd397caa5a2c874d73f74eb03d2dd Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sun, 20 Sep 2026 12:33:33 -0300
Subject: [PATCH 31/70] docs(examples): one example page per task, starting
with a generic document field
The examples move to a folder, one page per task, each with the framework tabs: the first is a
field that masks and validates CPF, CNPJ, CEP or a phone number, picked from a select. A map gives
each document its label, complete format, formatter and validator, so another one is four lines.
The mask got shorter: formatting what comes before the caret says where the caret goes, so it no
longer needs to know which characters a format keeps. The runner mounts the file that uses the
component, and compiles Angular with TypeScript, whose decorator output Angular's in-browser
compiler reads.
---
docs/404.html | 7 +-
docs/_sidebar.md | 3 +-
docs/examples.md | 49 ------
.../document-field.html} | 27 +--
docs/examples/document-field.md | 51 ++++++
docs/getting-started.html | 7 +-
docs/index.html | 7 +-
docs/migration-v1-to-v2.html | 7 +-
docs/pt-br/_sidebar.md | 3 +-
docs/pt-br/examples.md | 49 ------
.../document-field.html} | 27 +--
docs/pt-br/examples/document-field.md | 51 ++++++
docs/pt-br/getting-started.html | 7 +-
docs/pt-br/index.html | 7 +-
docs/pt-br/migration-v1-to-v2.html | 7 +-
docs/pt-br/utilities.html | 7 +-
docs/sitemap.xml | 16 +-
docs/snippets/cpf-field.html | 78 ---------
docs/snippets/cpf-field.ts | 111 ------------
docs/snippets/cpf-field.tsx | 79 ---------
docs/snippets/cpf-field.vue | 75 ---------
.../document-field/document-field.html | 125 ++++++++++++++
.../snippets/document-field/document-field.ts | 158 ++++++++++++++++++
.../document-field/document-field.tsx | 116 +++++++++++++
.../document-field/document-field.vue | 117 +++++++++++++
docs/snippets/document-field/document-form.ts | 58 +++++++
.../snippets/document-field/document-form.tsx | 46 +++++
.../snippets/document-field/document-form.vue | 38 +++++
docs/snippets/live/angular.html | 3 -
.../snippets/live/document-field-angular.html | 8 +
docs/snippets/live/document-field-react.html | 8 +
docs/snippets/live/document-field-vue.html | 8 +
docs/snippets/live/react.html | 3 -
docs/snippets/live/run.js | 77 +++++++--
docs/snippets/live/vue.html | 3 -
docs/snippets/usage/signup-form.tsx | 30 ----
docs/snippets/usage/signup-form.vue | 24 ---
docs/snippets/usage/signup.ts | 37 ----
docs/utilities.html | 7 +-
39 files changed, 942 insertions(+), 599 deletions(-)
delete mode 100644 docs/examples.md
rename docs/{examples.html => examples/document-field.html} (93%)
create mode 100644 docs/examples/document-field.md
delete mode 100644 docs/pt-br/examples.md
rename docs/pt-br/{examples.html => examples/document-field.html} (93%)
create mode 100644 docs/pt-br/examples/document-field.md
delete mode 100644 docs/snippets/cpf-field.html
delete mode 100644 docs/snippets/cpf-field.ts
delete mode 100644 docs/snippets/cpf-field.tsx
delete mode 100644 docs/snippets/cpf-field.vue
create mode 100644 docs/snippets/document-field/document-field.html
create mode 100644 docs/snippets/document-field/document-field.ts
create mode 100644 docs/snippets/document-field/document-field.tsx
create mode 100644 docs/snippets/document-field/document-field.vue
create mode 100644 docs/snippets/document-field/document-form.ts
create mode 100644 docs/snippets/document-field/document-form.tsx
create mode 100644 docs/snippets/document-field/document-form.vue
delete mode 100644 docs/snippets/live/angular.html
create mode 100644 docs/snippets/live/document-field-angular.html
create mode 100644 docs/snippets/live/document-field-react.html
create mode 100644 docs/snippets/live/document-field-vue.html
delete mode 100644 docs/snippets/live/react.html
delete mode 100644 docs/snippets/live/vue.html
delete mode 100644 docs/snippets/usage/signup-form.tsx
delete mode 100644 docs/snippets/usage/signup-form.vue
delete mode 100644 docs/snippets/usage/signup.ts
diff --git a/docs/404.html b/docs/404.html
index 4d08426ea..9f0b4ff3c 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -111,7 +111,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 20ebc8119..e5d9cb5ec 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -1,4 +1,5 @@
* [Getting Started](getting-started.md)
* [Utilities](utilities.md)
-* [Examples](examples.md)
+* Examples
+ * [Document field](examples/document-field.md)
* [Migration v1 to v2](migration-v1-to-v2.md)
diff --git a/docs/examples.md b/docs/examples.md
deleted file mode 100644
index ff62f8aac..000000000
--- a/docs/examples.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-title: "Examples"
-description: "A CPF field that formats as you type and validates the result, with Brazilian Utils in React, Angular, Vue and plain JavaScript."
-keywords: ["examples", "CPF input mask", "React CPF", "Angular CPF", "Vue CPF", "format CPF as you type", "validate CPF"]
----
-
-A CPF field that formats as you type, with `formatCpf`, and validates once complete, with `isValidCpf`. Each tab runs the code below it, which you can copy as is.
-
-`formatCpf` formats whatever has been typed so far (`"9438"` becomes `"943.8"`), drops anything that is not a digit and cuts digits past the eleventh. Replacing a field's value moves the caret to the end, though, so `maskCpf` puts it back next to the digit being edited, and turns a deleted `.` or `-` into a deleted digit, which `formatCpf` would otherwise put straight back. The field is marked `aria-invalid` when a complete CPF is not valid, and hands the formatted CPF to its parent the way its framework expects, so it drops into the form library most projects use.
-
-
-
-[cpf-field.tsx](snippets/cpf-field.tsx ':include :type=code tsx')
-
-A controlled component: the parent holds the CPF and passes `value` and `onChange`. It also takes the input's own props (`name`, `onBlur`, `ref`), so react-hook-form's `field` spreads into it as is, with focus on error and touched state:
-
-[signup-form.tsx](snippets/usage/signup-form.tsx ':include :type=code tsx')
-
-
-
-
-
-[cpf-field.ts](snippets/cpf-field.ts ':include :type=code ts')
-
-A `ControlValueAccessor`, so it takes `formControlName` (or `formControl`, or `ngModel`) like a native input, touched on blur and disabled with its control. With Reactive Forms:
-
-[signup.ts](snippets/usage/signup.ts ':include :type=code ts')
-
-
-
-
-
-[cpf-field.vue](snippets/cpf-field.vue ':include :type=code vue')
-
-The CPF is the component's `v-model` (`defineModel`), which is what VeeValidate's `defineField` binds to:
-
-[signup-form.vue](snippets/usage/signup-form.vue ':include :type=code vue')
-
-
-
-
-
-No build step: save it as an `.html` file and open it. It imports the package from a CDN and calls `setCustomValidity`, so a form around the field refuses to submit an invalid CPF.
-
-[cpf-field.html](snippets/cpf-field.html ':include :type=code html')
-
-
-
-The [utilities reference](utilities.md) lists every function.
diff --git a/docs/examples.html b/docs/examples/document-field.html
similarity index 93%
rename from docs/examples.html
rename to docs/examples/document-field.html
index 33f4b59b5..11f9b675a 100644
--- a/docs/examples.html
+++ b/docs/examples/document-field.html
@@ -3,8 +3,8 @@
-
Examples · Brazilian Utils
-
+
Document field · Brazilian Utils
+
@@ -15,25 +15,25 @@
hook further down rewrites the title, description, canonical, hreflang and Open Graph tags for
the page being shown; the values here are the home page's, what a crawler without JavaScript
and a link preview see. -->
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/examples/document-field.md b/docs/examples/document-field.md
new file mode 100644
index 000000000..d192e2a4a
--- /dev/null
+++ b/docs/examples/document-field.md
@@ -0,0 +1,51 @@
+---
+title: "Document field"
+description: "One field that masks and validates CPF, CNPJ, CEP or a phone number as you type, with Brazilian Utils in React, Angular, Vue and plain JavaScript."
+keywords: ["CPF input mask", "CNPJ mask", "CEP mask", "phone mask", "React CPF", "Angular CPF", "Vue CPF", "validate CPF"]
+---
+
+One field for any of the documents, picked from the select: it formats as you type and validates once the value is complete. Each tab runs the code below it, which you can copy as is.
+
+The field is generic: a small map gives each document its label, its complete format, its `format*` and its `isValid*`, so adding another one is four lines. Formatting a field's value moves the caret to the end, so `mask` puts the caret back next to the character being edited, and turns a deleted separator into a deleted character, which the formatter would otherwise put straight back. The caret comes out of the formatter itself: whatever is typed before the caret, formatted, is as long as the caret's new position.
+
+
+
+
+A controlled component: the parent holds the value and passes `value` and `onChange`. It also takes the input's own props, so react-hook-form's `field` spreads into it as is:
+
+[document-field.tsx](../snippets/document-field/document-field.tsx ':include :type=code tsx')
+
+[document-form.tsx](../snippets/document-field/document-form.tsx ':include :type=code tsx')
+
+
+
+
+
+A `ControlValueAccessor`, so it takes `formControlName` (or `formControl`, or `ngModel`) like a native input, touched on blur and disabled with its control:
+
+[document-field.ts](../snippets/document-field/document-field.ts ':include :type=code ts')
+
+[document-form.ts](../snippets/document-field/document-form.ts ':include :type=code ts')
+
+
+
+
+
+The value is the component's `v-model` (`defineModel`), which is what VeeValidate's `defineField` binds to:
+
+[document-field.vue](../snippets/document-field/document-field.vue ':include :type=code vue')
+
+[document-form.vue](../snippets/document-field/document-form.vue ':include :type=code vue')
+
+
+
+
+
+No build step: save it as an `.html` file and open it. It imports the package from a CDN and calls `setCustomValidity`, so the form refuses to submit an invalid document.
+
+[document-field.html](../snippets/document-field/document-field.html ':include :type=code html')
+
+
+
+
+The [utilities reference](utilities.md) lists every function.
diff --git a/docs/getting-started.html b/docs/getting-started.html
index c66d1f9df..64e766512 100644
--- a/docs/getting-started.html
+++ b/docs/getting-started.html
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/index.html b/docs/index.html
index d48c77393..61a954630 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -115,7 +115,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/migration-v1-to-v2.html b/docs/migration-v1-to-v2.html
index 737d9f813..ff7c4d918 100644
--- a/docs/migration-v1-to-v2.html
+++ b/docs/migration-v1-to-v2.html
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/pt-br/_sidebar.md b/docs/pt-br/_sidebar.md
index 4b4133439..bdab810be 100644
--- a/docs/pt-br/_sidebar.md
+++ b/docs/pt-br/_sidebar.md
@@ -1,4 +1,5 @@
* [Introdução](pt-br/getting-started.md)
* [Utilitários](pt-br/utilities.md)
-* [Exemplos](pt-br/examples.md)
+* Exemplos
+ * [Campo de documento](pt-br/examples/document-field.md)
* [Migração v1 para v2](pt-br/migration-v1-to-v2.md)
diff --git a/docs/pt-br/examples.md b/docs/pt-br/examples.md
deleted file mode 100644
index e9a82c9e1..000000000
--- a/docs/pt-br/examples.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-title: "Exemplos"
-description: "Um campo de CPF que formata enquanto você digita e valida o resultado, com Brazilian Utils em React, Angular, Vue e JavaScript puro."
-keywords: ["exemplos", "máscara de CPF", "CPF React", "CPF Angular", "CPF Vue", "formatar CPF ao digitar", "validar CPF"]
----
-
-Um campo de CPF que formata enquanto você digita, com `formatCpf`, e valida quando completo, com `isValidCpf`. Cada aba roda o código logo abaixo dela, que pode ser copiado do jeito que está.
-
-`formatCpf` formata o que já foi digitado (`"9438"` vira `"943.8"`), descarta o que não é dígito e corta os dígitos além do décimo primeiro. Só que trocar o valor do campo joga o cursor para o fim, então `maskCpf` devolve o cursor para perto do dígito que está sendo editado, e transforma um `.` ou `-` apagado num dígito apagado, que o `formatCpf` recolocaria na hora. O campo recebe `aria-invalid` quando um CPF completo não é válido, e entrega o CPF formatado ao componente pai do jeito que o seu framework espera, então encaixa na biblioteca de formulários que a maioria dos projetos usa.
-
-
-
-[cpf-field.tsx](../snippets/cpf-field.tsx ':include :type=code tsx')
-
-Um componente controlado: o pai guarda o CPF e passa `value` e `onChange`. Ele também aceita as props do próprio input (`name`, `onBlur`, `ref`), então o `field` do react-hook-form entra inteiro, com foco no erro e estado de touched:
-
-[signup-form.tsx](../snippets/usage/signup-form.tsx ':include :type=code tsx')
-
-
-
-
-
-[cpf-field.ts](../snippets/cpf-field.ts ':include :type=code ts')
-
-Um `ControlValueAccessor`, então aceita `formControlName` (ou `formControl`, ou `ngModel`) como um input nativo, fica touched no blur e desabilitado junto com o seu controle. Com Reactive Forms:
-
-[signup.ts](../snippets/usage/signup.ts ':include :type=code ts')
-
-
-
-
-
-[cpf-field.vue](../snippets/cpf-field.vue ':include :type=code vue')
-
-O CPF é o `v-model` do componente (`defineModel`), que é onde o `defineField` do VeeValidate se liga:
-
-[signup-form.vue](../snippets/usage/signup-form.vue ':include :type=code vue')
-
-
-
-
-
-Sem build: salve como um arquivo `.html` e abra. Ele importa o pacote de um CDN e chama `setCustomValidity`, então um formulário em volta do campo se recusa a enviar um CPF inválido.
-
-[cpf-field.html](../snippets/cpf-field.html ':include :type=code html')
-
-
-
-A [referência de utilitários](pt-br/utilities.md) lista todas as funções.
diff --git a/docs/pt-br/examples.html b/docs/pt-br/examples/document-field.html
similarity index 93%
rename from docs/pt-br/examples.html
rename to docs/pt-br/examples/document-field.html
index c0b0bc67c..bccb83f63 100644
--- a/docs/pt-br/examples.html
+++ b/docs/pt-br/examples/document-field.html
@@ -3,8 +3,8 @@
-
Exemplos · Brazilian Utils
-
+
Campo de documento · Brazilian Utils
+
@@ -15,25 +15,25 @@
hook further down rewrites the title, description, canonical, hreflang and Open Graph tags for
the page being shown; the values here are the home page's, what a crawler without JavaScript
and a link preview see. -->
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/pt-br/examples/document-field.md b/docs/pt-br/examples/document-field.md
new file mode 100644
index 000000000..8f2c3cc1b
--- /dev/null
+++ b/docs/pt-br/examples/document-field.md
@@ -0,0 +1,51 @@
+---
+title: "Campo de documento"
+description: "Um campo que aplica máscara e valida CPF, CNPJ, CEP ou telefone enquanto você digita, com Brazilian Utils em React, Angular, Vue e JavaScript puro."
+keywords: ["máscara de CPF", "máscara de CNPJ", "máscara de CEP", "máscara de telefone", "CPF React", "CPF Angular", "CPF Vue", "validar CPF"]
+---
+
+Um campo para qualquer um dos documentos, escolhido no select: formata enquanto você digita e valida quando o valor fica completo. Cada aba roda o código logo abaixo dela, que pode ser copiado do jeito que está.
+
+O campo é genérico: um mapa pequeno dá a cada documento o rótulo, o formato completo, o `format*` e o `isValid*`, então incluir outro são quatro linhas. Formatar o valor do campo joga o cursor para o fim, então o `mask` devolve o cursor para perto do caractere que está sendo editado, e transforma um separador apagado num caractere apagado, que o formatter recolocaria na hora. A posição do cursor sai do próprio formatter: o que foi digitado antes do cursor, formatado, tem o tamanho da nova posição.
+
+
+
+
+Um componente controlado: o pai guarda o valor e passa `value` e `onChange`. Ele também aceita as props do próprio input, então o `field` do react-hook-form entra inteiro:
+
+[document-field.tsx](../../snippets/document-field/document-field.tsx ':include :type=code tsx')
+
+[document-form.tsx](../../snippets/document-field/document-form.tsx ':include :type=code tsx')
+
+
+
+
+
+Um `ControlValueAccessor`, então aceita `formControlName` (ou `formControl`, ou `ngModel`) como um input nativo, fica touched no blur e desabilita junto com o seu controle:
+
+[document-field.ts](../../snippets/document-field/document-field.ts ':include :type=code ts')
+
+[document-form.ts](../../snippets/document-field/document-form.ts ':include :type=code ts')
+
+
+
+
+
+O valor é o `v-model` do componente (`defineModel`), que é onde o `defineField` do VeeValidate se liga:
+
+[document-field.vue](../../snippets/document-field/document-field.vue ':include :type=code vue')
+
+[document-form.vue](../../snippets/document-field/document-form.vue ':include :type=code vue')
+
+
+
+
+
+Sem build: salve como um arquivo `.html` e abra. Ele importa o pacote de um CDN e chama `setCustomValidity`, então o formulário se recusa a enviar um documento inválido.
+
+[document-field.html](../../snippets/document-field/document-field.html ':include :type=code html')
+
+
+
+
+A [referência de utilitários](pt-br/utilities.md) lista todas as funções.
diff --git a/docs/pt-br/getting-started.html b/docs/pt-br/getting-started.html
index e1da33f5b..f6a71e436 100644
--- a/docs/pt-br/getting-started.html
+++ b/docs/pt-br/getting-started.html
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/pt-br/index.html b/docs/pt-br/index.html
index e1da33f5b..f6a71e436 100644
--- a/docs/pt-br/index.html
+++ b/docs/pt-br/index.html
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/pt-br/migration-v1-to-v2.html b/docs/pt-br/migration-v1-to-v2.html
index e05c32ff8..fb5bac6f2 100644
--- a/docs/pt-br/migration-v1-to-v2.html
+++ b/docs/pt-br/migration-v1-to-v2.html
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/pt-br/utilities.html b/docs/pt-br/utilities.html
index ee67ad680..8d407759f 100644
--- a/docs/pt-br/utilities.html
+++ b/docs/pt-br/utilities.html
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
diff --git a/docs/sitemap.xml b/docs/sitemap.xml
index de04adaab..23753f8ce 100644
--- a/docs/sitemap.xml
+++ b/docs/sitemap.xml
@@ -18,10 +18,10 @@
- https://brazilian-utils.com.br/examples
-
-
-
+ https://brazilian-utils.com.br/examples/document-field
+
+
+
https://brazilian-utils.com.br/migration-v1-to-v2
@@ -42,10 +42,10 @@
- https://brazilian-utils.com.br/pt-br/examples
-
-
-
+ https://brazilian-utils.com.br/pt-br/examples/document-field
+
+
+
https://brazilian-utils.com.br/pt-br/migration-v1-to-v2
diff --git a/docs/snippets/cpf-field.html b/docs/snippets/cpf-field.html
deleted file mode 100644
index 47a802d99..000000000
--- a/docs/snippets/cpf-field.html
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
- CPF field
-
-
-
- CPF
-
-
-
-
-
-
diff --git a/docs/snippets/cpf-field.ts b/docs/snippets/cpf-field.ts
deleted file mode 100644
index f3f0a6602..000000000
--- a/docs/snippets/cpf-field.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { Component, computed, forwardRef, signal } from "@angular/core";
-import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
-import { formatCpf, isValidCpf } from "@brazilian-utils/brazilian-utils";
-
-type MaskCpfParams = {
- /** The field the CPF is typed into. */
- input: HTMLInputElement;
- /** The `inputType` of the `input` event. */
- inputType?: string;
-};
-
-/**
- * Formats the CPF typed into `input` in place and keeps the caret next to
- * the digit being edited, so typing, deleting or pasting anywhere works.
- * Returns the formatted value.
- */
-function maskCpf({ input, inputType = "" }: MaskCpfParams): string {
- let value = input.value;
- let caret = input.selectionStart ?? value.length;
-
- // A deleted "." or "-" would come straight back: delete the digit next to it.
- if (
- inputType.startsWith("delete") &&
- formatCpf(value).length > value.length
- ) {
- if (inputType === "deleteContentBackward") caret -= 1;
- value = value.slice(0, caret) + value.slice(caret + 1);
- }
-
- const digitsBeforeCaret = value.slice(0, caret).replace(/\D/g, "").length;
- const formatted = formatCpf(value);
- let position = 0;
-
- for (
- let seen = 0;
- seen < digitsBeforeCaret && position < formatted.length;
- position += 1
- ) {
- if (/\d/.test(formatted.charAt(position))) seen += 1;
- }
-
- input.value = formatted;
- input.setSelectionRange(position, position);
-
- return formatted;
-}
-
-@Component({
- selector: "app-cpf-field",
- providers: [
- {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => CpfField),
- multi: true,
- },
- ],
- template: `
-
- CPF
-
- @if (complete()) {
- {{ valid() ? "✓ Valid CPF" : "✗ Invalid CPF" }}
- }
-
- `,
-})
-export class CpfField implements ControlValueAccessor {
- protected readonly value = signal("");
- protected readonly disabled = signal(false);
- protected readonly complete = computed(() => this.value().length === 14);
- protected readonly valid = computed(
- () => this.complete() && isValidCpf(this.value()),
- );
-
- protected onChange: (cpf: string) => void = () => {};
- protected onTouched: () => void = () => {};
-
- writeValue(cpf: string | null): void {
- this.value.set(formatCpf(cpf ?? ""));
- }
-
- registerOnChange(onChange: (cpf: string) => void): void {
- this.onChange = onChange;
- }
-
- registerOnTouched(onTouched: () => void): void {
- this.onTouched = onTouched;
- }
-
- setDisabledState(disabled: boolean): void {
- this.disabled.set(disabled);
- }
-
- protected onInput(event: Event) {
- const cpf = maskCpf({
- input: event.target as HTMLInputElement,
- inputType: (event as InputEvent).inputType,
- });
-
- this.value.set(cpf);
- this.onChange(cpf);
- }
-}
diff --git a/docs/snippets/cpf-field.tsx b/docs/snippets/cpf-field.tsx
deleted file mode 100644
index 4bc395b4a..000000000
--- a/docs/snippets/cpf-field.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { ComponentProps } from "react";
-import { formatCpf, isValidCpf } from "@brazilian-utils/brazilian-utils";
-
-type MaskCpfParams = {
- /** The field the CPF is typed into. */
- input: HTMLInputElement;
- /** The `inputType` of the `input` event. */
- inputType?: string;
-};
-
-/**
- * Formats the CPF typed into `input` in place and keeps the caret next to
- * the digit being edited, so typing, deleting or pasting anywhere works.
- * Returns the formatted value.
- */
-function maskCpf({ input, inputType = "" }: MaskCpfParams): string {
- let value = input.value;
- let caret = input.selectionStart ?? value.length;
-
- // A deleted "." or "-" would come straight back: delete the digit next to it.
- if (
- inputType.startsWith("delete") &&
- formatCpf(value).length > value.length
- ) {
- if (inputType === "deleteContentBackward") caret -= 1;
- value = value.slice(0, caret) + value.slice(caret + 1);
- }
-
- const digitsBeforeCaret = value.slice(0, caret).replace(/\D/g, "").length;
- const formatted = formatCpf(value);
- let position = 0;
-
- for (
- let seen = 0;
- seen < digitsBeforeCaret && position < formatted.length;
- position += 1
- ) {
- if (/\d/.test(formatted.charAt(position))) seen += 1;
- }
-
- input.value = formatted;
- input.setSelectionRange(position, position);
-
- return formatted;
-}
-
-type CpfFieldProps = Omit, "value" | "onChange"> & {
- /** The formatted CPF. */
- value: string;
- /** Called with the formatted CPF on every change. */
- onChange: (cpf: string) => void;
-};
-
-export function CpfField({ value, onChange, ...props }: CpfFieldProps) {
- const complete = value.length === 14;
- const valid = complete && isValidCpf(value);
-
- return (
-
- CPF
-
- onChange(
- maskCpf({
- input: event.currentTarget,
- inputType: (event.nativeEvent as InputEvent).inputType,
- }),
- )
- }
- />
- {complete && {valid ? "✓ Valid CPF" : "✗ Invalid CPF"} }
-
- );
-}
diff --git a/docs/snippets/cpf-field.vue b/docs/snippets/cpf-field.vue
deleted file mode 100644
index 6de4efe23..000000000
--- a/docs/snippets/cpf-field.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
- CPF
-
- {{
- valid ? "✓ Valid CPF" : "✗ Invalid CPF"
- }}
-
-
diff --git a/docs/snippets/document-field/document-field.html b/docs/snippets/document-field/document-field.html
new file mode 100644
index 000000000..cb55e8fbe
--- /dev/null
+++ b/docs/snippets/document-field/document-field.html
@@ -0,0 +1,125 @@
+
+
+
+ Document field
+
+
+
+
+
+
diff --git a/docs/snippets/document-field/document-field.ts b/docs/snippets/document-field/document-field.ts
new file mode 100644
index 000000000..bd8c989d6
--- /dev/null
+++ b/docs/snippets/document-field/document-field.ts
@@ -0,0 +1,158 @@
+import { Component, Input, computed, forwardRef, signal } from "@angular/core";
+import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
+import {
+ formatCep,
+ formatCnpj,
+ formatCpf,
+ formatPhone,
+ isValidCep,
+ isValidCnpj,
+ isValidCpf,
+ isValidPhone,
+} from "@brazilian-utils/brazilian-utils";
+
+export type DocumentKind = "cpf" | "cnpj" | "cep" | "phone";
+
+export type DocumentSpec = {
+ label: string;
+ /** The complete format, which also tells the field when the value is complete. */
+ placeholder: string;
+ format: (value: string) => string;
+ validator: (value: string) => boolean;
+};
+
+export const DOCUMENTS: Record = {
+ cpf: {
+ label: "CPF",
+ placeholder: "000.000.000-00",
+ format: formatCpf,
+ validator: isValidCpf,
+ },
+ cnpj: {
+ label: "CNPJ",
+ placeholder: "00.ABC.000/0001-00",
+ format: (value) => formatCnpj(value, { version: 2 }),
+ validator: (value) => isValidCnpj(value, { version: 2 }),
+ },
+ cep: {
+ label: "CEP",
+ placeholder: "00000-000",
+ format: formatCep,
+ validator: isValidCep,
+ },
+ phone: {
+ label: "Phone",
+ placeholder: "(00) 00000-0000",
+ format: (value) => formatPhone(value, { mask: "nanp" }),
+ validator: isValidPhone,
+ },
+};
+
+type MaskParams = {
+ /** The field the document is typed into. */
+ input: HTMLInputElement;
+ /** The `inputType` of the `input` event. */
+ inputType?: string;
+ /** The formatter of the document being typed. */
+ format: (value: string) => string;
+};
+
+/**
+ * Formats the document typed into `input` in place and keeps the caret next to
+ * the character being edited, so typing, deleting or pasting anywhere works.
+ * Returns the formatted value.
+ */
+export function mask({ input, inputType = "", format }: MaskParams): string {
+ let value = input.value;
+ let caret = input.selectionStart ?? value.length;
+
+ // A deleted separator would come straight back: delete the character next to it.
+ if (inputType.startsWith("delete") && format(value).length > value.length) {
+ if (inputType === "deleteContentBackward") caret -= 1;
+ value = value.slice(0, caret) + value.slice(caret + 1);
+ }
+
+ // Formatting what comes before the caret says where the caret goes.
+ const position = format(value.slice(0, caret)).length;
+
+ input.value = format(value);
+ input.setSelectionRange(position, position);
+
+ return input.value;
+}
+
+@Component({
+ selector: "app-document-field",
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DocumentField),
+ multi: true,
+ },
+ ],
+ template: `
+
+ {{ spec.label }}
+
+ @if (complete) {
+ {{ valid ? "✓ Valid" : "✗ Invalid" }}
+ }
+
+ `,
+})
+export class DocumentField implements ControlValueAccessor {
+ @Input({ required: true }) kind!: DocumentKind;
+
+ protected readonly value = signal("");
+ protected readonly disabled = signal(false);
+
+ protected get spec(): DocumentSpec {
+ return DOCUMENTS[this.kind];
+ }
+
+ protected get valid(): boolean {
+ return this.spec.validator(this.value());
+ }
+
+ protected get complete(): boolean {
+ return this.valid || this.value().length === this.spec.placeholder.length;
+ }
+
+ protected onChange: (value: string) => void = () => {};
+ protected onTouched: () => void = () => {};
+
+ writeValue(value: string | null): void {
+ this.value.set(value ? this.spec.format(value) : "");
+ }
+
+ registerOnChange(onChange: (value: string) => void): void {
+ this.onChange = onChange;
+ }
+
+ registerOnTouched(onTouched: () => void): void {
+ this.onTouched = onTouched;
+ }
+
+ setDisabledState(disabled: boolean): void {
+ this.disabled.set(disabled);
+ }
+
+ protected onInput(event: Event) {
+ const value = mask({
+ input: event.target as HTMLInputElement,
+ inputType: (event as InputEvent).inputType,
+ format: this.spec.format,
+ });
+
+ this.value.set(value);
+ this.onChange(value);
+ }
+}
diff --git a/docs/snippets/document-field/document-field.tsx b/docs/snippets/document-field/document-field.tsx
new file mode 100644
index 000000000..50d2809a1
--- /dev/null
+++ b/docs/snippets/document-field/document-field.tsx
@@ -0,0 +1,116 @@
+import type { ComponentProps } from "react";
+import {
+ formatCep,
+ formatCnpj,
+ formatCpf,
+ formatPhone,
+ isValidCep,
+ isValidCnpj,
+ isValidCpf,
+ isValidPhone,
+} from "@brazilian-utils/brazilian-utils";
+
+export type DocumentKind = "cpf" | "cnpj" | "cep" | "phone";
+
+export type DocumentSpec = {
+ label: string;
+ /** The complete format, which also tells the field when the value is complete. */
+ placeholder: string;
+ format: (value: string) => string;
+ validator: (value: string) => boolean;
+};
+
+export const DOCUMENTS: Record = {
+ cpf: {
+ label: "CPF",
+ placeholder: "000.000.000-00",
+ format: formatCpf,
+ validator: isValidCpf,
+ },
+ cnpj: {
+ label: "CNPJ",
+ placeholder: "00.ABC.000/0001-00",
+ format: (value) => formatCnpj(value, { version: 2 }),
+ validator: (value) => isValidCnpj(value, { version: 2 }),
+ },
+ cep: {
+ label: "CEP",
+ placeholder: "00000-000",
+ format: formatCep,
+ validator: isValidCep,
+ },
+ phone: {
+ label: "Phone",
+ placeholder: "(00) 00000-0000",
+ format: (value) => formatPhone(value, { mask: "nanp" }),
+ validator: isValidPhone,
+ },
+};
+
+type MaskParams = {
+ /** The field the document is typed into. */
+ input: HTMLInputElement;
+ /** The `inputType` of the `input` event. */
+ inputType?: string;
+ /** The formatter of the document being typed. */
+ format: (value: string) => string;
+};
+
+/**
+ * Formats the document typed into `input` in place and keeps the caret next to
+ * the character being edited, so typing, deleting or pasting anywhere works.
+ * Returns the formatted value.
+ */
+export function mask({ input, inputType = "", format }: MaskParams): string {
+ let value = input.value;
+ let caret = input.selectionStart ?? value.length;
+
+ // A deleted separator would come straight back: delete the character next to it.
+ if (inputType.startsWith("delete") && format(value).length > value.length) {
+ if (inputType === "deleteContentBackward") caret -= 1;
+ value = value.slice(0, caret) + value.slice(caret + 1);
+ }
+
+ // Formatting what comes before the caret says where the caret goes.
+ const position = format(value.slice(0, caret)).length;
+
+ input.value = format(value);
+ input.setSelectionRange(position, position);
+
+ return input.value;
+}
+
+type DocumentFieldProps = Omit, "value" | "onChange"> & {
+ kind: DocumentKind;
+ value: string;
+ onChange: (value: string) => void;
+};
+
+export function DocumentField({ kind, value, onChange, ...props }: DocumentFieldProps) {
+ const spec = DOCUMENTS[kind];
+ const valid = spec.validator(value);
+ const complete = valid || value.length === spec.placeholder.length;
+
+ return (
+
+ {spec.label}
+
+ onChange(
+ mask({
+ input: event.currentTarget,
+ inputType: (event.nativeEvent as InputEvent).inputType,
+ format: spec.format,
+ }),
+ )
+ }
+ />
+ {complete && {valid ? "✓ Valid" : "✗ Invalid"} }
+
+ );
+}
diff --git a/docs/snippets/document-field/document-field.vue b/docs/snippets/document-field/document-field.vue
new file mode 100644
index 000000000..e4c8c726a
--- /dev/null
+++ b/docs/snippets/document-field/document-field.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ {{ spec.label }}
+
+ {{ valid ? "✓ Valid" : "✗ Invalid" }}
+
+
diff --git a/docs/snippets/document-field/document-form.ts b/docs/snippets/document-field/document-form.ts
new file mode 100644
index 000000000..da29a0a6e
--- /dev/null
+++ b/docs/snippets/document-field/document-form.ts
@@ -0,0 +1,58 @@
+import { Component, computed, signal } from "@angular/core";
+import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
+import { DOCUMENTS, DocumentField, type DocumentKind } from "./document-field";
+
+@Component({
+ selector: "app-document-form",
+ imports: [ReactiveFormsModule, DocumentField],
+ template: `
+
+ `,
+})
+export class DocumentForm {
+ protected readonly documents = Object.entries(DOCUMENTS).map(([kind, spec]) => ({
+ kind: kind as DocumentKind,
+ label: spec.label,
+ }));
+
+ protected readonly kind = signal("cpf");
+ protected readonly spec = computed(() => DOCUMENTS[this.kind()]);
+ protected readonly form = new FormGroup({
+ value: new FormControl("", {
+ nonNullable: true,
+ validators: (control) =>
+ this.spec().validator(control.value) ? null : { document: true },
+ }),
+ });
+
+ protected get value() {
+ return this.form.controls.value;
+ }
+
+ protected selectKind(event: Event) {
+ this.kind.set((event.target as HTMLSelectElement).value as DocumentKind);
+ this.form.reset();
+ }
+
+ protected submit() {
+ if (this.form.invalid) return this.form.markAllAsTouched();
+
+ console.log(this.form.getRawValue());
+ }
+}
diff --git a/docs/snippets/document-field/document-form.tsx b/docs/snippets/document-field/document-form.tsx
new file mode 100644
index 000000000..7ec35461a
--- /dev/null
+++ b/docs/snippets/document-field/document-form.tsx
@@ -0,0 +1,46 @@
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { DOCUMENTS, DocumentField, type DocumentKind } from "./document-field";
+
+export function DocumentForm() {
+ const [kind, setKind] = useState("cpf");
+ const { control, handleSubmit, reset } = useForm({ defaultValues: { value: "" } });
+ const spec = DOCUMENTS[kind];
+
+ return (
+
+ );
+}
diff --git a/docs/snippets/document-field/document-form.vue b/docs/snippets/document-field/document-form.vue
new file mode 100644
index 000000000..ec6c7eb67
--- /dev/null
+++ b/docs/snippets/document-field/document-form.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/docs/snippets/live/angular.html b/docs/snippets/live/angular.html
deleted file mode 100644
index 8de782530..000000000
--- a/docs/snippets/live/angular.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/snippets/live/document-field-angular.html b/docs/snippets/live/document-field-angular.html
new file mode 100644
index 000000000..e7279035a
--- /dev/null
+++ b/docs/snippets/live/document-field-angular.html
@@ -0,0 +1,8 @@
+
+
+
diff --git a/docs/snippets/live/document-field-react.html b/docs/snippets/live/document-field-react.html
new file mode 100644
index 000000000..e85b6881e
--- /dev/null
+++ b/docs/snippets/live/document-field-react.html
@@ -0,0 +1,8 @@
+
+
+
diff --git a/docs/snippets/live/document-field-vue.html b/docs/snippets/live/document-field-vue.html
new file mode 100644
index 000000000..7ff652842
--- /dev/null
+++ b/docs/snippets/live/document-field-vue.html
@@ -0,0 +1,8 @@
+
+
+
diff --git a/docs/snippets/live/react.html b/docs/snippets/live/react.html
deleted file mode 100644
index 8eaa12125..000000000
--- a/docs/snippets/live/react.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/snippets/live/run.js b/docs/snippets/live/run.js
index 22061e8f0..2422c6971 100644
--- a/docs/snippets/live/run.js
+++ b/docs/snippets/live/run.js
@@ -1,12 +1,15 @@
/**
- * Runs an example of docs/snippets in the browser, for the live demos of examples.md, so each demo
- * is exactly the code the page shows. The page names the example in `data-example`; this script
- * compiles it with Babel (TypeScript, JSX, decorators) and, for a `.vue` file, the Vue SFC
- * compiler, both from a CDN, and mounts it into the page. Nothing is built ahead of time.
+ * Runs an example of docs/snippets in the browser, for the live demos of the example pages, so each
+ * demo is exactly the code the page shows. The page names the folder in `data-dir`, the component
+ * in `data-example` and, when the demo is the component used by another file, that file in
+ * `data-usage`. This script compiles them with Babel (TypeScript, JSX, decorators) and, for a
+ * `.vue` file, the Vue SFC compiler, both from a CDN, and mounts the result. Nothing is built
+ * ahead of time.
*/
(function () {
var CDN = "https://cdn.jsdelivr.net/npm/";
- var example = document.currentScript.dataset.example;
+ var data = document.currentScript.dataset;
+ var example = data.example;
var script = document.createElement("script");
script.type = "importmap";
@@ -22,6 +25,8 @@
"@angular/compiler": CDN + "@angular/compiler@22.1.7/+esm",
"@angular/platform-browser": CDN + "@angular/platform-browser@22.1.7/+esm",
"@angular/forms": CDN + "@angular/forms@22.1.7/+esm",
+ "react-hook-form": "https://esm.sh/react-hook-form@7.88.0?external=react",
+ "vee-validate": "https://esm.sh/vee-validate@4.15.1?external=vue",
},
});
document.head.appendChild(script);
@@ -36,7 +41,7 @@
};
var read = function (name) {
- return fetch("../" + name).then(function (response) {
+ return fetch("../" + data.dir + "/" + name).then(function (response) {
if (!response.ok) throw new Error(name + ": HTTP " + response.status);
return response.text();
});
@@ -52,11 +57,31 @@
document.head.appendChild(babel);
});
+ // Angular's JIT compiler reads the decorators TypeScript emits, which Babel does not reproduce,
+ // so an Angular file goes through TypeScript itself.
+ var transpileTypeScript = function (code, filename) {
+ return import(CDN + "typescript@5.9.3/+esm").then(function (ts) {
+ return ts.default.transpileModule(code, {
+ fileName: filename,
+ compilerOptions: {
+ target: ts.default.ScriptTarget.ES2022,
+ module: ts.default.ModuleKind.ESNext,
+ experimentalDecorators: true,
+ useDefineForClassFields: false,
+ },
+ }).outputText;
+ });
+ };
+
var transpile = function (code, filename) {
return window.Babel.transform(code, {
filename: filename,
presets: [["typescript", { allExtensions: true, isTSX: /\.tsx$/.test(filename) }], ["react", { runtime: "automatic" }]],
- plugins: [["proposal-decorators", { legacy: true }]],
+ // Class properties after the decorators, or Angular's signal inputs never register.
+ plugins: [
+ ["proposal-decorators", { legacy: true }],
+ ["proposal-class-properties", { loose: true }],
+ ],
}).code;
};
@@ -64,10 +89,21 @@
return URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
};
- var compileVue = function (source) {
+ var compileVue = function (name, source) {
return import(CDN + "@vue/compiler-sfc@3.5.43/dist/compiler-sfc.esm-browser.js").then(function (sfc) {
- var descriptor = sfc.parse(source, { filename: example }).descriptor;
- return sfc.compileScript(descriptor, { id: "example", inlineTemplate: true }).content;
+ var descriptor = sfc.parse(source, { filename: name }).descriptor;
+ return sfc.compileScript(descriptor, { id: name, inlineTemplate: true }).content;
+ });
+ };
+
+ // A file of the example: TypeScript, JSX or a single-file component, compiled to a module.
+ var compile = function (name, code) {
+ if (/\.ts$/.test(name)) return transpileTypeScript(code, name);
+
+ var source = /\.vue$/.test(name) ? compileVue(name, code) : Promise.resolve(code);
+
+ return source.then(function (js) {
+ return transpile(js, name.replace(/\.vue$/, ".ts"));
});
};
@@ -99,17 +135,24 @@
});
};
- Promise.all([loadBabel, read(example)])
+ Promise.all([loadBabel, read(example), data.usage ? read(data.usage) : ""])
.then(function (files) {
- return /\.vue$/.test(example) ? compileVue(files[1]) : files[1];
- })
- .then(function (code) {
// Angular's JIT compiler has to be evaluated before any other Angular package, the example
// included: the partially compiled packages look for it as they load.
- return /\.ts$/.test(example) ? import("@angular/compiler").then(function () { return code; }) : code;
+ var angular = /\.ts$/.test(example) ? import("@angular/compiler") : Promise.resolve();
+
+ return angular.then(function () {
+ return Promise.all([compile(example, files[1]), data.usage ? compile(data.usage, files[2]) : ""]);
+ });
})
- .then(function (code) {
- return import(toModule(transpile(code, example.replace(/\.vue$/, ".ts"))));
+ .then(function (compiled) {
+ if (!data.usage) return import(toModule(compiled[0]));
+
+ // The usage imports the component beside it; that import becomes the compiled module.
+ var component = toModule(compiled[0]);
+ var pattern = new RegExp("([\"'])\\./" + example.replace(/\.\w+$/, "") + "(\\.vue)?\\1", "g");
+
+ return import(toModule(compiled[1].replace(pattern, '"' + component + '"')));
})
.then(mount)
.catch(fail);
diff --git a/docs/snippets/live/vue.html b/docs/snippets/live/vue.html
deleted file mode 100644
index 4d706af13..000000000
--- a/docs/snippets/live/vue.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/docs/snippets/usage/signup-form.tsx b/docs/snippets/usage/signup-form.tsx
deleted file mode 100644
index 2e3029202..000000000
--- a/docs/snippets/usage/signup-form.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Controller, useForm } from "react-hook-form";
-import { isValidCpf } from "@brazilian-utils/brazilian-utils";
-import { CpfField } from "./cpf-field";
-
-type SignupValues = {
- cpf: string;
-};
-
-export function SignupForm() {
- const { control, handleSubmit } = useForm({
- defaultValues: { cpf: "" },
- });
-
- return (
-
- );
-}
diff --git a/docs/snippets/usage/signup-form.vue b/docs/snippets/usage/signup-form.vue
deleted file mode 100644
index 8290a6b01..000000000
--- a/docs/snippets/usage/signup-form.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
diff --git a/docs/snippets/usage/signup.ts b/docs/snippets/usage/signup.ts
deleted file mode 100644
index 3afc0a7f2..000000000
--- a/docs/snippets/usage/signup.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Component } from "@angular/core";
-import {
- FormControl,
- FormGroup,
- ReactiveFormsModule,
- type ValidatorFn,
-} from "@angular/forms";
-import { isValidCpf } from "@brazilian-utils/brazilian-utils";
-import { CpfField } from "./cpf-field";
-
-const cpfValidator: ValidatorFn = (control) =>
- isValidCpf(control.value) ? null : { cpf: true };
-
-@Component({
- selector: "app-signup",
- imports: [ReactiveFormsModule, CpfField],
- template: `
-
- `,
-})
-export class Signup {
- protected readonly signup = new FormGroup({
- cpf: new FormControl("", { nonNullable: true, validators: cpfValidator }),
- });
-
- protected submit() {
- if (this.signup.invalid) return this.signup.markAllAsTouched();
-
- console.log(this.signup.getRawValue());
- }
-}
diff --git a/docs/utilities.html b/docs/utilities.html
index 3f2cb2e6d..97adc16cf 100644
--- a/docs/utilities.html
+++ b/docs/utilities.html
@@ -116,7 +116,12 @@
},
alias: {
'/': 'https://raw.githubusercontent.com/brazilian-utils/javascript/main/README.md',
- '/pt-br/': '/pt-br/getting-started.md'
+ '/pt-br/': '/pt-br/getting-started.md',
+ // The example pages live in a folder; they share their language's sidebar and navbar
+ '/examples/_sidebar.md': '/_sidebar.md',
+ '/examples/_navbar.md': '/_navbar.md',
+ '/pt-br/examples/_sidebar.md': '/pt-br/_sidebar.md',
+ '/pt-br/examples/_navbar.md': '/pt-br/_navbar.md'
},
repo: 'brazilian-utils/javascript',
loadNavbar: true,
From b3bd3506f29552bf7ba2bd0d87068785a8fb2e32 Mon Sep 17 00:00:00 2001
From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com>
Date: Sun, 20 Sep 2026 12:47:35 -0300
Subject: [PATCH 32/70] docs(examples): one focused example per document
instead of a generic field
A reader copies the CPF field, not a generic one to narrow down first, so the document picker now
swaps the example itself: four documents by four frameworks, each about its own document and
sharing only the `mask` function. They are generated by `npm run build:examples` from a template
per framework and a table of documents, so the shared parts are written once and CI fails when
they are stale. The twelve demo pages are gone too: one page runs every demo, told which files to
compile by its query string.
---
.github/workflows/check.yml | 3 +
CONTRIBUTING.md | 14 +-
docs/404.html | 106 +++++++--
docs/examples/document-field.html | 110 ++++++---
docs/examples/document-field.md | 135 +++++++++--
docs/getting-started.html | 106 +++++++--
docs/index.html | 106 +++++++--
docs/migration-v1-to-v2.html | 106 +++++++--
docs/pt-br/examples/document-field.html | 106 +++++++--
docs/pt-br/examples/document-field.md | 133 +++++++++--
docs/pt-br/getting-started.html | 106 +++++++--
docs/pt-br/index.html | 106 +++++++--
docs/pt-br/migration-v1-to-v2.html | 106 +++++++--
docs/pt-br/utilities.html | 106 +++++++--
.../document-field/_templates/_mask.ts | 32 +++
.../_templates/angular-field.ts | 71 ++++++
.../document-field/_templates/angular-form.ts | 32 +++
.../document-field/_templates/react-field.tsx | 37 +++
.../document-field/_templates/react-form.tsx | 24 ++
.../document-field/_templates/vanilla.html | 34 +++
.../document-field/_templates/vue-field.vue | 33 +++
.../document-field/_templates/vue-form.vue | 24 ++
.../document-field/document-field.html | 125 ----------
.../document-field/document-field.tsx | 116 ---------
.../document-field/document-field.vue | 117 ----------
docs/snippets/document-field/document-form.ts | 58 -----
.../snippets/document-field/document-form.tsx | 46 ----
.../snippets/document-field/document-form.vue | 38 ---
.../generated/cep/cep-field.html | 57 +++++
.../cep/cep-field.ts} | 90 ++-----
.../generated/cep/cep-field.tsx | 68 ++++++
.../generated/cep/cep-field.vue | 64 +++++
.../document-field/generated/cep/cep-form.ts | 32 +++
.../document-field/generated/cep/cep-form.tsx | 24 ++
.../document-field/generated/cep/cep-form.vue | 24 ++
.../generated/cnpj/cnpj-field.html | 57 +++++
.../generated/cnpj/cnpj-field.ts | 102 ++++++++
.../generated/cnpj/cnpj-field.tsx | 68 ++++++
.../generated/cnpj/cnpj-field.vue | 64 +++++
.../generated/cnpj/cnpj-form.ts | 32 +++
.../generated/cnpj/cnpj-form.tsx | 24 ++
.../generated/cnpj/cnpj-form.vue | 24 ++
.../generated/cpf/cpf-field.html | 57 +++++
.../document-field/generated/cpf/cpf-field.ts | 102 ++++++++
.../generated/cpf/cpf-field.tsx | 68 ++++++
.../generated/cpf/cpf-field.vue | 64 +++++
.../document-field/generated/cpf/cpf-form.ts | 32 +++
.../document-field/generated/cpf/cpf-form.tsx | 24 ++
.../document-field/generated/cpf/cpf-form.vue | 24 ++
.../generated/phone/phone-field.html | 57 +++++
.../generated/phone/phone-field.ts | 102 ++++++++
.../generated/phone/phone-field.tsx | 68 ++++++
.../generated/phone/phone-field.vue | 64 +++++
.../generated/phone/phone-form.ts | 32 +++
.../generated/phone/phone-form.tsx | 24 ++
.../generated/phone/phone-form.vue | 24 ++
.../snippets/live/document-field-angular.html | 8 -
docs/snippets/live/document-field-react.html | 8 -
docs/snippets/live/document-field-vue.html | 8 -
docs/snippets/live/index.html | 4 +
docs/snippets/live/run.js | 35 ++-
docs/styles.css | 7 +-
docs/utilities.html | 106 +++++++--
package.json | 1 +
scripts/examples.ts | 221 ++++++++++++++++++
65 files changed, 3090 insertions(+), 916 deletions(-)
create mode 100644 docs/snippets/document-field/_templates/_mask.ts
create mode 100644 docs/snippets/document-field/_templates/angular-field.ts
create mode 100644 docs/snippets/document-field/_templates/angular-form.ts
create mode 100644 docs/snippets/document-field/_templates/react-field.tsx
create mode 100644 docs/snippets/document-field/_templates/react-form.tsx
create mode 100644 docs/snippets/document-field/_templates/vanilla.html
create mode 100644 docs/snippets/document-field/_templates/vue-field.vue
create mode 100644 docs/snippets/document-field/_templates/vue-form.vue
delete mode 100644 docs/snippets/document-field/document-field.html
delete mode 100644 docs/snippets/document-field/document-field.tsx
delete mode 100644 docs/snippets/document-field/document-field.vue
delete mode 100644 docs/snippets/document-field/document-form.ts
delete mode 100644 docs/snippets/document-field/document-form.tsx
delete mode 100644 docs/snippets/document-field/document-form.vue
create mode 100644 docs/snippets/document-field/generated/cep/cep-field.html
rename docs/snippets/document-field/{document-field.ts => generated/cep/cep-field.ts} (52%)
create mode 100644 docs/snippets/document-field/generated/cep/cep-field.tsx
create mode 100644 docs/snippets/document-field/generated/cep/cep-field.vue
create mode 100644 docs/snippets/document-field/generated/cep/cep-form.ts
create mode 100644 docs/snippets/document-field/generated/cep/cep-form.tsx
create mode 100644 docs/snippets/document-field/generated/cep/cep-form.vue
create mode 100644 docs/snippets/document-field/generated/cnpj/cnpj-field.html
create mode 100644 docs/snippets/document-field/generated/cnpj/cnpj-field.ts
create mode 100644 docs/snippets/document-field/generated/cnpj/cnpj-field.tsx
create mode 100644 docs/snippets/document-field/generated/cnpj/cnpj-field.vue
create mode 100644 docs/snippets/document-field/generated/cnpj/cnpj-form.ts
create mode 100644 docs/snippets/document-field/generated/cnpj/cnpj-form.tsx
create mode 100644 docs/snippets/document-field/generated/cnpj/cnpj-form.vue
create mode 100644 docs/snippets/document-field/generated/cpf/cpf-field.html
create mode 100644 docs/snippets/document-field/generated/cpf/cpf-field.ts
create mode 100644 docs/snippets/document-field/generated/cpf/cpf-field.tsx
create mode 100644 docs/snippets/document-field/generated/cpf/cpf-field.vue
create mode 100644 docs/snippets/document-field/generated/cpf/cpf-form.ts
create mode 100644 docs/snippets/document-field/generated/cpf/cpf-form.tsx
create mode 100644 docs/snippets/document-field/generated/cpf/cpf-form.vue
create mode 100644 docs/snippets/document-field/generated/phone/phone-field.html
create mode 100644 docs/snippets/document-field/generated/phone/phone-field.ts
create mode 100644 docs/snippets/document-field/generated/phone/phone-field.tsx
create mode 100644 docs/snippets/document-field/generated/phone/phone-field.vue
create mode 100644 docs/snippets/document-field/generated/phone/phone-form.ts
create mode 100644 docs/snippets/document-field/generated/phone/phone-form.tsx
create mode 100644 docs/snippets/document-field/generated/phone/phone-form.vue
delete mode 100644 docs/snippets/live/document-field-angular.html
delete mode 100644 docs/snippets/live/document-field-react.html
delete mode 100644 docs/snippets/live/document-field-vue.html
create mode 100644 docs/snippets/live/index.html
create mode 100644 scripts/examples.ts
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 4818c94b4..4512f3bff 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -59,6 +59,9 @@ jobs:
- name: Check the JSR exports are up to date
run: npm run build:jsr && git diff --exit-code -- jsr.json
+ - name: Check the generated examples are up to date
+ run: npm run build:examples && git add --intent-to-add docs && git diff --exit-code -- docs/snippets
+
- name: Check the docs site shells and sitemap are up to date
# --intent-to-add makes a shell written for a new page show up in the diff as well.
run: npm run build:site && git add --intent-to-add docs && git diff --exit-code -- docs/sitemap.xml 'docs/*.html' 'docs/pt-br/*.html'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index da710b5d9..6e69162f1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -45,6 +45,7 @@ and is invoked through the `npm` scripts below, so you don't need to install any
| `npm run build:llms` | Regenerates `docs/llms.txt` and `docs/llms-full.txt` from the docs (`scripts/llms.ts`); CI fails if they're out of date. |
| `npm run build:site` | Regenerates the per-page copies of `docs/index.html`, `docs/404.html` and `docs/sitemap.xml` from the sidebars (`scripts/site.ts`); CI fails if they're out of date. |
| `npm run build:jsr` | Regenerates the `exports` of `jsr.json`, one per utility folder (`scripts/jsr.ts`); CI fails if they're out of date. |
+| `npm run build:examples` | Regenerates the examples of the document field page from their templates (`scripts/examples.ts`); CI fails if they're out of date. |
| `npm run check:dependencies` | Fails if `package.json` declares any runtime `dependencies` (this package ships zero by design). |
| `npm run check:tree-shaking` | Builds nothing; measures the single-import size of every export against `dist` (`scripts/tree-shaking.ts`). Run it after `npm run build` when you change a dataset, and update the bundle-size table in `docs/getting-started.md` / `docs/pt-br/getting-started.md`. |
| `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. |
@@ -375,10 +376,15 @@ browser, with `_sidebar.md`, `_navbar.md` and `_coverpage.md` as its navigation.
the block through `scripts/front-matter.ts`.
- The site's own CSS is `docs/styles.css`, linked by every shell: styles go there, not in a
`