File

/home/workflows/workspace/libs/common/analytics/src/lib/event/event.directive.ts

Description

Specialized version of hraEvent that only emits keyboard events

See EventDirective

Extends

BaseEventDirective

Metadata

Index

Properties
Methods
Inputs

Inputs

disabled
Default value : false, { alias: 'hraKeyboardEventDisabled', transform: booleanAttribute }

Whether this event is disabled

props
Type : EventPropsFor<CoreEvents['Keyboard']>
Default value : '', { alias: 'hraKeyboardEvent' }

Event properties

triggerOn
Type : EventTrigger<'keyup' | 'keydown'> | 'none' | undefined
Default value : undefined, { alias: 'hraKeyboardEventTriggerOn', }

keydown (default), keyup, or 'none' if events are sent programatically

Methods

logEvent
logEvent(trigger?: E, event?: EventTriggerPayloadFor, extraProps: Partial>)
Inherited from BaseEventDirective
Type parameters :
  • E

Logs an event to analytics

Parameters :
Name Type Optional Default value Description
trigger E Yes

Built-in event that triggered the call

event EventTriggerPayloadFor<E> Yes

Event object

extraProps Partial<EventPayloadFor<T>> No {}

Additional event properties

Returns : void

Properties

Readonly event
Type : unknown
Default value : () => {...}
Inherited from BaseEventDirective

Event type

Abstract Readonly disabled
Type : function
Inherited from BaseEventDirective

Whether this event is disabled

Abstract Readonly props
Type : function
Inherited from BaseEventDirective

Event properties

Abstract Readonly triggerOn
Type : function
Inherited from BaseEventDirective

Built-in trigger to log events on or 'none' if events are sent programatically

import {
  AfterViewInit,
  booleanAttribute,
  DestroyRef,
  Directive,
  effect,
  ElementRef,
  inject,
  input,
  Renderer2,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NgControl } from '@angular/forms';
import {
  AnalyticsEvent,
  CoreEvents,
  EventPayloadFor,
  EventPropsFor,
  EventTrigger,
  EventTriggerPayloadFor,
} from '@hra-ui/common/analytics/events';
import { skip } from 'rxjs';
import { UnknownRecord } from 'type-fest';
import { injectLogEvent } from '../analytics/analytics.service';

/** Shared base implementation for event directives */
@Directive()
export abstract class BaseEventDirective<T extends AnalyticsEvent> {
  /** Event type */
  abstract readonly event: () => T;
  /** Event properties */
  abstract readonly props: () => EventPropsFor<T>;
  /** Built-in trigger to log events on or 'none' if events are sent programatically */
  abstract readonly triggerOn: () => EventTrigger | 'none' | undefined;
  /** Whether this event is disabled */
  abstract readonly disabled: () => boolean;

  /** Reference to renderer for dom interactions */
  private readonly renderer = inject(Renderer2);
  /** Host element */
  private readonly el = inject(ElementRef).nativeElement as Element;
  /** Raw logEvent function */
  private readonly logEvent_ = injectLogEvent();

  /** Initialize the directive */
  constructor() {
    effect((onCleanup) => {
      const trigger = this.triggerOn() ?? this.event().trigger ?? 'click';
      if (trigger !== 'none' && !this.disabled()) {
        const { el, renderer } = this;
        const handler = this.logEvent.bind(this, trigger);
        const parts = trigger.split(':', 2);
        const [target, eventName] = parts.length === 2 ? parts : [el, trigger];
        const unlisten = renderer.listen(target, eventName, handler);
        // Delay cleanup to avoid issues with Angular destroying the element before the event is fully processed
        onCleanup(() => setTimeout(() => unlisten()));
      }
    });
  }

  /**
   * Logs an event to analytics
   *
   * @param trigger Built-in event that triggered the call
   * @param event Event object
   * @param extraProps Additional event properties
   */
  logEvent<E extends EventTrigger>(
    trigger?: E,
    event?: EventTriggerPayloadFor<E>,
    extraProps: Partial<EventPayloadFor<T>> = {},
  ): void {
    const props = this.props();
    this.logEvent_(this.event(), {
      trigger: trigger,
      triggerData: event,
      ...(props !== '' ? props : ({} as EventPayloadFor<T>)),
      ...extraProps,
    });
  }
}

