Exit Intent — Survey
blockmarketingOne-question exit survey asking what the visitor did not find, with preset answers, an optional free-text field, and a thank-you state that closes itself.
Installation
$pnpm dlx uipkge-ng@latest add exit-intent-survey$npx uipkge-ng@latest add exit-intent-survey$yarn dlx uipkge-ng@latest add exit-intent-survey$bunx uipkge-ng@latest add exit-intent-surveyManifest:
https://uipkge.dev/r/angular/exit-intent-survey.jsonInstalls to:src/app/components/blocks/exit-intent-survey/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
Files installed (2)
src/app/components/blocks/exit-intent-survey/exit-intent-survey.component.ts5.2 kB
// One-question exit survey asking what the visitor did not find, with preset answers, an // optional free-text field, and a thank-you state that closes itself. Fires once per session // when the pointer leaves toward the browser chrome. Port of the Vue/React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, OnDestroy, OnInit, inject, signal } from '@angular/core' import { Check, LucideAngularModule } from 'lucide-angular' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiTextareaComponent } from '@/ui/textarea/textarea.component' import { UiToggleGroupComponent, UiToggleGroupItemComponent } from '@/ui/toggle-group/toggle-group.component' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-exit-intent-survey, [ui-exit-intent-survey]', standalone: true, // The block renders its own root <div> (and the dialog portals to body): keep the host out of layout. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiTextareaComponent, UiToggleGroupComponent, UiToggleGroupItemComponent, ], template: ` <div data-slot="exit-intent-survey"> <ui-dialog [open]="open()" (openChange)="open.set($event)"> <ui-dialog-content class="sm:max-w-md"> @if (!sent()) { <div> <div ui-dialog-header> <span ui-badge variant="secondary" class="w-fit">One question</span> <h2 ui-dialog-title class="mt-3 text-xl leading-snug text-balance">What were you looking for?</h2> <p ui-dialog-description> It goes to the people who build the page. No follow-up email unless you ask for one. </p> </div> <ui-toggle-group type="single" [value]="choice()" variant="outline" size="sm" class="mt-5 flex-wrap justify-start" aria-label="Reason for leaving" (valueChange)="onChoiceChange($event)" > @for (option of options; track option) { <button ui-toggle-group-item [value]="option">{{ option }}</button> } </ui-toggle-group> <ui-textarea [value]="detail()" (valueChange)="detail.set($event)" class="mt-3" [rows]="3" placeholder="Anything more (optional)" label="Anything more (optional)" /> <div class="mt-4 flex items-center gap-2"> <button ui-button [disabled]="!choice()" (click)="submit()">Send</button> <button ui-button variant="ghost" (click)="open.set(false)">Skip</button> </div> </div> } @else { <div class="py-6 text-center" aria-live="polite"> <span class="bg-success/10 mx-auto flex size-10 items-center justify-center rounded-full"> <lucide-icon [img]="Check" class="text-success size-5" aria-hidden="true" /> </span> <p class="mt-3 text-sm font-medium">Thanks — that is genuinely useful.</p> </div> } </ui-dialog-content> </ui-dialog> </div> `, }) export class UiExitIntentSurveyComponent implements OnInit, OnDestroy { protected readonly Check = Check @Input() storageKey = 'uipkge:exit-survey-seen' readonly open = signal(false) readonly choice = signal('') readonly detail = signal('') readonly sent = signal(false) protected readonly options = [ 'Pricing was unclear', 'Missing an integration', 'Just browsing', 'Security questions', 'Something else', ] private sendTimer: ReturnType<typeof setTimeout> | undefined private readonly onPointerOut = (event: MouseEvent): void => { if (event.relatedTarget || event.clientY > 4) return try { if (window.sessionStorage.getItem(this.storageKey) === '1') return window.sessionStorage.setItem(this.storageKey, '1') } catch { // Blocked storage: shows once more this page view, never in a loop. } this.open.set(true) document.removeEventListener('mouseout', this.onPointerOut) } constructor() { inject(DestroyRef).onDestroy(() => clearTimeout(this.sendTimer)) } ngOnInit(): void { document.addEventListener('mouseout', this.onPointerOut) } ngOnDestroy(): void { document.removeEventListener('mouseout', this.onPointerOut) } onChoiceChange(value: string): void { this.choice.set(value ?? '') } submit(): void { if (!this.choice()) return this.sent.set(true) // Close on its own rather than making someone dismiss a thank-you. clearTimeout(this.sendTimer) this.sendTimer = setTimeout(() => this.open.set(false), 1400) } }src/app/components/blocks/exit-intent-survey/index.ts0.1 kB
export { UiExitIntentSurveyComponent } from './exit-intent-survey.component'
Raw manifest:https://uipkge.dev/r/angular/exit-intent-survey.json