Sheet

Extends the Dialog component to display content that complements the main content of the screen.

PreviousNext
import { Component, inject } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
 
import { ZardButtonComponent } from '../../button/button.component';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardSheetModule } from '../sheet.module';
import { Z_MODAL_DATA, ZardSheetService } from '../sheet.service';
 
interface iSheetData {
  name: string;
  username: string;
}
 
@Component({
  selector: 'zard-demo-sheet-basic',
  imports: [FormsModule, ReactiveFormsModule, ZardInputDirective],
  standalone: true,
  template: `
    <form [formGroup]="form" class="grid flex-1 auto-rows-min gap-6 px-4">
      <div class="grid gap-3">
        <label
          for="name"
          class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
          >Name</label
        >
        <input
          z-input
          formControlName="name"
          class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
        />
      </div>
 
      <div class="grid gap-3">
        <label
          for="username"
          class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
          >Username</label
        >
        <input
          z-input
          formControlName="username"
          class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
        />
      </div>
    </form>
  `,
  exportAs: 'zardDemoSheetBasic',
})
export class ZardDemoSheetBasicInputComponent {
  private zData: iSheetData = inject(Z_MODAL_DATA);
 
  form = new FormGroup({
    name: new FormControl('Matheus Ribeiro'),
    username: new FormControl('@ribeiromatheus.dev'),
  });
 
  constructor() {
    if (this.zData) this.form.patchValue(this.zData);
  }
}
 
@Component({
  imports: [ZardButtonComponent, ZardSheetModule],
  standalone: true,
  template: ` <button z-button zType="outline" (click)="openSheet()">Edit profile</button> `,
})
export class ZardDemoSheetBasicComponent {
  private sheetService = inject(ZardSheetService);
 
  openSheet() {
    this.sheetService.create({
      zTitle: 'Edit profile',
      zDescription: `Make changes to your profile here. Click save when you're done.`,
      zContent: ZardDemoSheetBasicInputComponent,
      zData: {
        name: 'Matheus Ribeiro',
        username: '@ribeiromatheus.dev',
      } as iSheetData,
      zOkText: 'Save changes',
      zOnOk: instance => {
        console.log('Form submitted:', instance.form.value);
      },
    });
  }
}
 

Installation

1

Run the CLI

Use the CLI to add the component to your project.

npx @ngzard/ui@latest add sheet
1

Add the component files

Create the component directory structure and add the following files to your project.

sheet.component.ts
sheet.component.ts
import { OverlayModule } from '@angular/cdk/overlay';
import {
  BasePortalOutlet,
  CdkPortalOutlet,
  type ComponentPortal,
  PortalModule,
  type TemplatePortal,
} from '@angular/cdk/portal';
import {
  ChangeDetectionStrategy,
  Component,
  type ComponentRef,
  computed,
  ElementRef,
  type EmbeddedViewRef,
  type EventEmitter,
  inject,
  output,
  signal,
  type TemplateRef,
  type Type,
  viewChild,
  type ViewContainerRef,
} from '@angular/core';
 
import type { ZardSheetRef } from './sheet-ref';
import { sheetVariants, type ZardSheetVariants } from './sheet.variants';
import { mergeClasses, noopFun } from '../../shared/utils/utils';
import { ZardButtonComponent } from '../button/button.component';
import { ZardIconComponent } from '../icon/icon.component';
import type { ZardIcon } from '../icon/icons';
 
export type OnClickCallback<T> = (instance: T) => false | void | object;
export class ZardSheetOptions<T, U> {
  zCancelIcon?: ZardIcon;
  zCancelText?: string | null;
  zClosable?: boolean;
  zContent?: string | TemplateRef<T> | Type<T>;
  zCustomClasses?: string;
  zData?: U;
  zDescription?: string;
  zHeight?: string;
  zHideFooter?: boolean;
  zMaskClosable?: boolean;
  zOkDestructive?: boolean;
  zOkDisabled?: boolean;
  zOkIcon?: ZardIcon;
  zOkText?: string | null;
  zOnCancel?: EventEmitter<T> | OnClickCallback<T> = noopFun;
  zOnOk?: EventEmitter<T> | OnClickCallback<T> = noopFun;
  zSide?: ZardSheetVariants['zSide'] = 'left';
  zSize?: ZardSheetVariants['zSize'] = 'default';
  zTitle?: string | TemplateRef<T>;
  zViewContainerRef?: ViewContainerRef;
  zWidth?: string;
}
 