/**
 * Directive that log events to analytics whenever a built-in trigger event is dispatched
 * on the host element. It can also be configured for manual logging.
 *
 * @example <caption>Basic usage</caption>
 * ```html
 * <button [hraEvent]="EventType.Click" [hraEventProps]="{...}" >
 * ```
 *
 * @example <caption>Different trigger</caption>
 * ```html
 * <button [hraEvent]="EventType.Click" [hraEventProps]="{...}" hraEventTriggerOn="dblclick" >
 * ```
 *
 * @example <caption>Manual logging</caption>
 * ```html
 * <button [hraEvent]="EventType.Click" [hraEventProps]="{...}" hraEventTriggerOn="none" #eventDir="hraEvent"
 *   (hover)="eventDir.logEvent(...)" >
 * ```
 */
@Directive({
  selector: '[hraEvent]',
  exportAs: 'hraEvent',
})
export class EventDirective<T extends AnalyticsEvent> extends BaseEventDirective<T> {
  /** Event type */
  override readonly event = input.required<T>({ alias: 'hraEvent' });
  /** Event properties */
  override readonly props = input.required<EventPropsFor<T>>({ alias: 'hraEventProps' });
  /** Built-in trigger to log events on or 'none' if events are sent programatically */
  override readonly triggerOn = input<EventTrigger | 'none' | undefined>(undefined, { alias: 'hraEventTriggerOn' });
  /** Whether this event is disabled */
  override readonly disabled = input(false, { alias: 'hraEventDisabled', transform: booleanAttribute });
}

/**
 * Specialized version of `hraEvent` that only emits click events
 *
 * @see {@link EventDirective}
 */
@Directive({
  selector: '[hraClickEvent]',
  exportAs: 'hraClickEvent',
})
export class ClickEventDirective extends BaseEventDirective<CoreEvents['Click']> {
  /** Event type */
  override readonly event = () => CoreEvents.Click;
  /** Event properties */
  override readonly props = input<EventPropsFor<CoreEvents['Click']>>('', { alias: 'hraClickEvent' });
  /** 'none' if events are sent programatically */
  override readonly triggerOn = input<'none' | undefined>(undefined, { alias: 'hraClickEventTriggerOn' });
  /** Whether this event is disabled */
  override readonly disabled = input(false, { alias: 'hraClickEventDisabled', transform: booleanAttribute });
}

/**
 * Specialized version of `hraEvent` that only emits double click events
 *
 * @see {@link EventDirective}
 */
@Directive({
  selector: '[hraDoubleClickEvent]',
  exportAs: 'hraDoubleClickEvent',
})
export class DoubleClickEventDirective extends BaseEventDirective<CoreEvents['DoubleClick']> {
  /** Event type */
  override readonly event = () => CoreEvents.DoubleClick;
  /** Event properties */
  override readonly props = input<EventPropsFor<CoreEvents['DoubleClick']>>('', { alias: 'hraDoubleClickEvent' });
  /** 'none' if events are sent programatically */
  override readonly triggerOn = input<'none' | undefined>(undefined, { alias: 'hraDoubleClickEventTriggerOn' });
  /** Whether this event is disabled */
  override readonly disabled = input(false, { alias: 'hraDoubleClickEventDisabled', transform: booleanAttribute });
}

/**
 * Specialized version of `hraEvent` that only emits hover events
 *
 * @see {@link EventDirective}
 */
@Directive({
  selector: '[hraHoverEvent]',
  exportAs: 'hraHoverEvent',
})
export class HoverEventDirective extends BaseEventDirective<CoreEvents['Hover']> {
  /** Event type */
  override readonly event = () => CoreEvents.Hover;
  /** Event properties */
  override readonly props = input<EventPropsFor<CoreEvents['Hover']>>('', { alias: 'hraHoverEvent' });
  /** mouseenter, mouseleave, mouseover, mouseout, or 'none' if events are sent programatically */
  override readonly triggerOn = input<
    EventTrigger<'mouseenter' | 'mouseleave' | 'mouseover' | 'mouseout'> | 'none' | undefined
  >(undefined, { alias: 'hraHoverEventTriggerOn' });
  /** Whether this event is disabled */
  override readonly disabled = input(false, { alias: 'hraHoverEventDisabled', transform: booleanAttribute });
}

