Menu

Change language

GitHub repo

Exit Intent — Newsletter

blockmarketing

Exit-intent dialog capturing an email for the monthly notes rather than pushing a discount, with the last issue named so the ask is concrete.

Installation

$npx uipkge-ng@latest add exit-intent-newsletter
Manifest:https://uipkge.dev/r/angular/exit-intent-newsletter.jsonInstalls to:src/app/components/blocks/exit-intent-newsletter/

Variants

Loading interactive previews…

Files installed (2)

  • src/app/components/blocks/exit-intent-newsletter/exit-intent-newsletter.component.ts4.4 kB
    // Exit-intent capture for the monthly notes rather than a discount, naming the last issue
    // so the ask is concrete. Fires once per session when the pointer leaves toward the browser
    // chrome. Port of the Vue/React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, signal } from '@angular/core'
    import { Check, LucideAngularModule, Mail } 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 { UiInputComponent } from '@/ui/input/input.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-exit-intent-newsletter, [ui-exit-intent-newsletter]',
      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,
        UiInputComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div data-slot="exit-intent-newsletter">
          <ui-dialog [open]="open()" (openChange)="open.set($event)">
            <ui-dialog-content class="sm:max-w-md">
              <div ui-dialog-header>
                <span ui-badge variant="secondary" class="w-fit gap-1.5">
                  <lucide-icon [img]="Mail" class="size-3" aria-hidden="true" />
                  Monthly notes
                </span>
                <h2 ui-dialog-title class="mt-3 text-xl leading-snug text-balance">
                  Not ready to trial? Take the writing instead
                </h2>
                <p ui-dialog-description class="leading-relaxed">
                  Last issue: “Why we version metric definitions instead of dashboards.” One email a month, no product
                  announcements, unsubscribe in one click.
                </p>
              </div>
    
              <ui-separator />
    
              <form (submit)="subscribe($event)">
                <div class="flex gap-2">
                  <ui-input
                    [value]="email()"
                    (valueChange)="email.set($event)"
                    type="email"
                    required
                    placeholder="[email protected]"
                    autoComplete="email"
                    aria-label="Email address"
                  />
                  <button ui-button type="submit" class="shrink-0">Subscribe</button>
                </div>
                <p class="mt-2 min-h-5 text-xs" aria-live="polite">
                  @if (submitted()) {
                    <span class="text-success inline-flex items-center gap-1.5">
                      <lucide-icon [img]="Check" class="size-3" aria-hidden="true" />
                      Check your inbox to confirm.
                    </span>
                  } @else {
                    <span class="text-muted-foreground">8,400 subscribers, mostly data and finance engineers.</span>
                  }
                </p>
              </form>
            </ui-dialog-content>
          </ui-dialog>
        </div>
      `,
    })
    export class UiExitIntentNewsletterComponent implements OnInit, OnDestroy {
      protected readonly Check = Check
      protected readonly Mail = Mail
    
      @Input() storageKey = 'uipkge:exit-newsletter-seen'
    
      readonly open = signal(false)
      readonly email = signal('')
      readonly submitted = signal(false)
    
      private readonly onPointerOut = (event: MouseEvent): void => {
        // Top edge only: leaving sideways is usually the scrollbar or another window.
        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)
      }
    
      ngOnInit(): void {
        document.addEventListener('mouseout', this.onPointerOut)
      }
    
      ngOnDestroy(): void {
        document.removeEventListener('mouseout', this.onPointerOut)
      }
    
      subscribe(event: Event): void {
        event.preventDefault()
        if (!this.email().trim()) return
        this.submitted.set(true)
      }
    }
    
  • src/app/components/blocks/exit-intent-newsletter/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/exit-intent-newsletter.json