Run the codemod
One ng update ngwr@14, from whatever major you are on: it installs 14, then runs every migration newer than the version you had installed, in order, rewriting templates, TypeScript and stylesheets in place. Commit first, run it, then review with git diff. Do not step through the majors.
# Run the codemod — rewrites templates, TS and stylesheets in place.
# One command, whatever major you are on: ng update applies every migration
# between your version and 14, in order. From v12 that is v13 then v14; from
# v6 it is v7, v8, v9, v12, v13, v14. v10 and v11 ship none — nothing to skip.
# Never target a 7.x, 8.x or 9.x release: their schematics do not load.
#
# v14 rewrites its six renames and REPORTS the rest.
# v13 only reports: it names the files, it edits nothing.
# Read the output of both — the reports are the part that matters.
ng update ngwr@14Never target a 7.x, 8.x or 9.x release — not with ng update ngwr@7, @8 or @9, not with ng add, not with any ng g ngwr:*. Every release from 7.0.0 to 9.1.0 ships CommonJS schematics under a package.json that declares "type": "module", so Node loads them as ES modules and the command dies with exports is not defined in ES module scope. 10.0.0 fixed the packaging. The one command above never loads those files: the migrations it runs, v7, v8 and v9 included, are the ones shipped inside 14. The “migrate each major version individually” refusal the Angular CLI prints is for Angular's own packages — it matches @angular/* and @nguniversal/* and nothing else — and does not apply to ngwr.
If an ng update ngwr@9 already failed with that error, it had moved package.json and your lockfile to 9.x when the migration step crashed, and nothing moved them back: the CLI restores package.json only when the install itself fails. A later ng update ngwr@14 therefore starts from 9.x and silently skips the v9 migration — the <wr-checkbox> rename below, whose leftovers raise no error either. Do not count on reverting the bump: committing it, which is how most people get past Repository is not clean, makes it stick. Run the skipped step by name instead. It works however package.json was left. --name runs the migration shipped inside the installed 14 and checks no version, and it rewrites only <wr-checkbox> bindings, which none of v12, v13 or v14 touches. A failed @7 or @8 leaves the same hole for that major, and for each later one you also tried: run each skipped migration the same way, oldest first.
# Only if an earlier ng update ngwr@9 died with
# "exports is not defined in ES module scope".
ng update ngwr@14 # skip if 14 is already installed
# Commit that result first: ng update refuses a migration on a dirty tree too.
ng update ngwr --migrate-only --name=migration-v9 # the step the range skippedTwo things that stop the first run, and neither is ngwr. Under pnpm 11 a fresh install can exit 1 with ERR_PNPM_IGNORED_BUILDS — its default is to refuse dependency build scripts until the workspace allows them — and a release published within the last day is held back by pnpm's minimum release age, so ng update ngwr@14 can land on the previous patch and report nothing to do. Both are package-manager policy: allow the builds, or wait out the age window, then re-run. The migration itself is idempotent, so running it twice costs nothing.
Upgrading is also the moment to read the range you pinned. A breaking change can no longer arrive in a minor or a patch — the release script refuses that bump — so a caret range on a major is a safe range, which it was not when 12.2.0 shipped one. Versioning & support has that rule, how long the line you are leaving keeps getting security fixes, and what the open-ended Angular peer range does and does not promise.
v13 to v14 — the three that fail silently
Read this section even if your build is green, because these are the three changes nothing will tell you about. Everything else in v14 is either rewritten for you or caught by the compiler; these three compile, run, and quietly do something other than what they did in v13.
<wr-loading-bar> no longer follows the router. The subscription is opt-in now — add provideWrLoadingBarRouter() from ngwr/loading-bar/router to your application providers. Without it the bar still works for manual start() / complete() and simply never moves for a navigation. Nothing throws, nothing warns, and the bar renders exactly as it did before: it just stays at 0%. This project's own site shipped an empty bar for the length of a release by missing precisely this. ng update lists every file that renders one.
A translated pagination.of stops being read. The whole “1–10 of 235” line is one pagination.range template now, so a catalog that still defines of keeps a key nothing looks up — and that one line reverts to English while every other string in the same catalog goes on working, which is the version of this bug that survives a review. Replace it with pagination.range and pagination.compact, whose shapes are in the diff below. common.of is a different key and is untouched — it is still shipped, and still unread.
A date the field cannot parse now returns null instead of a wrong date. The one change here with no codemod entry at all, because there is no token to detect. It turns a wrong answer into no answer, so a host that relied on new Date()'s leniency sees the committed value stay put where it used to change — and a value that stays put looks like a user who has not finished typing. Its own section below has the detail; read it if any date in your app is typed rather than picked.
v13 to v14 — the six renames
Six names moved, and ng update ngwr@14 rewrites every one of them: a rename is exactly what a codemod does well, since the new name means what the old one meant on the same element. <wr-alert closeable> becomes closable, matching the three places that already spelled it that way — and note that neither spelling ever raised a template error, because a bare attribute matching no input is just an attribute. <wr-table [totalItems]> and <wr-pagination [(currentPage)]> become [total] and [(page)], so two components built to sit on the same screen stop using two words for each of the two concepts they share. isDisabledWhenLoading loses its prefix, leaving the library with no is* boolean at all. And the window's chromeSize moves from compact/normal to sm/md — the vocabulary v8 renamed everywhere else three majors ago. Sixth: [wrInput]'s size input is now plain size, like the other twenty-three components. It was spelled with a prefix to dodge the native <input size> attribute, which measures the field in characters and which .wr-input's width: 100% has always overridden — so the clash it guarded against was never observable, and the cost of dropping it is that a native size on an ngwr input, which already did nothing, now does nothing under a different name.
<!-- The size input matches the other twenty-three components. -->
- <input wrInput wrSize="lg" />
+ <input wrInput size="lg" />
<!-- One dismiss spelling. wr-drawer, WrDrawerOptions and -->
<!-- WrDialogOptions already said closable; wr-alert was the outlier. -->
- <wr-alert type="danger" closeable />
+ <wr-alert type="danger" closable />
<!-- Table and pagination now use one word per concept. -->
- <wr-table [totalItems]="count" [(page)]="page" />
+ <wr-table [total]="count" [(page)]="page" />
- <wr-pagination [total]="count" [(currentPage)]="page" />
+ <wr-pagination [total]="count" [(page)]="page" />
- <wr-pagination [currentPage]="p()" (currentPageChange)="p.set($event)" />
+ <wr-pagination [page]="p()" (pageChange)="p.set($event)" />
<!-- The only is-prefixed boolean in the library, against thirty-nine bare ones. -->
- <button wr-btn loading [isDisabledWhenLoading]="false">Save</button>
+ <button wr-btn loading [disabledWhenLoading]="false">Save</button>
// The window chrome finishes the density rename v8 started.
- manager.open(PanelComponent, { chromeSize: 'compact' })
+ manager.open(PanelComponent, { chromeSize: 'sm' })
- .wr-window--chrome-compact { … }
+ .wr-window--chrome-sm { … } Two things the codemod cannot reach. A component class reading one of these off a viewChild() reference is a type error on upgrade, which is the loud failure and needs no help. The quiet one is a test: WrWindowHarness.getChromeSize() answers 'sm' / 'md' now, so an assertion on 'compact' fails as a wrong value rather than as a wrong type. The pagination harness filter moved with its input — WrPaginationHarness.with({ currentPage: 3 }) becomes { page: 3 } — and that one the codemod does rewrite.
One correction, because the release notes said otherwise: v13 announced this rename and did not make it — its subject line reads “input size loses its prefix” while wrSize shipped unchanged in 13.0.0 and 13.0.1. It lands here, in v14. If you already followed that note and wrote size on a v13 input, nothing broke loudly: size is a legal native attribute, so the template compiled and the control quietly fell back to md. On v14 the same markup means what it says.
v13 to v14 — routing is opt-in
Router integration became opt-in, which is what buys back the 66–76 kB of @angular/router every app was paying for whether it routed or not. Beyond the renames above, ng update ngwr@14 REPORTS rather than rewrites: each of these needs a decision a codemod cannot make — the provider belongs in a bootstrap file it cannot identify, and the tab adapter has to reach an imports array it would have to match to a template by convention — so it names the files and leaves them to you. One half of this fails loudly and one silently, and the diff says which.
// The loading bar no longer subscribes to the router on its own.
// Without this it still works for start() / complete() and never moves
// for navigation — nothing throws, which is what makes it worth checking.
providers: [
provideRouter(routes),
provideWrLoadingBarRouter(), // from 'ngwr/loading-bar/router'
]
// A tab carrying routerLink needs the adapter on the strip.
// This half DOES throw, so your build finds it for you.
- imports: [WrTabs, WrTab]
+ imports: [WrTabs, WrTab, WrTabsRouting] // from 'ngwr/tabs/router'
- <wr-tabs>
+ <wr-tabs wrTabsRouting>
// The pagination range is one template now, not a word in the middle.
- pagination: { of: 'von' }
+ pagination: { range: '{{from}}–{{to}} von {{total}}' }v13 to v14 — dates now refuse what they cannot read
The third of the silent three, in full. A NAMED format (shortDate, time, …) used to parse through new Date(), which understands only Anglo-American forms: a German field printing 15.3.2026 read its own output back as 1 January 2001, because new Date('1') is that date and the first keystroke committed. Parsing is now built from the same Intl formatter that printed the string, so it round-trips — and where it cannot read the input it returns null and LEAVES the committed value alone, the rule wr-input-number already followed. If your host relied on new Date()'s leniency (an ISO string in a d.M.y app, say), pass an explicit token [format].
// A named format round-trips now. Nothing to change if you only
// bind [(value)] — this is about what happens when a user TYPES.
<wr-date-picker [(value)]="d" format="shortDate" />
- // de-DE: field shows 15.3.2026, user retypes it, model becomes 1 Jan 2001
+ // de-DE: field shows 15.3.2026, user retypes it, model is 15 March 2026
- // Unreadable input used to commit whatever new Date() made of it
+ // Unreadable input now returns null and leaves the committed value alone
// If you relied on new Date()'s leniency, name the shape you accept:
+ <wr-date-picker [(value)]="d" format="yyyy-MM-dd" />
// Also: MMMM declines beside a day token (ru "15 марта", not "15 март"),
// the `a` token emits the locale's marker (ja 午後, ar م), and every Intl
// call pins calendar: 'gregory' — so a th-TH app prints 2026, not 2569.v13 to v14 — locale comes from LOCALE_ID, and one input is gone
WR_DATE_LOCALE, WrI18n's defaultLocale and availableLocales now resolve from Angular's LOCALE_ID instead of the browser, so an app that sets LOCALE_ID correctly stops formatting dates in whatever language the visitor's browser happens to be; DEFAULT_WR_I18N_CONFIG lost the two fields that encoded the old default. And <wr-pagination ofLabel> is gone — the range is one pagination.range template rather than a word assembled in the middle of it. The codemod reports every one of these, including the static ofLabel form, which is the silent one.
v12 to v13 — the host id
One change reaches templates, and it is the kind that says nothing. [id] on <wr-checkbox>, <wr-radio> and <wr-switch> no longer lands on the HOST element — it goes to the inner <input>, which is where the input was always documented to put it. It only ever reached the host by accident: a static id was written there as a plain attribute AS WELL as fed to the input, so two elements carried the same id, <label for> resolved through getElementById to the unlabelable host (input.labels went from 1 to 0) and document.getElementById returned the wrong node. So a <label for> pointing at one of these got BETTER; what breaks is a selector anchored to the host, and CSS has no error for a selector that stops matching. The v13 migration rewrites nothing and lists every file holding one — whether wr-checkbox#agree should become #agree (the 16px tick) or wr-checkbox:has(#agree) (the whole row) is a decision only you can make. <wr-checkbox-group> and <wr-radio-group> keep their host id and are not affected. v13's other half — readonly and invalid reaching every control — is additive.
<!-- The binding is unchanged — the id just lands somewhere else. -->
<wr-checkbox id="agree">I agree</wr-checkbox>
<!-- A selector anchored to the HOST stops matching, and nothing says so. -->
- ::ng-deep wr-checkbox#agree { margin: 0; }
+ ::ng-deep #agree { margin: 0; } <!-- the inner input: the 16px tick -->
+ ::ng-deep wr-checkbox:has(#agree) { … } <!-- …or the whole row, if that is what you meant -->
// Same shape in TypeScript and in a test locator.
- document.querySelector('wr-checkbox#agree')
+ document.getElementById('agree') // the native input, as the input always documented
<!-- The groups are NOT affected — they keep their host id. -->
wr-checkbox-group#filters { … }v11 to v12
Two breaking changes, and they need opposite treatment. The entry-point move is pure import paths, so the v12 migration rewrites all of it. The i18n helper is a type change, which no codemod should guess at — but the compiler points at every call site, so it is a mechanical fix you can trust your build to find.
The date entry points nest under `ngwr/date`. `ngwr/icon` has always nested its implementations at `ngwr/icon/adapters/lucide` and `…/feather`, while the date ones were flat and hyphenated — one idea spelled two ways depending on which feature you reached for. Only the import specifier changes: every exported symbol keeps its name, and these entry points ship no styles, so no `@use` path moves with them.
| Name | Description | Type | Default |
|---|---|---|---|
'ngwr/date-adapter' | The adapter contract, the locale token and provideWrDateAdapter(). | → 'ngwr/date' | — |
'ngwr/date-adapter-fns' | The date-fns implementation. | → 'ngwr/date/adapters/fns' | — |
'ngwr/date-adapter-luxon' | The luxon implementation. | → 'ngwr/date/adapters/luxon' | — |
// Import paths only — WrDateAdapter, WrDateFnsAdapter, WrLuxonAdapter,
// provideWrDateAdapter and WR_DATE_LOCALE all keep their names.
- import { provideWrDateAdapter } from 'ngwr/date-adapter';
- import { WrDateFnsAdapter } from 'ngwr/date-adapter-fns';
- import { WrLuxonAdapter } from 'ngwr/date-adapter-luxon';
+ import { provideWrDateAdapter } from 'ngwr/date';
+ import { WrDateFnsAdapter } from 'ngwr/date/adapters/fns';
+ import { WrLuxonAdapter } from 'ngwr/date/adapters/luxon';`readI18nText()` returns `Signal<string>`. Add `()` at every read. This is a fix rather than a refactor, and the reason matters if you have ever wondered why a component announced itself in the wrong language: `WrI18n` writes every loader-backed catalog from a promise, which resolves a microtask AFTER the first change-detection pass — so the old one-shot read froze the ENGLISH fallback for the life of the app. A `<wr-date-picker>` said “Открыть календарь” on the field and “Open calendar” on the button beside it, off the same key. A synchronous read cannot be correct against an asynchronous catalog, so the type had to move. Only affects code that calls the helper directly; the `wrT` pipe, the `[wrT]` directive and `useI18nText` are unchanged.
// `readI18nText` returns a Signal now. Every read needs a call.
protected readonly label = readI18nText('datePicker.open', 'Open calendar');
- <button [attr.aria-label]="label">
+ <button [attr.aria-label]="label()">v8 to v9
Three breaking changes. The codemod auto-fixes the checkbox rename; the other two are one-line manual edits — and both of those fail SILENTLY, so check them even if your build is green.
`<wr-checkbox>` — `value` is now `checkboxValue`. The checkbox became a signal-forms native `FormCheckboxControl`, whose contract reserves `value` for the form value — so its boolean state is the `checked` model, and the group-membership identity moved to `checkboxValue`. Watch out: the static form fails silently. `<wr-checkbox value="x">` raises no template error, it just lands as a plain DOM attribute, leaving every child of the group with the same `null` identity — so they all toggle together. The codemod rewrites `value` / `[value]` / `[(value)]` scoped to the `<wr-checkbox` tag, and deliberately leaves `<wr-checkbox-group>`'s own `value` alone.
| Name | Description | Type | Default |
|---|---|---|---|
value | Renamed — the group-membership identity. FormCheckboxControl reserves value. | → checkboxValue | — |
checked | Now the form value — a two-way model(). Bind [(checked)], [formField] or [(ngModel)]. | model<boolean> | — |
<!-- Inside <wr-checkbox-group>: the identity input was renamed. -->
- <wr-checkbox value="autosave">Autosave</wr-checkbox>
+ <wr-checkbox checkboxValue="autosave">Autosave</wr-checkbox>
<!-- Standalone: the boolean form value is the checked model. -->
- <wr-checkbox [(ngModel)]="agree">I agree</wr-checkbox>
+ <wr-checkbox [(checked)]="agree">I agree</wr-checkbox>
<!-- The group itself keeps its own value input — do not rename it. -->
<wr-checkbox-group [(value)]="features">…</wr-checkbox-group>Lucide icon names register verbatim. `lucideIcons()` no longer kebab-cases its keys, so a `chevronDown` key now registers as `chevronDown`, not `chevron-down`. No codemod — and nothing errors, the icon simply stops rendering. Either quote the key or update the usages.
// Icon names are now registered VERBATIM — no kebab-casing.
// A camelCase key used to register as 'chevron-down'; now it stays 'chevronDown'.
- provideWrIcons(lucideIcons({ chevronDown: ChevronDown })) // <wr-icon name="chevron-down">
+ provideWrIcons(lucideIcons({ 'chevron-down': ChevronDown })) // keeps <wr-icon name="chevron-down">
// …or leave the key and update the usages instead:
+ provideWrIcons(lucideIcons({ chevronDown: ChevronDown })) // <wr-icon name="chevronDown">`info` added to `WR_COLORS` / `WrColor`. Additive at runtime, but it widens the union — an exhaustive `switch` over `WrColor` without a `default` will stop compiling until you handle the new member.
v7 to v8
Three breaking changes. The codemod auto-fixes density + pagination and warns (with file paths) about the removed components, which have no automatic replacement.
Density values renamed. `compact` / `default` / `comfortable` become `sm` / `md` / `lg`; `touch` is unchanged and `md` is now the default. Affects `provideWrDensity`, the `wrDensity` directive, and the `[data-wr-density]` selector.
| Name | Description | Type | Default |
|---|---|---|---|
'compact' | Renamed. | → 'sm' | — |
'default' | Renamed — and is now the default. | → 'md' | — |
'comfortable' | Renamed. | → 'lg' | — |
'touch' | Unchanged. | 'touch' | — |
// Provider
- provideWrDensity({ defaultDensity: 'comfortable' })
+ provideWrDensity({ defaultDensity: 'lg' })<!-- Directive + selector -->
- <aside wrDensity="compact">…</aside>
+ <aside wrDensity="sm">…</aside>
- [data-wr-density='comfortable'] { … }
+ [data-wr-density='lg'] { … }Pagination sizes trimmed. `WrPaginationSize` drops `xs` and `xl` — it is now `'sm' | 'md' | 'lg'`.
| Name | Description | Type | Default |
|---|---|---|---|
size="xs" | Dropped. | → "sm" | — |
size="xl" | Dropped. | → "lg" | — |
Removed components. `WrReveal` (the `wrReveal` directive in `ngwr/directives`) and `WrScrambleText` (`ngwr/scramble-text`) were removed — both were unreliable. There is no drop-in replacement; the codemod lists every file that uses them so you can swap them out.
<!-- WrReveal — removed, no replacement. Drop the directive + its inputs. -->
- <div wrReveal [threshold]="0.5">…</div>
+ <div>…</div>
<!-- WrScrambleText — removed. Use <wr-decrypt-text> (or plain text). -->
- <wr-scramble-text>Hover me</wr-scramble-text>
+ <wr-decrypt-text text="Hover me" />v6 to v7
v7 consolidated ten standalone entry-points into shared components. The v7 migration rewrites the tags, TypeScript imports and SCSS @use paths — and it runs as the first step of ng update ngwr@14, so a v6 app does not climb the ladder one major at a time.
Key renames: `wr-autocomplete` / `wr-chips-input` → `wr-select` (with `mode`); `wr-time-picker` / `wr-date-time-picker` → `wr-date-picker`; `wr-tooltip` → `wr-popover`; `wr-tree-select` → `wr-tree`; `wr-bottom-sheet` → `wr-drawer`; `wr-image` → `wr-lightbox`; `ngwr/tag` → `ngwr/badge`; `wr-animated-text` → `wr-typewriter` / `wr-decrypt-text` / `wr-split-text` (per mode). See the Schematics page for the full collection.