@Component({
  selector: 'z-sheet',
  imports: [OverlayModule, PortalModule, ZardButtonComponent, ZardIconComponent],
  template: `
    @if (config.zClosable || config.zClosable === undefined) {
      <button
        type="button"
        data-testid="z-close-header-button"
        z-button
        zType="ghost"
        zSize="sm"
        class="absolute top-1 right-1 cursor-pointer"
        (click)="onCloseClick()"
      >
        <z-icon zType="x" />
      </button>
    }
 
    @if (config.zTitle || config.zDescription) {
      <header data-slot="sheet-header" class="flex flex-col gap-1.5 p-4">
        @if (config.zTitle) {
          <h4 data-testid="z-title" data-slot="sheet-title" class="text-lg leading-none font-semibold tracking-tight">
            {{ config.zTitle }}
          </h4>
 
          @if (config.zDescription) {
            <p data-testid="z-description" data-slot="sheet-description" class="text-muted-foreground text-sm">
              {{ config.zDescription }}
            </p>
          }
        }
      </header>
    }
 
    <main class="flex w-full flex-col space-y-4">
      <ng-template cdkPortalOutlet />
 
      @if (isStringContent) {
        <div data-testid="z-content" data-slot="sheet-content" [innerHTML]="config.zContent"></div>
      }
    </main>
 
    @if (!config.zHideFooter) {
      <footer data-slot="sheet-footer" class="mt-auto flex flex-col gap-2 p-4">
        @if (config.zOkText !== null) {
          <button
            type="button"
            data-testid="z-ok-button"
            class="cursor-pointer"
            z-button
            [zType]="config.zOkDestructive ? 'destructive' : 'default'"
            [disabled]="config.zOkDisabled"
            (click)="onOkClick()"
          >
            @if (config.zOkIcon) {
              <z-icon [zType]="config.zOkIcon" />
            }
 
            {{ config.zOkText ?? 'OK' }}
          </button>
        }
 
        @if (config.zCancelText !== null) {
          <button
            type="button"
            data-testid="z-cancel-button"
            class="cursor-pointer"
            z-button
            zType="outline"
            (click)="onCloseClick()"
          >
            @if (config.zCancelIcon) {
              <z-icon [zType]="config.zCancelIcon" />
            }
 
            {{ config.zCancelText ?? 'Cancel' }}
          </button>
        }
      </footer>
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    'data-slot': 'sheet',
    '[class]': 'classes()',
    '[attr.data-state]': 'state()',
    '[style.width]': 'config.zWidth ? config.zWidth + " !important" : null',
    '[style.height]': 'config.zHeight ? config.zHeight + " !important" : null',
  },
  exportAs: 'zSheet',
})
export class ZardSheetComponent<T, U> extends BasePortalOutlet {
  private readonly host = inject(ElementRef<HTMLElement>);
  protected readonly config = inject(ZardSheetOptions<T, U>);
 
  protected readonly classes = computed(() => {
    const zSize = this.config.zWidth || this.config.zHeight ? 'custom' : this.config.zSize;
 
    return mergeClasses(
      sheetVariants({
        zSide: this.config.zSide,
        zSize,
      }),
      this.config.zCustomClasses,
    );
  });
 
  sheetRef?: ZardSheetRef<T>;
 
  protected readonly isStringContent = typeof this.config.zContent === 'string';
 
  readonly portalOutlet = viewChild.required(CdkPortalOutlet);
 
  readonly okTriggered = output<void>();
  readonly cancelTriggered = output<void>();
  readonly state = signal<'closed' | 'open'>('closed');
 
  constructor() {
    super();
  }
 
  getNativeElement(): HTMLElement {
    return this.host.nativeElement;
  }
 
  attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
    if (this.portalOutlet()?.hasAttached()) {
      throw new Error('Attempting to attach modal content after content is already attached');
    }
    return this.portalOutlet()?.attachComponentPortal(portal);
  }
 
  attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
    if (this.portalOutlet()?.hasAttached()) {
      throw new Error('Attempting to attach modal content after content is already attached');
    }
 
    return this.portalOutlet()?.attachTemplatePortal(portal);
  }
 
  onOkClick() {
    this.okTriggered.emit();
  }
 
  onCloseClick() {
    this.cancelTriggered.emit();
  }
}
 
sheet.variants.ts
sheet.variants.ts
import { cva, type VariantProps } from 'class-variance-authority';
 
export const sheetVariants = cva(
  'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
  {
    variants: {
      zSide: {
        right:
          'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 border-l',
        left: 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 border-r',
        top: 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b',
        bottom:
          'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t',
      },
      zSize: {
        default: '',
        sm: '',
        lg: '',
        custom: '',
      },
    },
    compoundVariants: [
      {
        zSide: ['left', 'right'],
        zSize: 'default',
        class: 'w-3/4 sm:max-w-sm h-full',
      },
      {
        zSide: ['left', 'right'],
        zSize: 'sm',
        class: 'w-1/2 sm:max-w-xs h-full',
      },
      {
        zSide: ['left', 'right'],
        zSize: 'lg',
        class: 'w-full sm:max-w-lg h-full',
      },
      {
        zSide: ['top', 'bottom'],
        zSize: 'default',
        class: 'h-auto',
      },
      {
        zSide: ['top', 'bottom'],
        zSize: 'sm',
        class: 'h-1/3',
      },
      {
        zSide: ['top', 'bottom'],
        zSize: 'lg',
        class: 'h-3/4',
      },
    ],
    defaultVariants: {
      zSide: 'right',
      zSize: 'default',
    },
  },
);
export type ZardSheetVariants = VariantProps<typeof sheetVariants>;
 
sheet-ref.ts
sheet-ref.ts
import type { OverlayRef } from '@angular/cdk/overlay';
import { isPlatformBrowser } from '@angular/common';
import { EventEmitter, Inject, PLATFORM_ID } from '@angular/core';
 
import { filter, fromEvent, Subject, takeUntil } from 'rxjs';
 
import type { ZardSheetComponent, ZardSheetOptions } from './sheet.component';
 
const enum eTriggerAction {
  CANCEL = 'cancel',
  OK = 'ok',
}
 
export class ZardSheetRef<T = any, R = any, U = any> {
  private destroy$ = new Subject<void>();
  private isClosing = false;
  protected result?: R;
  componentInstance: T | null = null;
 
  constructor(
    private overlayRef: OverlayRef,
    private config: ZardSheetOptions<T, U>,
    private containerInstance: ZardSheetComponent<T, U>,
    @Inject(PLATFORM_ID) private platformId: object,
  ) {
    this.containerInstance.cancelTriggered.subscribe(() => this.trigger(eTriggerAction.CANCEL));
    this.containerInstance.okTriggered.subscribe(() => this.trigger(eTriggerAction.OK));
 
    if ((this.config.zMaskClosable ?? true) && isPlatformBrowser(this.platformId)) {
      this.overlayRef
        .outsidePointerEvents()
        .pipe(takeUntil(this.destroy$))
        .subscribe(() => this.close());
    }
 
    if (isPlatformBrowser(this.platformId)) {
      fromEvent<KeyboardEvent>(document, 'keydown')
        .pipe(
          filter(event => event.key === 'Escape'),
          takeUntil(this.destroy$),
        )
        .subscribe(() => this.close());
    }
  }
 
  close(result?: R) {
    if (this.isClosing) {
      return;
    }
 
    this.isClosing = true;
    this.result = result;
    this.containerInstance.state.set('closed');
 
    if (isPlatformBrowser(this.platformId)) {
      const element = this.containerInstance.getNativeElement();
      let cleanupCalled = false;
 
      const onAnimationEnd = () => {
        if (cleanupCalled) {
          return;
        }
 
        cleanupCalled = true;
        element.removeEventListener('animationend', onAnimationEnd);
        this.closeCleanup();
      };
 
      element.addEventListener('animationend', onAnimationEnd);
      setTimeout(onAnimationEnd, 300); // Fallback after expected animation duration
    } else {
      this.closeCleanup();
    }
  }
 
  private trigger(action: eTriggerAction) {
    const trigger = { ok: this.config.zOnOk, cancel: this.config.zOnCancel }[action];
 
    if (trigger instanceof EventEmitter) {
      trigger.emit(this.getContentComponent());
    } else if (typeof trigger === 'function') {
      const result = trigger(this.getContentComponent()) as R;
      this.closeWithResult(result);
    } else this.close();
  }
 
  private getContentComponent(): T {
    return this.componentInstance as T;
  }
 
  private closeWithResult(result: R): void {
    if (result !== false) this.close(result);
  }
 
  private closeCleanup(): void {
    if (this.overlayRef) {
      if (this.overlayRef.hasAttached()) {
        this.overlayRef.detachBackdrop();
      }
      this.overlayRef.dispose();
    }
 
    if (!this.destroy$.closed) {
      this.destroy$.next();
      this.destroy$.complete();
    }
  }
}
 
sheet.module.ts
sheet.module.ts
import { OverlayModule } from '@angular/cdk/overlay';
import { PortalModule } from '@angular/cdk/portal';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
 
import { ZardSheetComponent } from './sheet.component';
import { ZardSheetService } from './sheet.service';
import { ZardButtonComponent } from '../button/button.component';
 
const components = [CommonModule, ZardButtonComponent, ZardSheetComponent, OverlayModule, PortalModule];
 
@NgModule({
  imports: components,
  exports: components,
})
export class ZardBreadcrumbModule {}
 
@NgModule({
  imports: [CommonModule, ZardButtonComponent, ZardSheetComponent, OverlayModule, PortalModule],
  providers: [ZardSheetService],
})
export class ZardSheetModule {}
 
sheet.service.ts
sheet.service.ts
import { type ComponentType, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
import { isPlatformBrowser } from '@angular/common';
import { inject, Injectable, InjectionToken, Injector, PLATFORM_ID, TemplateRef } from '@angular/core';
 
import { ZardSheetRef } from './sheet-ref';
import { ZardSheetComponent, ZardSheetOptions } from './sheet.component';
 
type ContentType<T> = ComponentType<T> | TemplateRef<T> | string;
export const Z_MODAL_DATA = new InjectionToken<any>('Z_MODAL_DATA');
 
@Injectable({
  providedIn: 'root',
})
export class ZardSheetService {
  private overlay = inject(Overlay);
  private injector = inject(Injector);
  private platformId = inject(PLATFORM_ID);
 
  create<T, U>(config: ZardSheetOptions<T, U>): ZardSheetRef<T> {
    return this.open<T, U>(config.zContent as ComponentType<T>, config);
  }
 
  private open<T, U>(componentOrTemplateRef: ContentType<T>, config: ZardSheetOptions<T, U>) {
    const overlayRef = this.createOverlay();
 
    if (!overlayRef) {
      // Return a mock sheet ref for SSR environments
      return new ZardSheetRef(undefined as any, config, undefined as any, this.platformId);
    }
 
    const sheetContainer = this.attachSheetContainer<T, U>(overlayRef, config);
 
    const sheetRef = this.attachSheetContent<T, U>(componentOrTemplateRef, sheetContainer, overlayRef, config);
    sheetContainer.sheetRef = sheetRef;
 
    return sheetRef;
  }
 
  private createOverlay(): OverlayRef | undefined {
    if (isPlatformBrowser(this.platformId)) {
      const overlayConfig = new OverlayConfig({
        hasBackdrop: true,
        positionStrategy: this.overlay.position().global(),
      });
 
      return this.overlay.create(overlayConfig);
    }
    return undefined;
  }
 
  private attachSheetContainer<T, U>(overlayRef: OverlayRef, config: ZardSheetOptions<T, U>) {
    const injector = Injector.create({
      parent: this.injector,
      providers: [
        { provide: OverlayRef, useValue: overlayRef },
        { provide: ZardSheetOptions, useValue: config },
      ],
    });
 
    const containerPortal = new ComponentPortal<ZardSheetComponent<T, U>>(
      ZardSheetComponent,
      config.zViewContainerRef,
      injector,
    );
    const containerRef = overlayRef.attach<ZardSheetComponent<T, U>>(containerPortal);
    containerRef.instance.state.set('open');
 
    return containerRef.instance;
  }
 
  private attachSheetContent<T, U>(
    componentOrTemplateRef: ContentType<T>,
    sheetContainer: ZardSheetComponent<T, U>,
    overlayRef: OverlayRef,
    config: ZardSheetOptions<T, U>,
  ) {
    const sheetRef = new ZardSheetRef<T>(overlayRef, config, sheetContainer, this.platformId);
 
    if (componentOrTemplateRef instanceof TemplateRef) {
      sheetContainer.attachTemplatePortal(
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        new TemplatePortal<T>(componentOrTemplateRef, null!, {
          sheetRef: sheetRef,
        } as any),
      );
    } else if (typeof componentOrTemplateRef !== 'string') {
      const injector = this.createInjector<T, U>(sheetRef, config);
      const contentRef = sheetContainer.attachComponentPortal<T>(
        new ComponentPortal(componentOrTemplateRef, config.zViewContainerRef, injector),
      );
      sheetRef.componentInstance = contentRef.instance;
    }
 
    return sheetRef;
  }
 
  private createInjector<T, U>(sheetRef: ZardSheetRef<T>, config: ZardSheetOptions<T, U>) {
    return Injector.create({
      parent: this.injector,
      providers: [
        { provide: ZardSheetRef, useValue: sheetRef },
        { provide: Z_MODAL_DATA, useValue: config.zData },
      ],
    });
  }
}
 

Examples

basic

import { Component, inject } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
 
import { ZardButtonComponent } from '../../button/button.component';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardSheetModule } from '../sheet.module';
import { Z_MODAL_DATA, ZardSheetService } from '../sheet.service';
 
interface iSheetData {
  name: string;
  username: string;
}
 
@Component({
  selector: 'zard-demo-sheet-basic',
  imports: [FormsModule, ReactiveFormsModule, ZardInputDirective],
  standalone: true,
  template: `
    <form [formGroup]="form" class="grid flex-1 auto-rows-min gap-6 px-4">
      <div class="grid gap-3">
        <label
          for="name"
          class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
          >Name</label
        >
        <input
          z-input
          formControlName="name"
          class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
        />
      </div>
 
      <div class="grid gap-3">
        <label
          for="username"
          class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
          >Username</label
        >
        <input
          z-input
          formControlName="username"
          class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
        />
      </div>
    </form>
  `,
  exportAs: 'zardDemoSheetBasic',
})
export class ZardDemoSheetBasicInputComponent {
  private zData: iSheetData = inject(Z_MODAL_DATA);
 
  form = new FormGroup({
    name: new FormControl('Matheus Ribeiro'),
    username: new FormControl('@ribeiromatheus.dev'),
  });
 
  constructor() {
    if (this.zData) this.form.patchValue(this.zData);
  }
}
 
@Component({
  imports: [ZardButtonComponent, ZardSheetModule],
  standalone: true,
  template: ` <button z-button zType="outline" (click)="openSheet()">Edit profile</button> `,
})
export class ZardDemoSheetBasicComponent {
  private sheetService = inject(ZardSheetService);
 
  openSheet() {
    this.sheetService.create({
      zTitle: 'Edit profile',
      zDescription: `Make changes to your profile here. Click save when you're done.`,
      zContent: ZardDemoSheetBasicInputComponent,
      zData: {
        name: 'Matheus Ribeiro',
        username: '@ribeiromatheus.dev',
      } as iSheetData,
      zOkText: 'Save changes',
      zOnOk: instance => {
        console.log('Form submitted:', instance.form.value);
      },
    });
  }
}
 

