summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 15:01:02 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 15:01:02 -0300
commit36486007ccada8c31d30e8f528d21233cabce32c (patch)
treed14a108845e4c984ec922ba98d153c8d68da6ebb /README.md
downloadautofill-36486007ccada8c31d30e8f528d21233cabce32c.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md232
1 files changed, 232 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..242859a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,232 @@
+# cepis-autofill
+
+Drop-in address autofill for Brazilian checkouts, on top of the
+[public CEP API at cep.is](https://cep.is). The shopper types a CEP; the
+widget fills the street, neighbourhood, city and state fields you point it
+at. Official Correios data (DNE).
+
+One `<script>` tag, no build step, no framework, no dependencies.
+**3.3 KB minified and gzipped** (7.7 KB minified, 7859 bytes).
+
+```html
+<script src="cepis-autofill.js"></script>
+<script>
+ CepisAutofill.attach({
+ cep: "#cep",
+ logradouro: "#endereco",
+ bairro: "#bairro",
+ localidade: "#cidade",
+ uf: "#estado",
+ numero: "#numero",
+ });
+</script>
+```
+
+Nothing about any particular e-commerce platform is baked in: which field
+is which is configuration. See
+[Mapping your platform's fields](#mapping-your-platforms-fields).
+
+## Install
+
+Copy `dist/cepis-autofill.min.js` next to your theme's other assets and load
+it with a `<script>` tag. It defines the global `CepisAutofill` and nothing
+else.
+
+Bundling instead? `dist/cepis-autofill.esm.js` is the same code as an ES
+module:
+
+```js
+import { attach } from "./cepis-autofill.esm.js";
+```
+
+Requires a browser with `fetch` and `AbortController` — every browser since 2017. Run `make build` to produce `dist/`.
+
+## Usage
+
+Give `attach` the field the shopper types into, plus the fields to fill.
+Every selector accepts a CSS string or an element:
+
+```js
+const handle = CepisAutofill.attach({
+ cep: "#cep",
+ logradouro: "#endereco",
+ bairro: "#bairro",
+ localidade: "#cidade",
+ uf: "#estado",
+ numero: "#numero",
+ focusNumero: true,
+});
+```
+
+A lookup goes out once the field holds 8 digits and the shopper stops
+typing. The `01001-000` mask is accepted. Repeat lookups of the same CEP are
+served from memory, so going back to fix the house number costs nothing.
+
+A `<select>` for the state is matched by option value or label, against both
+the two-letter UF and the full state name — `SP` and `São Paulo` both work.
+The `numero` field is never written to; it exists so `focusNumero` knows
+where to send the cursor.
+
+`attach` returns a handle:
+
+```js
+handle.lookup(); // run now, skipping the debounce
+handle.lookup("01001-000"); // …for a specific CEP
+handle.detach(); // remove listeners, abort anything in flight
+```
+
+Call `handle.lookup()` when your checkout writes into the CEP field without
+firing an `input` event — a "use my saved address" button, for instance.
+
+### Options
+
+| option | default | what it does |
+| ----------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------- |
+| `cep` | — | the field the shopper types into (required) |
+| `logradouro`, `complemento`, `unidade`, `bairro`, `localidade`, `uf`, `estado`, `regiao`, `ibge`, `gia`, `ddd`, `siafi` | — | fields to fill; map only the ones you have |
+| `numero` | — | house-number field, for `focusNumero` |
+| `baseUrl` | `https://cep.is` | API root; point it at your own instance |
+| `debounceMs` | `250` | quiet period after the last keystroke |
+| `timeoutMs` | `8000` | deadline for one request |
+| `overwrite` | `"auto"` | what to do with a field that already has a value |
+| `focusNumero` | `false` | move focus to `numero` after a successful lookup |
+| `status` | auto | where to announce progress |
+| `messages` | see below | override any status message |
+| `onResult` | — | `(address, { cep }) => void`, after the fields are filled |
+| `onError` | — | `(error, { cep }) => void` |
+| `onStatus` | — | `(state, message) => void` for a custom UI |
+
+`overwrite` decides what happens to a field that is not empty:
+
+- `"auto"` — fill empty fields, replace values this widget wrote itself, and
+ leave anything the shopper typed alone. A manual correction survives the
+ next CEP.
+- `false` — only ever fill empty fields.
+- `true` — always overwrite.
+
+The widget writes through the native value setter and dispatches `input` and
+`change`, so validation in the page — including React's — sees the values.
+It never reformats what the shopper typed in the CEP field.
+
+### Status and accessibility
+
+Unless you pass `status`, a `<div role="status" aria-live="polite">` is
+inserted right after the CEP field and referenced from its
+`aria-describedby`. It is created empty, at a fixed minimum height, so
+announcing something never shifts the page. Focus is never taken, except by
+`focusNumero`, which moves it only while the CEP field still has focus.
+
+Style it through `.cepis-autofill-status[data-cepis-state="…"]`, where the
+state is `idle`, `loading`, `found` or `error`. The message text carries the
+meaning on its own — do not use colour as the only signal:
+
+```css
+.cepis-autofill-status[data-cepis-state="error"]::before {
+ content: "! ";
+}
+```
+
+Pass `status: "#my-node"` to use an element of your own, or `status: false`
+to render nothing and drive your own UI from `onStatus`.
+
+### Messages and errors
+
+Every failure has its own message, in Portuguese, and reaches both the
+status region and `onError`:
+
+| `error.code` | message | worth retrying? |
+| ------------------ | ------------------------------------------------------------------------- | --------------- |
+| `invalid` | CEP inválido. Digite os 8 dígitos. | no |
+| `not_found` | CEP não encontrado. Confira o número ou preencha o endereço manualmente. | no |
+| `rate_limited` | Muitas consultas em pouco tempo. Aguarde alguns segundos e tente de novo. | later |
+| `server` | O serviço de CEP está indisponível. Preencha o endereço manualmente. | later |
+| `network` | Não foi possível consultar o CEP. Verifique sua conexão e tente de novo. | later |
+| `timeout` | A consulta do CEP demorou demais. Tente de novo ou preencha manualmente. | later |
+| `invalid_response` | Resposta inesperada do serviço de CEP. Preencha o endereço manualmente. | probably not |
+
+Override any of them with `messages`:
+
+```js
+CepisAutofill.attach({
+ cep: "#cep",
+ localidade: "#cidade",
+ messages: { not_found: "Não achamos esse CEP. Digite o endereço." },
+});
+```
+
+`rate_limited` carries `retryAfterMs` and `server` carries `status`.
+
+### Looking a CEP up yourself
+
+`CepisAutofill.lookup` is the request layer on its own — same cache, same
+errors, no DOM:
+
+```js
+const address = await CepisAutofill.lookup("01001-000");
+address.logradouro; // "Praça da Sé"
+address.uf; // "SP"
+```
+
+It resolves to the API's own JSON: Portuguese field names, empty strings
+rather than nulls, and `geo` present only for the roughly 57% of CEPs that
+have a coordinate.
+
+## Mapping your platform's fields
+
+Every checkout names its fields differently, and the same platform names
+them differently between themes, versions and one-page checkout apps. **No
+per-platform preset is shipped here, because none could be verified against
+a live store.** Finding yours takes a minute:
+
+1. Open your checkout, right-click the CEP field, Inspect.
+2. Read the `id` or `name` on the `<input>`. An `id` gives you `#billing_postcode`;
+ a `name` gives you `[name="postalCode"]`.
+3. Do the same for street, neighbourhood, city, state and number.
+4. Paste this in the console to confirm every selector resolves:
+
+```js
+["#cep", "#endereco", "#bairro", "#cidade", "#estado", "#numero"].forEach((s) =>
+ console.log(s, document.querySelector(s)),
+);
+```
+
+Anything printing `null` is wrong. A selector that resolves to nothing is
+skipped silently, which is what makes a wrong one hard to spot.
+
+Two things worth knowing for checkouts that re-render (VTEX, Shopify and
+most React-based ones):
+
+- Pass **CSS strings, not elements**. Strings are resolved on every use and
+ the listener is delegated from `document`, so a form that is destroyed and
+ rebuilt keeps working. Elements are held directly and stop working when
+ that node is replaced.
+- Load the widget after the checkout has rendered its form once, or call
+ `attach` from whatever hook the platform gives you for "checkout ready".
+
+Where the CEP field is a component that writes its value without firing
+`input`, keep the handle and call `handle.lookup()` from the component's own
+change hook.
+
+## Development
+
+```sh
+make # list the targets
+make test # bun test — drives a real Chromium against a fake API
+make lint # prettier --check plus a bundle syntax check
+make format # apply prettier
+make build # bundle to dist/ and print the gzipped size
+make check # lint, then test, then build
+```
+
+The tests stand up two `Bun.serve` instances — one serving the checkout
+fixture with a freshly built bundle, one impersonating cep.is — and drive
+Chromium through Playwright: real keystrokes, real `fetch`, real aborts.
+They need a Chromium build; install one with
+`bunx playwright install chromium`, or point `CEPIS_CHROMIUM` at an existing
+binary.
+
+`demo.html` runs against the real API. Run `make build` and open it.
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).