This migration guide is intended to help users migrate from Nova Angular 5.x to Nova Angular 6.x. For other version updates, refer to the changelog.
- Overview
- Prerequisites
- Migration steps
- Breaking changes
- Benefits after migration
- Troubleshooting
- Looking forward
Nova Angular 6 represents the largest architectural change to the library since its release. The codebase has been refactored to be more efficient and leverage the latest Angular features, with a focus on:
- Zoneless Angular: Removed Zone.js dependency for more efficient change detection
- Signals-based: Migrated to Angular's signals for reactive state management
- User priority: Inputs now take precedence over internal state
This guide will help you navigate the migration process from Nova Angular 5.1.3 or higher to version 6.
Before starting the migration:
- Update to Nova Angular 5.2.0+: 5.2.0 is the earliest Nova Angular version that supports an overlapping Angular version as 6.0.0. For the simplest migration, update to 5.2.2. This version includes deprecation warnings to ease the upgrade process
- Update to Angular 18: Nova Angular 6 requires Angular 18 or higher. Follow Angular's Update Guide.
- Audit your codebase: Identify where you're using Nova Angular components, especially any custom implementations or service injections
Follow these steps to migrate your application to Nova Angular 6:
npm install @visa/nova-angular@latestNow that you have updated to 6.0.0, you can move to an Angular version higher than 18. Follow Angular's Update Guide to go from the version you're on to a new version.
If you're using components that require specific functionality:
- For Dialog components:
npm install @angular/cdk - For Icon components:
npm install @visa/nova-icons-angular
Some package managers may require you to install our peer dependency packages if you don't have them already:
"@angular/forms": "^18 || ^19 || ^20""@floating-ui/dom": "^1.6.5""@visa/nova-styles": "^1.6.3"
If you are importing the entire NovaLibModule rather than individual components, you can skip this step.
If you are importing specific components or services that have been renamed or removed, update your import statements accordingly. These include:
| Before | After |
|---|---|
AvatarRoleImgDirective |
AvatarDirective |
ButtonAsDisabledATag |
ButtonDirective |
ButtonDisabledDirective |
ButtonDirective |
ButtonIconDirective |
ButtonDirective |
ButtonStackedDirective |
ButtonDirective |
DialogComponent |
DialogDirective |
PanelComponent |
PanelDirective |
TabItemDisclosureDirective |
TabItemDirective |
TbodyDirective |
removed |
TrDirective |
removed |
TypographyColor |
TypographyDirective |
SwitchLabel |
LabelDirective |
While we tried to mitigate breaking changes as much as possible, some were unavoidable. Follow the Breaking Changes section below to update your components based on the changes in Nova Angular 6.
Nova Angular 6 uses a new pattern for input properties that prioritizes user inputs over internal state. Update your component usage to take advantage of this pattern:
Typescript
// Before (Nova Angular 5)
@ViewChild(ListboxDirective) listbox: ListboxDirective;
handleReset() {
this.listbox.value = '';
}
// After (Nova Angular 6)
readonly value: WritableSignal<SingleSelectValue | null> = signal<SingleSelectValue | null>(null);
handleReset() {
this.value.set(null);
}HTML
// Before (Nova Angular 5)
<ul v-listbox...>...</ul>
// After (Nova Angular 6)
<ul v-listbox [(value)]="value">...</ul>Familiarize yourself with the latest Angular best practices and implement them into your application. We specifically recommend updating to the new control flow.
Thoroughly test your application to ensure all components are functioning correctly after the migration.
- Dropped Angular 16/17 support: Nova Angular 6 requires Angular 18 or higher
The following dependencies have been removed and are now optional:
@angular/cdk@angular/routerrxjszone.js
If your application requires these dependencies, you'll need to install them separately.
The following services have been removed and replaced:
-
AccordionService
- Replaced with HTML details/summary elements
- Migration: Use native HTML elements instead
- Replaced with HTML details/summary elements
-
PaginationService
- Replaced with
PaginationControl- Migration: Update your pagination implementation to use
PaginationControl
- Migration: Update your pagination implementation to use
- Example:
- Replaced with
<!-- Before (elements stripped of aria and style inputs for comparison) -->
<nav>
<ul v-pagination>
<li>
<button
v-button-icon
[disabled]="isFirst()"
(click)="changePage(startPage)"
>
<svg v-icon-visa-arrow-start-tiny></svg>
</button>
</li>
<li>
<button
v-button-icon
[disabled]="isFirst()"
(click)="changePage(currentPage-1)"
>
<svg v-icon-visa-chevron-left-tiny></svg>
</button>
</li>
<li>
<button
(click)="changePage(startPage)"
[aria-current]="currentPage === startPage ? 'page' : false"
v-button
>
{{startPage}}
</button>
</li>
<li v-pagination-overflow *ngIf="pagesExceedBlockLimit && showStartingEllipses">
<svg v-icon-visa-option-horizontal-tiny></svg>
</li>
<ng-container *ngFor="let page of visiblePages()">
<li>
<button
(click)="changePage(page)"
[aria-current]="currentPage === page ? 'page' : false"
v-button
>
{{page}}
</button>
</li>
</ng-container>
<li v-pagination-overflow *ngIf="showEndingEllipses && pagesExceedBlockLimit">
<svg v-icon-visa-option-horizontal-tiny></svg>
</li>
<li>
<button
(click)="changePage(lastPage)"
[aria-current]="currentPage == lastPage ? 'page' : false"
v-button
>
{{lastPage}}
</button>
</li>
<li>
<button
v-button-icon
[disabled]="isLast()"
(click)="changePage(currentPage+1)"
>
<svg v-icon-visa-chevron-right-tiny></svg>
</button>
</li>
<li>
<button
v-button-icon
[disabled]="isLast()"
(click)="changePage(lastPage)"
>
<svg v-icon-visa-arrow-end-tiny></svg>
</button>
</li>
</ul>
</nav>
<!-- After (elements stripped of aria and style inputs for comparison) -->
<nav>
<ul v-pagination>
<li>
<button
v-button-icon
[disabled]="paginationControl.isFirstPage()"
(click)="paginationControl.goToFirstPage()"
>
<svg v-icon-visa-arrow-start-tiny />
</button>
</li>
<li>
<button
v-button-icon
[disabled]="paginationControl.isFirstPage()"
(click)="paginationControl.goToPreviousPage()"
>
<svg v-icon-visa-chevron-left-tiny />
</button>
</li>
<!-- create all the visible pages here -->
@for (page of paginationControl.pages(); track page) {
@if (page !== -1) {
<li>
<button
[aria-current]="paginationControl.isCurrentPage(page) ? 'page' : false"
v-button
(click)="paginationControl.goToPage(page)"
>
{{ page }}
</button>
</li>
} @else {
<li v-pagination-overflow>
<svg v-icon-visa-option-horizontal-tiny />
</li>
}
}
<li>
<button
v-button-icon
[disabled]="paginationControl.isLastPage()"
(click)="paginationControl.goToNextPage()"
>
<svg v-icon-visa-chevron-right-tiny />
</button>
</li>
<li>
<button
v-button-icon
[disabled]="paginationControl.isLastPage()"
(click)="paginationControl.goToLastPage()"
>
<svg v-icon-visa-arrow-end-tiny />
</button>
</li>
</ul>
</nav>// Before
currentPage: number = 1;
middleLimit: number = 5;
startEndLimit: number = 5;
startPage: number = 1;
totalPages: number = 100;
lastPage = this.totalPages + this.startPage - 1;
visiblePages = computed(() => this.pagination?.paginationService.visiblePages());
isFirst = computed(() => this.pagination?.paginationService.isFirst());
isLast = computed(() => this.pagination?.paginationService.isLast());
pagesExceedBlockLimit = true;
showStartingEllipses: boolean = false;
showEndingEllipses: boolean = true;
changePage(page: number) {
this.currentPage = this.pagination.paginationService.changePage(page);
this.showStartingEllipses = this.currentPage >= this.startEndLimit + this.startPage;
this.showEndingEllipses = this.currentPage <= this.lastPage - this.startEndLimit;
}
ngAfterViewInit() {
if (this.pagination) {
this.pagination.paginationService.currentPage = this.currentPage;
this.pagination.paginationService.middleLimit = this.middleLimit;
this.pagination.paginationService.startEndLimit = this.startEndLimit;
this.pagination.paginationService.startPage = this.startPage;
this.pagination.paginationService.totalPages = this.totalPages;
if (this.totalPages <= this.startEndLimit) {
this.pagesExceedBlockLimit = false;
}
this.visiblePages = computed(() => this.pagination.paginationService.visiblePages());
this.isFirst = computed(() => this.pagination.paginationService.isFirst());
this.isLast = computed(() => this.pagination.paginationService.isLast());
this.pagination.paginationService.initializePages(this.currentPage);
}
}
// After
readonly paginationControl = new PaginationControl({
blockMaxLength: 5,
defaultSelected: 1,
defaultTotalPages: 100
});-
UuidGenerator
- Replaced with
IdGeneratorfor deterministic IDs- Replace
UuidGeneratorwithIdGenerator
- Replace
- Example:
// Before id = this.uuidGenerator.generate(); // e.g., "f47ac10b-58cc-4372-a567-0e02b2c3d479" // After id = this.idGenerator.newId('button'); // e.g., "button-0"
- Replaced with
-
AppReadyService
- Not entirely removed, but it's recommended to use
afterNextRenderfrom@angular/corewhere possible- Consider replacing
AppReadyServiceusage withafterNextRenderfor better performance and compatibility with zoneless Angular
- Consider replacing
- Not entirely removed, but it's recommended to use
-
NovaLibService
getCurrentRoutemethod androuteChangeobservable are removed- Use Angular's Router and toSignal instead (see breadcrumb component examples)
selectItems,selectItem,deselectItems,deselectItemare deprecated from this service and moved toListboxServicesetAriaCurrentmethod is deprecated - usehandleAriaCurrentfor a list of LinkDirectives or manipulate the property via template binding to[attr.aria-current]
- Nearly all internal properties have been migrated to signals
- If you inject directives programmatically, you'll need to update how you interface with them
- Recommendation: Interface with components through HTML inputs rather than directly accessing properties
- The custom button markup is no longer supported. If you haven't yet, move to the native details and summary markup. Before:
<div v-accordion>
<button v-button v-accordion-heading>
<v-icon-visa-toggle>
<svg v-toggle-default-template v-icon-visa-chevron-right-tiny></svg>
<svg v-toggle-rotated-template v-icon-visa-chevron-down-tiny></svg>
</v-icon-visa-toggle>
Accordion title
</button>
<div v-accordion-panel>This is required text that describes the accordion section in more detail.</div>
...
</div>After:
<div v-accordion>
<details v-accordion-item>
<summary v-accordion-heading v-button>
<v-icon-visa-toggle>
<svg v-toggle-default-template v-icon-visa-chevron-right-tiny />
<svg v-toggle-rotated-template v-icon-visa-chevron-down-tiny />
</v-icon-visa-toggle>
<div vFlex vAlignItemsCenter vGap="6">Accordion title</div>
</summary>
<div v-accordion-panel>This is required text that describes the accordion section in more detail.</div>
</details>
...
</div>ToggleIconenum moved toIconToggleto simplify enums offered.
| Before | After |
|---|---|
ToggleIcon.EXPANDED |
IconToggle.ACCORDION_EXPANDED |
ToggleIcon.COLLAPSED |
IconToggle.ACCORDION_COLLAPSED |
AnchorLinkMenuHeaderno longer appliesidby default- Users can no longer access
AnchorLinkMenuDirective.ariaLabelas the property was removed. Use the HTMLaria-labelproperty as needed.
- Removed
BaseInteractiveDirective - Replace event handlers:
Before:
<input v-input (blurred)="handleBlur($event)" (clicked)="handleClick($event)" (focused)="handleFocus($event)"/>After:
<input v-input
(blur)="handleBlur($event)" (click)="handleClick($event)" (focus)="handleFocus($event)"/>aria-controlsis a no-op and was removed. Replace[aria-controls]with the HTML attribute[attr.aria-controls]oraria-controlsdepending on usage.
- Users can no longer access
CheckboxDirective.valueas the property was a no-op and is removed.
- No longer supports setting the value to an empty string (
'')- To reset the value, set it to
null,{ label: '', value: ''}, or{ label: '', value: []}
- To reset the value, set it to
autoFilterDisplayedItemsis deprecated.- Use
autoFilter(combobox, listItems, 'label')instead, where listItems is the same list you pass to the combobox template. - If you copied our documentation logic, this will allow you to drop the
extractListmethod Before
- Use
filteredItems = this.cardTypes;
ngAfterViewInit(): void {
if (this.combobox) {
// ComboboxService provider needed to get unique reference to filteredListEmitter
this.combobox.filteredListEmitter.subscribe((listItems: ListboxItemComponent[]) => {
this.extractList(listItems);
});
this.comboboxService.autoFilterDisplayedItems(this.combobox);
this.cdRef.detectChanges(); // force change detection for initial view
// autoSelectItem MUST be called after autoFilterDisplayedItems
this.comboboxService.autoSelectItem(this.combobox);
}
}
/**
* This function takes the ListboxItems[] and transforms it into the filtered array of the same shape of cardTypes ([{ label: '', value: '' }])
* @param listItems
*/
extractList(listItems: ListboxItemComponent[]) {
let values: (string | number)[] = [];
if (!listItems.length) this.filteredItems = [];
listItems.forEach((item: ListboxItemComponent) => {
if (item.value) values.push(item.value);
});
this.filteredItems = this.cardTypes.filter((item) => values.includes(item.value));
}After
readonly filteredItems = signal<ListboxItemType[]>(this.cardTypes);
ngAfterViewInit(): void {
// ComboboxService provider needed to get unique reference to filteredListEmitter
this.combobox().filteredListEmitter.subscribe((listItems: ListboxItemType[]) => {
this.filteredItems.set(listItems);
});
this.comboboxService?.autoFilterBasedOnList(this.combobox(), this.cardTypes);
this.comboboxService?.autoSelectItem(this.combobox());
}v-comboboxno longer addsv-floating-ui-containerlogic by default- Add both
v-combobox v-floating-ui-containerto your element
- Add both
Before:
<div v-combobox>...</div>After:
<div v-combobox v-floating-ui-container>...</div>- Per Angular best practices, value recognition is now top-down. To bind a value to combobox, ensure it is on
v-comboboxrather thanv-listboxorv-input
Before:
The code below shows a value passed to a listbox directly, but the same is true if you are passing active to a v-listbox-item or if you were passing value to v-input directly.
<div v-combobox>
...
<div v-listbox-container v-floating-ui-element>
<ul v-listbox value="option-a">
<li v-listbox-item value="option-a">Option A</li>
<li v-listbox-item value="option-b">Option B</li>
</ul>
</div>
</div>After:
<div v-combobox v-floating-ui-container [value]="{ label: 'Option A', value: 'option-a'}">
...
<div v-listbox-container v-floating-ui-element>
<ul v-listbox>
<li v-listbox-item value="option-a">Option A</li>
<li v-listbox-item value="option-b">Option B</li>
</ul>
</div>
</div>DialogComponentconverted toDialogDirective- Requires focus trapping implementation
- Wrap the content of the
<dialog>with<div cdkTrapFocus cdkTrapFocusAutoCapture ...>as shown below.
- Wrap the content of the
- No longer applies id by default
Before:
import { Component, ElementRef, viewChild } from '@angular/core';
import { DialogComponent, NovaLibModule } from '@visa/nova-angular';
@Component({
imports: [NovaLibModule],
selector: "some-component",
standalone: true,
template: `<dialog v-dialog v-message>
<div v-message-content ...>
{{ ... }}
</div>
<button
v-button-icon
...
>
{{ ... }}
</button>
</dialog>`
})
export class SomeComponent(){
readonly dialog = viewChild<DialogComponent, ElementRef<HTMLDialogElement>>(DialogComponent, {
read: ElementRef
});
toggleDialog(open: boolean) {
open ? this.dialog()?.nativeElement.showModal() : this.dialog()?.nativeElement.close();
}
}After:
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { Component, ElementRef, viewChild } from '@angular/core';
import { DialogDirective, NovaLibModule } from '@visa/nova-angular';
@Component({
imports: [NovaLibModule, CdkTrapFocus],
selector: "some-component",
standalone: true,
template: `<dialog v-dialog v-message>
<div cdkTrapFocus cdkTrapFocusAutoCapture vFlex vFlexRow>
<div v-message-content ...>
{{ ... }}
</div>
<button
v-button-icon
...
>
{{ ... }}
</button>
</div>
</dialog>`
})
export class SomeComponent(){
readonly dialog = viewChild<DialogDirective, ElementRef<HTMLDialogElement>>(DialogDirective, {
read: ElementRef
});
toggleDialog(open: boolean) {
open ? this.dialog()?.nativeElement.showModal() : this.dialog()?.nativeElement.close();
}
}DropdownMenuDirective-zIndexrenamed toz-indexto align with native CSS- Padding has been built into the class via Nova Styles, and you may no longer need to add any padding utilities depending on your version of Nova Styles.
Before:
<button v-dropdown-item vPX="8" vPY="11">Label 1</button>After:
<button v-dropdown-item>Label 1</button>FloatingUIElementDirective-zIndexrenamed toz-indexto align with native CSS
- No longer supports variant with radio and checkbox components embedded (styles are still available, but state management is limited)
v-listbox-itemno longer supportsinvalidsince there is no single listbox item invalid state.- set the invalid state on the
v-listboxinstead
- set the invalid state on the
- Deprecated Listbox service method
selectContiguousItems- use
selectContiguousItemsinstead
- use
PanelDirectiveno longer appliesidby default- Converted
PanelComponenttoPanelDirective - Make sure
<button v-panel-toggle>...</button>is the first child of<div v-panel>...</div>if your panel has a panel toggle
Before:
<dialog
v-panel
expandable
vML="auto"
[expanded]="panelOpen()"
aria-describedby="modal-expandable-subtitle"
aria-labelledby="modal-expandable-title"
>
@if(panelOpen()){
<div v-panel-content>
<header vFlex vJustifyContentBetween vGap="4">
<h3 vTypography="headline-4" id="modal-expandable-title">Panel title</h3>
</header>
<div v-panel-body>
<h4 vTypography="subtitle-2" id="modal-expandable-subtitle">Subtitle of panel</h4>
<p>
panel content shows here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse purus sem,
fringilla ac lorem vel, maxialiquam urna.
</p>
</div>
</div>
}
<button
v-panel-toggle
autofocus
buttonSize="large"
v-button-icon
aria-label="collapse panel"
(click)="panelOpen.set(false)"
>
<svg v-icon-visa-media-fast-forward-tiny v-icon-two-color />
</button>
</dialog>After:
<dialog
v-panel
expandable
vML="auto"
[expanded]="panelOpen()"
aria-describedby="modal-expandable-subtitle"
aria-labelledby="modal-expandable-title"
>
<button
v-panel-toggle
autofocus
buttonSize="large"
v-button-icon
aria-label="collapse panel"
(click)="panelOpen.set(false)"
>
<svg v-icon-visa-media-fast-forward-tiny v-icon-two-color />
</button>
@if(panelOpen()){
<div v-panel-content>
<header vFlex vJustifyContentBetween vGap="4">
<h3 vTypography="headline-4" id="modal-expandable-title">Panel title</h3>
</header>
<div v-panel-body>
<h4 vTypography="subtitle-2" id="modal-expandable-subtitle">Subtitle of panel</h4>
<p>
panel content shows here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse purus sem,
fringilla ac lorem vel, maxialiquam urna.
</p>
</div>
</div>
}
</dialog>SwitchDirectiveno longer appliesidby default
TbodyDirective(v-tbody) was removed since it didn't bind to any properties or add any logic
Before:
<tbody v-tbody>...</tbody>After:
<tbody>...</tbody>TrDirective(v-tr) was removed since it didn't bind to any properties or add any logic
Before:
<tr v-tr>...</tr>After:
<tr>...</tr>ToggleButton-requiredproperty was removed since it didn't add any functionality- No longer supports variant with radio and checkbox components embedded (styles are still available, but state management is limited)
TooltipDirective-zIndexrenamed toz-indexto align with native CSS
- The
_activeIndexproperty is removed fromWizardDirective - The
completeandinvalidproperties are removed fromWizardStepDirective
vGap- no longer addsvFlexby default. If the element does not havedisplay: flexyou will have to explicitly add css or a flex display directive.- This allows you to now easily add
vGapto grid layouts
Before:
<div vGap>...</div>After:
<div vGap vFlex>...</div>Nova Angular 6 implements a new pattern for handling input properties that ensures user-provided inputs always take precedence over internal state. This creates a more predictable API that respects developer intentions.
The pattern uses signals and computed properties:
// Example from a component
readonly buttonSizeInput = input<ButtonSize>('medium'); // User input with default
readonly buttonSizeInternal = signal<ButtonSize>('medium'); // Internal state
// User input is always prioritized over internal state
readonly buttonSize = computed(() => this.buttonSizeInput() ?? this.buttonSizeInternal());This pattern is used throughout the library and provides several benefits:
- Prevents internal component state from overriding developer-specified inputs
- Creates a more predictable API
- Reduces support requests and allows for workarounds
- Allows components to have internal state that doesn't conflict with user inputs
Nova Angular 6 replaces random UUID generation with deterministic ID generation. This ensures consistent behavior across renders while still providing unique identifiers.
The ID generator creates predictable IDs based on component type and instance count:
// Example usage
id = this.idGenerator.newId('button'); // Generates "button-0" for first instanceBenefits of deterministic IDs:
- Makes testing more predictable and reliable
- Simplifies analytics tracking
- Ensures consistent behavior across renders
- Improves debugging experience
The new PaginationControl replaces PaginationService with a more flexible, signals-based approach:
// Creating a basic pagination control
readonly paginationControl = new PaginationControl({
totalItems: 100,
itemsPerPage: 10
});
// Using the pagination control
paginationControl.goToNextPage();
paginationControl.goToPage(5);
const currentPage = paginationControl.currentPage;Key features:
- Signals-based reactivity
- Support for bringing your own signal for selected page
- Better type safety
- Easier to use with multiple pagination instances
- More customization options
- Migrated from
@HostListener/@HostBindingtohost: {}property - Added deterministic IDs for better testing and analytics
- Improved component customization options
- Supports Angular 18, 19, and 20
- Reactive architecture with signals
- More atomic directives
- Smaller bundle size of library
- Faster components
- Less services
-
Component not rendering correctly
- Check if you're using any removed directives like
TbodyDirectiveorTrDirective - Ensure you've updated event handlers from custom events to native events
- Check if you're using any removed directives like
-
Service injection errors
- If you're injecting services that have been removed, update to the recommended alternatives
-
Styling issues
- If using
vGap, make sure to addvFlexif you need flex layout - Update any renamed properties like
zIndextoz-index
- If using
-
Dialog focus issues
- Ensure you've added focus trapping to your dialogs
- Import and use
CdkTrapFocusfrom@angular/cdk/a11y
Nova Angular 6 sets the foundation for a more native, lightweight component library that will:
- Allow developers to bring their own state/logic
- Focus on providing styles and optional controllers
- Simplify future Angular version upgrades
- Continue improving performance and developer experience
The future of Nova Angular is more native, customizable, and lightweight. The plan going forward is to migrate to a more native approach to components, and do less under-the-hood logic. This allows users to bring their own state/logic, making the library more flexible and easier to integrate with different application architectures.