side

import { Component, inject, signal } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
 
import { ZardButtonComponent } from '../../button/button.component';
import { ZardInputDirective } from '../../input/input.directive';
import { ZardRadioComponent } from '../../radio/radio.component';
import { ZardSheetModule } from '../sheet.module';
import { Z_MODAL_DATA, ZardSheetService } from '../sheet.service';
 
interface iSheetData {
  name: string;
  username: string;
}
 
@Component({
  selector: 'zard-demo-sheet-side',
  imports: [FormsModule, ReactiveFormsModule, ZardInputDirective],
  standalone: true,
  template: `
    <form [formGroup]="form" class="grid flex-1 auto-rows-min gap-6 px-4">
      <div class="grid gap-3">
        <label
          for="name"
          class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
          >Name</label
        >
        <input
          z-input
          formControlName="name"
          class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
        />
      </div>
 
      <div class="grid gap-3">
        <label
          for="username"
          class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
          >Username</label
        >
        <input
          z-input
          formControlName="username"
          class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
        />
      </div>
    </form>
  `,
  exportAs: 'zardDemoSheetSide',
})
export class ZardDemoSheetSideInputComponent {
  private zData: iSheetData = inject(Z_MODAL_DATA);
 
  form = new FormGroup({
    name: new FormControl('Matheus Ribeiro'),
    username: new FormControl('@ribeiromatheus.dev'),
  });
 
