diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 232 |
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). |