/**
 * Specialized version of `hraEvent` that only emits keyboard events
 *
 * @see {@link EventDirective}
 */
@Directive({
  selector: '[hraKeyboardEvent]',
  exportAs: 'hraKeyboardEvent',
})
export class KeyboardEventDirective extends BaseEventDirective<CoreEvents['Keyboard']> {
  /** Event type */
  override readonly event = () => CoreEvents.Keyboard;
  /** Event properties */
  override readonly props = input<EventPropsFor<CoreEvents['Keyboard']>>('', { alias: 'hraKeyboardEvent' });
  /** keydown (default), keyup, or 'none' if events are sent programatically */
  override readonly triggerOn = input<EventTrigger<'keyup' | 'keydown'> | 'none' | undefined>(undefined, {
    alias: 'hraKeyboardEventTriggerOn',
  });
  /** Whether this event is disabled */
  override readonly disabled = input(false, { alias: 'hraKeyboardEventDisabled', transform: booleanAttribute });
}

/** A list of property keys or a filter function */
export type ModelChangeFilter = PropertyKey[] | ((value: unknown) => unknown);
/** Either additional event props or a model value filter */
export type ModelChangePropsOrFilter = EventPropsFor<CoreEvents['ModelChange']> | ModelChangeFilter;

/**
 * Specialized version of `hraEvent` that only emits when a NgControl's value changes
 *
 * @see {@link EventDirective}
 */
@Directive({
  selector: '[hraModelChangeEvent]',
  exportAs: 'hraModelChangeEvent',
})
export class ModelChangeEventDirective extends BaseEventDirective<CoreEvents['ModelChange']> implements AfterViewInit {
  /** Event type */
  override readonly event = () => CoreEvents.ModelChange;
  /** Event properties */
  override readonly props = () => '' as const;
  /** Event props or a model value filter */
  readonly propsOrFilter = input<ModelChangePropsOrFilter>('', { alias: 'hraModelChangeEvent' });
  /** Always triggered programatically */
  override readonly triggerOn = () => 'none' as const;
  /** Whether this event is disabled */
  override readonly disabled = input(false, { alias: 'hraModelChangeEventDisabled', transform: booleanAttribute });

  /** Model control reference */
  private readonly ngControl = inject(NgControl);

  /** Cleanup manager */
  private readonly destroyRef = inject(DestroyRef);

  /** Connect to the NgControl */
  ngAfterViewInit(): void {
    const { valueChanges } = this.ngControl;
    valueChanges?.pipe(takeUntilDestroyed(this.destroyRef), skip(1)).subscribe((value) => {
      const props = this.selectProps(value, this.propsOrFilter());
      this.logEvent(undefined, undefined, props);
    });
  }

  /**
   * Selects event properties based on the current model value
   *
   * @param value Latest model value
   * @param propsOrFilter Additional event properties or a value filter
   * @returns Event properties
   */
  private selectProps(
    value: unknown,
    propsOrFilter: ModelChangePropsOrFilter,
  ): EventPayloadFor<CoreEvents['ModelChange']> {
    if (propsOrFilter === '') {
      return { value };
    } else if (typeof propsOrFilter === 'function') {
      return { value: propsOrFilter(value) };
    } else if (typeof propsOrFilter === 'object' && !Array.isArray(propsOrFilter)) {
      return { value, ...propsOrFilter };
    } else if (value === undefined || value === null || propsOrFilter.length === 0) {
      return { value: {} };
    }

    const source = value as UnknownRecord;
    const result: UnknownRecord = {};
    for (const key of propsOrFilter) {
      if (key in source) {
        result[key] = source[key];
      }
    }

    return { value: result };
  }
}

results matching ""

    No results matching ""