  constructor() {
    if (this.zData) this.form.patchValue(this.zData);
  }
}
 
@Component({
  imports: [ZardRadioComponent, FormsModule, ZardButtonComponent, ZardSheetModule],
  standalone: true,
  template: `
    <div class="flex flex-col justify-center space-y-6">
      <div class="flex space-x-4">
        <span z-radio name="top" [(ngModel)]="placement" value="top">top</span>
        <span z-radio name="bottom" [(ngModel)]="placement" value="bottom">bottom</span>
        <span z-radio name="left" [(ngModel)]="placement" value="left">left</span>
        <span z-radio name="right" [(ngModel)]="placement" value="right">right</span>
      </div>
      <button z-button zType="outline" (click)="openSheet()">Edit profile</button>
    </div>
  `,
})
export class ZardDemoSheetSideComponent {
  protected readonly placement = signal<'right' | 'left' | 'top' | 'bottom' | null | undefined>('right');
 
  private sheetService = inject(ZardSheetService);
 
  openSheet() {
    this.sheetService.create({
      zTitle: 'Edit profile',
      zDescription: `Make changes to your profile here. Click save when you're done.`,
      zContent: ZardDemoSheetSideInputComponent,
      zData: {
        name: 'Matheus Ribeiro',
        username: '@ribeiromatheus.dev',
      } as iSheetData,
      zOkText: 'Save changes',
      zOnOk: instance => {
        console.log('Form submitted:', instance.form.value);
      },
      zSide: this.placement(),
    });
  }
}
 

