@@ -22,8 +22,8 @@
bitTypography="helper"
class="tw-text-muted tw-w-full"
[ngClass]="{
- 'tw-truncate': truncate,
- 'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
+ 'tw-truncate': truncate(),
+ 'tw-text-wrap tw-overflow-auto tw-break-words': !truncate(),
}"
>
diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts
index 0f828de33b4..f75713b139e 100644
--- a/libs/components/src/item/item-content.component.ts
+++ b/libs/components/src/item/item-content.component.ts
@@ -7,9 +7,9 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
- Input,
signal,
ViewChild,
+ input,
} from "@angular/core";
import { TypographyModule } from "../typography";
@@ -39,7 +39,7 @@ export class ItemContentComponent implements AfterContentChecked {
*
* Default behavior is truncation.
*/
- @Input() truncate = true;
+ readonly truncate = input(true);
ngAfterContentChecked(): void {
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);
diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts
index 1a653fd1c83..aced89fc7b3 100644
--- a/libs/components/src/link/link.directive.ts
+++ b/libs/components/src/link/link.directive.ts
@@ -1,12 +1,4 @@
-import {
- Input,
- HostBinding,
- Directive,
- inject,
- ElementRef,
- input,
- booleanAttribute,
-} from "@angular/core";
+import { HostBinding, Directive, inject, ElementRef, input, booleanAttribute } from "@angular/core";
import { ariaDisableElement } from "../utils";
@@ -77,8 +69,7 @@ const commonStyles = [
@Directive()
abstract class LinkDirective {
- @Input()
- linkType: LinkType = "primary";
+ readonly linkType = input
("primary");
}
/**
@@ -96,7 +87,7 @@ export class AnchorLinkDirective extends LinkDirective {
@HostBinding("class") get classList() {
return ["before:-tw-inset-y-[0.125rem]"]
.concat(commonStyles)
- .concat(linkStyles[this.linkType] ?? []);
+ .concat(linkStyles[this.linkType()] ?? []);
}
}
@@ -111,7 +102,7 @@ export class ButtonLinkDirective extends LinkDirective {
@HostBinding("class") get classList() {
return ["before:-tw-inset-y-[0.25rem]"]
.concat(commonStyles)
- .concat(linkStyles[this.linkType] ?? []);
+ .concat(linkStyles[this.linkType()] ?? []);
}
constructor() {
diff --git a/libs/components/src/menu/menu-item.directive.ts b/libs/components/src/menu/menu-item.directive.ts
index 37b68eecd74..088e2a4293f 100644
--- a/libs/components/src/menu/menu-item.directive.ts
+++ b/libs/components/src/menu/menu-item.directive.ts
@@ -39,6 +39,9 @@ export class MenuItemDirective implements FocusableOption {
return this.disabled || null; // native disabled attr must be null when false
}
+ // TODO: Skipped for signal migration because:
+ // This input overrides a field from a superclass, while the superclass field
+ // is not migrated.
@Input({ transform: coerceBooleanProperty }) disabled?: boolean = false;
constructor(public elementRef: ElementRef) {}
diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts
index 528697600c4..94353299c5d 100644
--- a/libs/components/src/menu/menu-trigger-for.directive.ts
+++ b/libs/components/src/menu/menu-trigger-for.directive.ts
@@ -8,26 +8,30 @@ import {
ElementRef,
HostBinding,
HostListener,
- Input,
OnDestroy,
ViewContainerRef,
+ input,
} from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { filter, mergeWith } from "rxjs/operators";
import { MenuComponent } from "./menu.component";
-@Directive({ selector: "[bitMenuTriggerFor]", exportAs: "menuTrigger", standalone: true })
+@Directive({
+ selector: "[bitMenuTriggerFor]",
+ exportAs: "menuTrigger",
+ standalone: true,
+ host: { "[attr.role]": "this.role()" },
+})
export class MenuTriggerForDirective implements OnDestroy {
@HostBinding("attr.aria-expanded") isOpen = false;
@HostBinding("attr.aria-haspopup") get hasPopup(): "menu" | "dialog" {
- return this.menu?.ariaRole || "menu";
+ return this.menu()?.ariaRole() || "menu";
}
- @HostBinding("attr.role")
- @Input()
- role = "button";
- @Input("bitMenuTriggerFor") menu: MenuComponent;
+ readonly role = input("button");
+
+ readonly menu = input(undefined, { alias: "bitMenuTriggerFor" });
private overlayRef: OverlayRef;
private defaultMenuConfig: OverlayConfig = {
@@ -66,14 +70,15 @@ export class MenuTriggerForDirective implements OnDestroy {
}
private openMenu() {
- if (this.menu == null) {
+ const menu = this.menu();
+ if (menu == null) {
throw new Error("Cannot find bit-menu element");
}
this.isOpen = true;
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
- const templatePortal = new TemplatePortal(this.menu.templateRef, this.viewContainerRef);
+ const templatePortal = new TemplatePortal(menu.templateRef, this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
@@ -90,11 +95,11 @@ export class MenuTriggerForDirective implements OnDestroy {
}
this.destroyMenu();
});
- if (this.menu.keyManager) {
- this.menu.keyManager.setFirstItemActive();
+ if (menu.keyManager) {
+ menu.keyManager.setFirstItemActive();
this.keyDownEventsSub = this.overlayRef
.keydownEvents()
- .subscribe((event: KeyboardEvent) => this.menu.keyManager.onKeydown(event));
+ .subscribe((event: KeyboardEvent) => this.menu().keyManager.onKeydown(event));
}
}
@@ -105,19 +110,19 @@ export class MenuTriggerForDirective implements OnDestroy {
this.isOpen = false;
this.disposeAll();
- this.menu.closed.emit();
+ this.menu().closed.emit();
}
private getClosedEvents(): Observable {
const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef.keydownEvents().pipe(
filter((event: KeyboardEvent) => {
- const keys = this.menu.ariaRole === "menu" ? ["Escape", "Tab"] : ["Escape"];
+ const keys = this.menu().ariaRole() === "menu" ? ["Escape", "Tab"] : ["Escape"];
return keys.includes(event.key);
}),
);
const backdrop = this.overlayRef.backdropClick();
- const menuClosed = this.menu.closed;
+ const menuClosed = this.menu().closed;
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
}
diff --git a/libs/components/src/menu/menu.component.html b/libs/components/src/menu/menu.component.html
index 31fd6df0a66..f5c60660775 100644
--- a/libs/components/src/menu/menu.component.html
+++ b/libs/components/src/menu/menu.component.html
@@ -2,10 +2,10 @@
diff --git a/libs/components/src/menu/menu.component.ts b/libs/components/src/menu/menu.component.ts
index 8636f158729..0a76d59a09c 100644
--- a/libs/components/src/menu/menu.component.ts
+++ b/libs/components/src/menu/menu.component.ts
@@ -10,7 +10,7 @@ import {
ContentChildren,
QueryList,
AfterContentInit,
- Input,
+ input,
} from "@angular/core";
import { MenuItemDirective } from "./menu-item.directive";
@@ -28,12 +28,12 @@ export class MenuComponent implements AfterContentInit {
menuItems: QueryList;
keyManager?: FocusKeyManager;
- @Input() ariaRole: "menu" | "dialog" = "menu";
+ readonly ariaRole = input<"menu" | "dialog">("menu");
- @Input() ariaLabel: string;
+ readonly ariaLabel = input();
ngAfterContentInit() {
- if (this.ariaRole === "menu") {
+ if (this.ariaRole() === "menu") {
this.keyManager = new FocusKeyManager(this.menuItems)
.withWrap()
.skipPredicate((item) => item.disabled);
diff --git a/libs/components/src/multi-select/multi-select.component.html b/libs/components/src/multi-select/multi-select.component.html
index 0b46ef2662d..8aa45b7dae8 100644
--- a/libs/components/src/multi-select/multi-select.component.html
+++ b/libs/components/src/multi-select/multi-select.component.html
@@ -1,12 +1,12 @@
();
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
- @Input() removeSelectedItems = false;
- @Input() placeholder: string;
- @Input() loading = false;
- @Input({ transform: coerceBooleanProperty }) disabled?: boolean;
+ readonly removeSelectedItems = input(false);
+ readonly placeholder = model();
+ readonly loading = input(false);
+ // TODO: Skipped for signal migration because:
+ // Your application code writes to the input. This prevents migration.
+ @Input({ transform: booleanAttribute }) disabled?: boolean;
// Internal tracking of selected items
protected selectedItems: SelectItemView[];
@@ -79,7 +86,9 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
ngOnInit(): void {
// Default Text Values
- this.placeholder = this.placeholder ?? this.i18nService.t("multiSelectPlaceholder");
+ this.placeholder.update(
+ (placeholder) => placeholder ?? this.i18nService.t("multiSelectPlaceholder"),
+ );
this.loadingText = this.i18nService.t("multiSelectLoading");
}
@@ -119,15 +128,15 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
this.onItemsConfirmed.emit(this.selectedItems);
// Remove selected items from base list based on input property
- if (this.removeSelectedItems) {
- let updatedBaseItems = this.baseItems;
+ if (this.removeSelectedItems()) {
+ let updatedBaseItems = this.baseItems();
this.selectedItems.forEach((selectedItem) => {
updatedBaseItems = updatedBaseItems.filter((item) => selectedItem.id !== item.id);
});
// Reset Lists
this.selectedItems = null;
- this.baseItems = updatedBaseItems;
+ this.baseItems.set(updatedBaseItems);
}
}
@@ -186,9 +195,11 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
}
/**Implemented as part of BitFormFieldControl */
- @HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
+ readonly id = input(`bit-multi-select-${nextId++}`);
/**Implemented as part of BitFormFieldControl */
+ // TODO: Skipped for signal migration because:
+ // Accessor inputs cannot be migrated as they are too complex.
@HostBinding("attr.required")
@Input()
get required() {
diff --git a/libs/components/src/navigation/nav-base.component.ts b/libs/components/src/navigation/nav-base.component.ts
index dc3084509a5..0fb73740273 100644
--- a/libs/components/src/navigation/nav-base.component.ts
+++ b/libs/components/src/navigation/nav-base.component.ts
@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { Directive, EventEmitter, Input, Output } from "@angular/core";
+import { Directive, EventEmitter, Output, input } from "@angular/core";
import { RouterLink, RouterLinkActive } from "@angular/router";
/**
@@ -11,17 +11,17 @@ export abstract class NavBaseComponent {
/**
* Text to display in main content
*/
- @Input() text: string;
+ readonly text = input();
/**
* `aria-label` for main content
*/
- @Input() ariaLabel: string;
+ readonly ariaLabel = input();
/**
* Optional icon, e.g. `"bwi-collection-shared"`
*/
- @Input() icon: string;
+ readonly icon = input();
/**
* Optional route to be passed to internal `routerLink`. If not provided, the nav component will render as a button.
@@ -34,31 +34,31 @@ export abstract class NavBaseComponent {
*
* See: {@link https://github.com/angular/angular/issues/24482}
*/
- @Input() route?: RouterLink["routerLink"];
+ readonly route = input();
/**
* Passed to internal `routerLink`
*
* See {@link RouterLink.relativeTo}
*/
- @Input() relativeTo?: RouterLink["relativeTo"];
+ readonly relativeTo = input();
/**
* Passed to internal `routerLink`
*
* See {@link RouterLinkActive.routerLinkActiveOptions}
*/
- @Input() routerLinkActiveOptions?: RouterLinkActive["routerLinkActiveOptions"] = {
+ readonly routerLinkActiveOptions = input({
paths: "subset",
queryParams: "ignored",
fragment: "ignored",
matrixParams: "ignored",
- };
+ });
/**
* If `true`, do not change styles when nav item is active.
*/
- @Input() hideActiveStyles = false;
+ readonly hideActiveStyles = input(false);
/**
* Fires when main content is clicked
diff --git a/libs/components/src/navigation/nav-group.component.html b/libs/components/src/navigation/nav-group.component.html
index 6a9ee4c0ab9..d63cf5d8814 100644
--- a/libs/components/src/navigation/nav-group.component.html
+++ b/libs/components/src/navigation/nav-group.component.html
@@ -1,28 +1,28 @@
-@if (!hideIfEmpty || nestedNavComponents.length > 0) {
+@if (!hideIfEmpty() || nestedNavComponents.length > 0) {
@@ -32,10 +32,10 @@
@if (sideNavService.open$ | async) {
- @if (open) {
+ @if (open()) {
diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts
index b52f9fcd202..6e73d67f77a 100644
--- a/libs/components/src/navigation/nav-group.component.ts
+++ b/libs/components/src/navigation/nav-group.component.ts
@@ -4,11 +4,12 @@ import {
Component,
ContentChildren,
EventEmitter,
- Input,
Optional,
Output,
QueryList,
SkipSelf,
+ input,
+ model,
} from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -36,7 +37,7 @@ export class NavGroupComponent extends NavBaseComponent {
/** When the side nav is open, the parent nav item should not show active styles when open. */
protected get parentHideActiveStyles(): boolean {
- return this.hideActiveStyles || (this.open && this.sideNavService.open);
+ return this.hideActiveStyles() || (this.open() && this.sideNavService.open);
}
/**
@@ -47,14 +48,12 @@ export class NavGroupComponent extends NavBaseComponent {
/**
* Is `true` if the expanded content is visible
*/
- @Input()
- open = false;
+ readonly open = model(false);
/**
* Automatically hide the nav group if there are no child buttons
*/
- @Input({ transform: booleanAttribute })
- hideIfEmpty = false;
+ readonly hideIfEmpty = input(false, { transform: booleanAttribute });
@Output()
openChange = new EventEmitter
();
@@ -67,24 +66,24 @@ export class NavGroupComponent extends NavBaseComponent {
}
setOpen(isOpen: boolean) {
- this.open = isOpen;
- this.openChange.emit(this.open);
+ this.open.set(isOpen);
+ this.openChange.emit(this.open());
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
- this.open && this.parentNavGroup?.setOpen(this.open);
+ this.open() && this.parentNavGroup?.setOpen(this.open());
}
protected toggle(event?: MouseEvent) {
event?.stopPropagation();
- this.setOpen(!this.open);
+ this.setOpen(!this.open());
}
protected handleMainContentClicked() {
if (!this.sideNavService.open) {
- if (!this.route) {
+ if (!this.route()) {
this.sideNavService.setOpen();
}
- this.open = true;
+ this.open.set(true);
} else {
this.toggle();
}
diff --git a/libs/components/src/navigation/nav-item.component.html b/libs/components/src/navigation/nav-item.component.html
index 3d4dadacffa..f8e27dfc571 100644
--- a/libs/components/src/navigation/nav-item.component.html
+++ b/libs/components/src/navigation/nav-item.component.html
@@ -1,6 +1,6 @@
@let open = sideNavService.open$ | async;
- @if (open || icon) {
+ @if (open || icon()) {
diff --git a/libs/components/src/navigation/nav-logo.component.ts b/libs/components/src/navigation/nav-logo.component.ts
index 8c40ea0dc79..74b44266074 100644
--- a/libs/components/src/navigation/nav-logo.component.ts
+++ b/libs/components/src/navigation/nav-logo.component.ts
@@ -1,7 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+
import { CommonModule } from "@angular/common";
-import { Component, Input } from "@angular/core";
+import { Component, input } from "@angular/core";
import { RouterLinkActive, RouterLink } from "@angular/router";
import { Icon } from "../icon";
@@ -17,18 +18,18 @@ import { SideNavService } from "./side-nav.service";
})
export class NavLogoComponent {
/** Icon that is displayed when the side nav is closed */
- @Input() closedIcon = BitwardenShield;
+ readonly closedIcon = input(BitwardenShield);
/** Icon that is displayed when the side nav is open */
- @Input({ required: true }) openIcon: Icon;
+ readonly openIcon = input.required
();
/**
* Route to be passed to internal `routerLink`
*/
- @Input({ required: true }) route: string | any[];
+ readonly route = input.required();
/** Passed to `attr.aria-label` and `attr.title` */
- @Input({ required: true }) label: string;
+ readonly label = input.required();
constructor(protected sideNavService: SideNavService) {}
}
diff --git a/libs/components/src/navigation/side-nav.component.html b/libs/components/src/navigation/side-nav.component.html
index 124daa776d0..46ccc13bc8a 100644
--- a/libs/components/src/navigation/side-nav.component.html
+++ b/libs/components/src/navigation/side-nav.component.html
@@ -10,7 +10,7 @@
class="tw-fixed md:tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
[ngClass]="{ 'tw-w-60': data.open }"
[ngStyle]="
- variant === 'secondary' && {
+ variant() === 'secondary' && {
'--color-text-alt2': 'var(--color-text-main)',
'--color-background-alt3': 'var(--color-secondary-100)',
'--color-background-alt4': 'var(--color-secondary-300)',
diff --git a/libs/components/src/navigation/side-nav.component.ts b/libs/components/src/navigation/side-nav.component.ts
index 49b08c63f7c..75365b738af 100644
--- a/libs/components/src/navigation/side-nav.component.ts
+++ b/libs/components/src/navigation/side-nav.component.ts
@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { CdkTrapFocus } from "@angular/cdk/a11y";
import { CommonModule } from "@angular/common";
-import { Component, ElementRef, Input, ViewChild } from "@angular/core";
+import { Component, ElementRef, ViewChild, input } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -19,7 +19,7 @@ export type SideNavVariant = "primary" | "secondary";
imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe],
})
export class SideNavComponent {
- @Input() variant: SideNavVariant = "primary";
+ readonly variant = input("primary");
@ViewChild("toggleButton", { read: ElementRef, static: true })
private toggleButton: ElementRef;
diff --git a/libs/components/src/no-items/no-items.component.html b/libs/components/src/no-items/no-items.component.html
index ae2416d7a7b..a2659abb9a2 100644
--- a/libs/components/src/no-items/no-items.component.html
+++ b/libs/components/src/no-items/no-items.component.html
@@ -1,6 +1,6 @@
-
+
diff --git a/libs/components/src/no-items/no-items.component.ts b/libs/components/src/no-items/no-items.component.ts
index 20ce95a53ba..7c00999f59f 100644
--- a/libs/components/src/no-items/no-items.component.ts
+++ b/libs/components/src/no-items/no-items.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input } from "@angular/core";
+import { Component, input } from "@angular/core";
import { Icons } from "..";
import { BitIconComponent } from "../icon/icon.component";
@@ -12,5 +12,5 @@ import { BitIconComponent } from "../icon/icon.component";
imports: [BitIconComponent],
})
export class NoItemsComponent {
- @Input() icon = Icons.Search;
+ readonly icon = input(Icons.Search);
}
diff --git a/libs/components/src/popover/popover-trigger-for.directive.ts b/libs/components/src/popover/popover-trigger-for.directive.ts
index 47fda1ca267..d21ea9d7ed6 100644
--- a/libs/components/src/popover/popover-trigger-for.directive.ts
+++ b/libs/components/src/popover/popover-trigger-for.directive.ts
@@ -6,11 +6,11 @@ import {
AfterViewInit,
Directive,
ElementRef,
- HostBinding,
HostListener,
- Input,
OnDestroy,
ViewContainerRef,
+ input,
+ model,
} from "@angular/core";
import { Observable, Subscription, filter, mergeWith } from "rxjs";
@@ -20,27 +20,26 @@ import { PopoverComponent } from "./popover.component";
@Directive({
selector: "[bitPopoverTriggerFor]",
exportAs: "popoverTrigger",
+ host: {
+ "[attr.aria-expanded]": "this.popoverOpen()",
+ },
})
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
- @Input()
- @HostBinding("attr.aria-expanded")
- popoverOpen = false;
+ readonly popoverOpen = model(false);
- @Input("bitPopoverTriggerFor")
- popover: PopoverComponent;
+ readonly popover = input
(undefined, { alias: "bitPopoverTriggerFor" });
- @Input("position")
- position: string;
+ readonly position = input();
private overlayRef: OverlayRef;
private closedEventsSub: Subscription;
get positions() {
- if (!this.position) {
+ if (!this.position()) {
return defaultPositions;
}
- const preferredPosition = defaultPositions.find((position) => position.id === this.position);
+ const preferredPosition = defaultPositions.find((position) => position.id === this.position());
if (preferredPosition) {
return [preferredPosition, ...defaultPositions];
@@ -72,7 +71,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
@HostListener("click")
togglePopover() {
- if (this.popoverOpen) {
+ if (this.popoverOpen()) {
this.closePopover();
} else {
this.openPopover();
@@ -80,10 +79,10 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
}
private openPopover() {
- this.popoverOpen = true;
+ this.popoverOpen.set(true);
this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
- const templatePortal = new TemplatePortal(this.popover.templateRef, this.viewContainerRef);
+ const templatePortal = new TemplatePortal(this.popover().templateRef, this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe(() => {
@@ -97,17 +96,17 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
.keydownEvents()
.pipe(filter((event: KeyboardEvent) => event.key === "Escape"));
const backdrop = this.overlayRef.backdropClick();
- const popoverClosed = this.popover.closed;
+ const popoverClosed = this.popover().closed;
return detachments.pipe(mergeWith(escKey, backdrop, popoverClosed));
}
private destroyPopover() {
- if (this.overlayRef == null || !this.popoverOpen) {
+ if (this.overlayRef == null || !this.popoverOpen()) {
return;
}
- this.popoverOpen = false;
+ this.popoverOpen.set(false);
this.disposeAll();
}
@@ -117,7 +116,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
}
ngAfterViewInit() {
- if (this.popoverOpen) {
+ if (this.popoverOpen()) {
this.openPopover();
}
}
diff --git a/libs/components/src/popover/popover.component.html b/libs/components/src/popover/popover.component.html
index 05db9112b8a..dfcf3ff5d01 100644
--- a/libs/components/src/popover/popover.component.html
+++ b/libs/components/src/popover/popover.component.html
@@ -6,7 +6,7 @@
>
- {{ title }}
+ {{ title() }}