dimensions

import { Component, inject } from '@angular/core';
 
import { ZardButtonComponent } from '../../button/button.component';
import { ZardSheetService } from '../sheet.service';
 
@Component({
  selector: 'z-demo-sheet-dimensions',
  imports: [ZardButtonComponent],
  standalone: true,
  template: `
    <div class="flex flex-wrap gap-4">
      <button z-button zType="outline" (click)="openWideSheet()">Wide Sheet (500px)</button>
      <button z-button zType="outline" (click)="openTallSheet()">Tall Sheet (80vh)</button>
      <button z-button zType="outline" (click)="openCustomSheet()">Custom Dimensions</button>
      <button z-button zType="outline" (click)="openTopSheet()">Top Sheet</button>
    </div>
  `,
})
export class ZardDemoSheetDimensionsComponent {
  private sheetService = inject(ZardSheetService);
 
  openWideSheet() {
    this.sheetService.create({
      zTitle: 'Wide Sheet',
      zDescription: 'This sheet has a custom width of 500px',
      zContent: `
        <div class="p-4">
          <p>This is a wide sheet with custom width.</p>
          <p>Perfect for forms that need more horizontal space.</p>
        </div>
      `,
      zSide: 'right',
      zWidth: '500px',
      zOkText: 'Got it',
    });
  }
 
  openTallSheet() {
    this.sheetService.create({
      zTitle: 'Tall Sheet',
      zDescription: 'This sheet has a custom height of 80vh',
      zContent: `
        <div class="p-4 space-y-4">
          <p>This is a tall sheet with custom height (80% of viewport height).</p>
          <p>Great for content that needs vertical space.</p>
          <div class="h-96 bg-gray-100 rounded-md flex items-center justify-center">
            <p class="text-gray-500">Large content area</p>
          </div>
        </div>
      `,
      zSide: 'left',
      zHeight: '80vh',
      zOkText: 'Close',
    });
  }
 
  openCustomSheet() {
    this.sheetService.create({
      zTitle: 'Custom Dimensions',
      zDescription: 'Both width and height customized',
      zContent: `
        <div class="p-4">
          <p>Width: 400px, Height: 60vh</p>
          <p>Complete control over dimensions.</p>
        </div>
      `,
      zSide: 'right',
      zWidth: '400px',
      zHeight: '60vh',
      zOkText: 'Close',
    });
  }
 
  openTopSheet() {
    this.sheetService.create({
      zTitle: 'Top Sheet',
      zDescription: 'Custom height for top position',
      zContent: `
        <div class="p-4">
          <p>This top sheet has a custom height.</p>
          <p>Height: 50vh</p>
        </div>
      `,
      zSide: 'top',
      zHeight: '50vh',
      zOkText: 'Done',
    });
  }
}
 

Sheet API Reference

Service

ZardSheetService

Service for creating and managing sheet overlays.

Methods:

  • create(config: ZardSheetOptions): ZardSheetRef - Creates and opens a sheet

Configuration Options

ZardSheetOptions

Input Type Default Description
zTitle string | TemplateRef<T> - Sheet title text or template
zDescription string - Sheet description/body text
zContent string | TemplateRef<T> | Type<T> - Custom content component, template, or HTML
zSide 'left' | 'right' | 'top' | 'bottom' 'left' Position of the sheet on screen
zWidth string - Custom width (e.g., '400px', '50%')
zHeight string - Custom height (e.g., '80vh', '500px')
zOkText string | null 'OK' OK button text, null to hide button
zCancelText string | null 'Cancel' Cancel button text, null to hide button
zOkIcon string - OK button icon class name
zCancelIcon string - Cancel button icon class name
zOkDestructive boolean false Whether OK button should have destructive styling
zOkDisabled boolean false Whether OK button should be disabled
zHideFooter boolean false Whether to hide the footer with action buttons
zMaskClosable boolean true Whether clicking outside closes the sheet
zClosable boolean true Whether sheet can be closed
zCustomClasses string - Additional CSS classes to apply
zOnOk EventEmitter<T> | OnClickCallback<T> - OK button click handler
zOnCancel EventEmitter<T> | OnClickCallback<T> - Cancel button click handler
zData object - Data to pass to custom content components
zViewContainerRef ViewContainerRef - View container for rendering custom content

Sheet Reference

ZardSheetRef

Reference to a sheet instance, returned by ZardSheetService.create().

Properties:

  • componentInstance: T | null - Reference to the content component

Methods:

  • close(result?: R): void - Closes the sheet

Component

ZardSheetComponent

Output Type Description
okTriggered EventEmitter<void> Emitted when OK button is clicked
cancelTriggered EventEmitter<void> Emitted when Cancel button is clicked