File

libs/ui/option/src/lib/option/option.component.ts

Description

Single option inside of a TsSelectionListComponent

Implements

Highlightable AfterContentInit AfterViewChecked OnDestroy

Example

<ts-option
  id="my-id"
  [isDisabled]="true"
  [option]="myOptionObject"
  value="My value!"
  (selectionChange)="selectedStateChanged($event)"
></ts-option>

<example-url>https://getterminus.github.io/ui-demos-release/components/selection-list</example-url>

Metadata

changeDetection ChangeDetectionStrategy.OnPush
encapsulation ViewEncapsulation.None
exportAs tsOption
host {
}
selector ts-option
styleUrls ./option.component.scss
templateUrl ./option.component.html

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, ngZone: NgZone, parent: TsOptionParentComponent, group: TsOptgroupParentComponent)
Parameters :
Name Type Optional
elementRef ElementRef No
changeDetectorRef ChangeDetectorRef No
ngZone NgZone No
parent TsOptionParentComponent No
group TsOptgroupParentComponent No

Inputs

id
Type : string

Define an ID for the component

isDisabled
Type : boolean

Whether the option is disabled

option

Define the option data object (needed for template support)

value
Type : any

The form value of the option

Outputs

selectionChange
Type : EventEmitter

Event emitted when the option is selected or deselected

Methods

Public deselect
deselect()

Deselect the option

Returns : void
Public getLabel
getLabel()

Return the view value

Used by ListKeyManagerOption

Returns : string
Public handleKeydown
handleKeydown(event: KeyboardEvent)

Ensure the option is selected when activated from the keyboard

Parameters :
Name Type Optional
event KeyboardEvent No
Returns : void
Public select
select()

Select the option

Returns : void
Public selectViaInteraction
selectViaInteraction()

Selects the option while indicating the selection came from the user.

Used to determine if the select's view -> model callback should be invoked.

Returns : void
Public setActiveStyles
setActiveStyles()

This method sets display styles on the option to make it appear active. This is used by the ActiveDescendantKeyManager so key events will display the proper options as active on arrow key events.

Returns : void
Public setInactiveStyles
setInactiveStyles()

This method removes display styles on the option that made it appear active. This is used by the ActiveDescendantKeyManager so key events will display the proper options as active on arrow key events.

Returns : void

Properties

Protected _id
Type : string
Default value : this.uid
Public active
Default value : false

Define the active state

Public autocompleteComponent
Default value : false

Whether parent component is an autocomplete component

Public displayElementRef
Type : TsOptionDisplayDirective | undefined
Decorators :
@ContentChild(TsOptionDisplayDirective)

Access the user-defined text content

Public elementRef
Type : ElementRef
Public Readonly group
Type : TsOptgroupParentComponent
Decorators :
@Optional()
@Inject(TS_OPTGROUP_PARENT_COMPONENT)
Public optionTemplate
Type : TemplateRef<any> | undefined
Decorators :
@ContentChild(TemplateRef)

Optional template passed in by the consumer

Public selectComponent
Default value : false

Whether parent component is an autocomplete component

Public selected
Default value : false

Whether or not the option is currently selected

Public Readonly stateChanges
Default value : new Subject<void>()

Emits when the state of the option changes and any parents have to be notified

Public title
Type : string
Default value : ''

Store the text for the title attribute

Protected uid
Default value : `ts-option-${nextUniqueId++}`

Define the default component ID

Accessors

allowMultiple
getallowMultiple()

Whether the wrapping component is in multiple selection mode

Returns : boolean
tabIndex
gettabIndex()

Returns the correct tabindex for the option depending on the disabled state

Returns : string
hostElement
gethostElement()

Gets the host DOM element

Returns : HTMLElement
viewValue
getviewValue()

The displayed value of the option.

It is necessary to show the selected option in the TsSelectComponent trigger.

Returns : string
id
getid()
setid(value: string)

Define an ID for the component

Parameters :
Name Type Optional
value string No
Returns : void
isDisabled
getisDisabled()
setisDisabled(value: boolean)

Whether the option is disabled

Parameters :
Name Type Optional
value boolean No
Returns : void
option
getoption()
setoption(value)

Define the option data object (needed for template support)

Parameters :
Name Optional
value No
Returns : void
import { Highlightable } from '@angular/cdk/a11y';
import {
  AfterContentInit,
  AfterViewChecked,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ContentChild,
  ElementRef,
  EventEmitter,
  Inject,
  InjectionToken,
  Input,
  isDevMode,
  NgZone,
  OnDestroy,
  Optional,
  Output,
  QueryList,
  TemplateRef,
  ViewEncapsulation,
} from '@angular/core';
import { NgModel } from '@angular/forms';
import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';

import { KEYS } from '@terminus/fe-utilities';
import { TsStyleThemeTypes } from '@terminus/ui-utilities';

import { TsOptionDisplayDirective } from './option-display.directive';

export interface TsOption {
  isDisabled?: boolean;
  children?: TsOption[];
}

/**
 * Event object emitted by {@link TsOptionComponent} when selected or deselected
 */
export class TsOptionSelectionChange {
  constructor(
    // Reference to the option that emitted the event
    public source: TsOptionComponent,
    // Whether the change in the option's value was a result of a user action
    public isUserInput = false,
  ) {}
}

/**
 * Describes a parent component that manages a list of options.
 *
 * Contains properties that the options can inherit. Used by {@link TS_OPTION_PARENT_COMPONENT}
 */
export interface TsOptionParentComponent {
  componentName: string;
  allowMultiple: boolean;
  theme: TsStyleThemeTypes;
  ngControl?: NgModel;
}

/**
 * Injection token used to provide the parent component to options. Used by {@link TsOptionComponent}
 *
 * Since TsSelectionListComponent imports TsOptionComponent, importing TsSelectionListComponent here will cause a circular dependency.
 * Injecting via an InjectionToken helps us circumvent that limitation.
 */
export const TS_OPTION_PARENT_COMPONENT = new InjectionToken<TsOptionParentComponent>('TS_OPTION_PARENT_COMPONENT');

/**
 * Describes a parent optgroup component. Used by {@link TS_OPTGROUP_PARENT_COMPONENT}
 */
export interface TsOptgroupParentComponent {
  optgroupOptions: QueryList<TsOptionComponent>;
  isDisabled: boolean;
  triggerChangeDetection: Function;
}

/**
 * Injection token used to provide the parent optgroup to options. Used by {@link TsOptgroupComponent}
 */
export const TS_OPTGROUP_PARENT_COMPONENT = new InjectionToken<TsOptgroupParentComponent>('TS_OPTGROUP_PARENT_COMPONENT');

// Unique ID for each instance
let nextUniqueId = 0;


/**
 * Single option inside of a {@link TsSelectionListComponent}
 *
 * @example
 * <ts-option
 *              id="my-id"
 *              [isDisabled]="true"
 *              [option]="myOptionObject"
 *              value="My value!"
 *              (selectionChange)="selectedStateChanged($event)"
 * ></ts-option>
 *
 * <example-url>https://getterminus.github.io/ui-demos-release/components/selection-list</example-url>
 */
@Component({
  selector: 'ts-option',
  templateUrl: './option.component.html',
  styleUrls: ['./option.component.scss'],
  host: {
    'class': 'ts-option',
    'role': 'option',
    '[class.ts-selected]': 'selected',
    '[class.ts-option--multiple]': 'allowMultiple',
    '[class.ts-option--active]': 'active',
    '[class.ts-option--disabled]': 'isDisabled',
    '[class.ts-option--template]': 'optionTemplate',
    '[attr.tabindex]': 'tabIndex',
    '[attr.aria-selected]': 'selected.toString()',
    '[attr.aria-disabled]': '!!isDisabled',
    '[attr.title]': 'title',
    '[id]': 'id',
    '(click)': 'selectViaInteraction()',
    '(keydown)': 'handleKeydown($event)',
  },
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  exportAs: 'tsOption',
})
export class TsOptionComponent implements Highlightable, AfterContentInit, AfterViewChecked, OnDestroy {
  /**
   * Store the most recent view value
   */
  private mostRecentViewValue = '';

  /**
   * Emits when the state of the option changes and any parents have to be notified
   */
  public readonly stateChanges = new Subject<void>();

  /**
   * Store the text for the title attribute
   */
  public title = '';

  /**
   * Define the default component ID
   */
  protected uid = `ts-option-${nextUniqueId++}`;

  /**
   * Define the active state
   */
  public active = false;

  /**
   * Whether the wrapping component is in multiple selection mode
   */
  public get allowMultiple(): boolean {
    return !!(this.parent && this.parent.allowMultiple);
  }

  /**
   * Whether or not the option is currently selected
   */
  public selected = false;

  /**
   * Whether parent component is an autocomplete component
   */
  public autocompleteComponent = false;

  /**
   * Whether parent component is an autocomplete component
   */
  public selectComponent = false;

  /**
   * Returns the correct tabindex for the option depending on the disabled state
   */
  public get tabIndex(): string {
    return this.isDisabled ? '-1' : '0';
  }

  /**
   * Gets the host DOM element
   */
  public get hostElement(): HTMLElement {
    return this.elementRef.nativeElement;
  }

  /**
   * The displayed value of the option.
   *
   * It is necessary to show the selected option in the {@link TsSelectComponent} trigger.
   */
  public get viewValue(): string {
    // Use the user defined content if the {@link TsOptionDisplayDirective} was used
    const content = this.displayElementRef ? this.displayElementRef.elementRef.nativeElement.textContent : this.hostElement.textContent;
    return (content || '').trim();
  }

  /**
   * Optional template passed in by the consumer
   */
  @ContentChild(TemplateRef)
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  public optionTemplate: TemplateRef<any> | undefined;

  /**
   * Access the user-defined text content
   */
  @ContentChild(TsOptionDisplayDirective)
  public displayElementRef: TsOptionDisplayDirective | undefined;

  /**
   * Define an ID for the component
   *
   * @param value
   */
  @Input()
  public set id(value: string) {
    this._id = value || this.uid;
  }
  public get id(): string {
    return this._id;
  }
  protected _id: string = this.uid;

  /**
   * Whether the option is disabled
   *
   * @param value
   */
  @Input()
  public set isDisabled(value: boolean) {
    this._isDisabled = value;
  }
  public get isDisabled(): boolean {
    return (this.group && this.group.isDisabled) || this._isDisabled;
  }
  private _isDisabled = false;

  /**
   * Define the option data object (needed for template support)
   *
   * @param value
   */
  @Input()
  public set option(value: TsOption | undefined) {
    this._option = value;
  }
  public get option(): TsOption | undefined {
    return this._option;
  }
  private _option: TsOption | undefined;

  /**
   * The form value of the option
   */
  @Input()
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  public value: any;

  /**
   * Event emitted when the option is selected or deselected
   */
  @Output()
  public readonly selectionChange = new EventEmitter<TsOptionSelectionChange>();


  constructor(
    public elementRef: ElementRef,
    private changeDetectorRef: ChangeDetectorRef,
    private ngZone: NgZone,
    // Injecting via a provider helps us get around the circular dependency created by importing TsSelectComponent here.
    @Optional() @Inject(TS_OPTION_PARENT_COMPONENT) private parent: TsOptionParentComponent,
    @Optional() @Inject(TS_OPTGROUP_PARENT_COMPONENT) public readonly group: TsOptgroupParentComponent,
  ) {
    if (parent.componentName === 'TsAutocompleteComponent') {
      this.autocompleteComponent = true;
    } else if (parent.componentName === 'TsSelectComponent') {
      this.selectComponent = true;
    }
  }


  /**
   * If the user is trying to use a template without passing in data, alert the dev
   */
  public ngAfterContentInit(): void {
    // If a template is passed in but no option object, alert the consumer
    if (this.optionTemplate && !this.option && isDevMode()) {
      throw Error(`TsOptionComponent: The full 'option' object must be passed in when using a custom template.`);
    }

    // Set the title once the zone is stable. This is needed to avoid an ExpressionChangedAfterChecked error
    this.ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
      this.title = this.viewValue;
    });
  }

  /**
   * Trigger state changes if the view value has changed
   */
  public ngAfterViewChecked(): void {
    // Since parent components could be using the option's label to display the selected values
    // (e.g. `ts-select`) and they don't have a way of knowing if the option's label has changed
    // we have to check for changes in the DOM ourselves and dispatch an event. These checks are
    // relatively cheap, however we still limit them only to selected options in order to avoid
    // hitting the DOM too often.
    // istanbul ignore else
    if (this.selected) {
      const viewValue = this.viewValue;

      // istanbul ignore else
      if (viewValue !== this.mostRecentViewValue) {
        this.mostRecentViewValue = viewValue;
        this.stateChanges.next();
      }
    }
  }

  /**
   * Complete observables
   */
  public ngOnDestroy(): void {
    this.stateChanges.complete();
  }

  /**
   * Return the view value
   *
   * Used by `ListKeyManagerOption`
   */
  public getLabel(): string {
    return this.viewValue;
  }

  /**
   * Deselect the option
   */
  public deselect(): void {
    if (this.selected) {
      this.selected = false;
      this.changeDetectorRef.markForCheck();
      this.emitSelectionChangeEvent();
    }

    // Trigger update for the optgroup if a child changes
    // istanbul ignore else
    if (this.group && this.allowMultiple) {
      this.group.triggerChangeDetection(this.id);
    }
  }

  /**
   * Ensure the option is selected when activated from the keyboard
   *
   * @param event
   */
  public handleKeydown(event: KeyboardEvent): void {
    // istanbul ignore else
    if (event.code === KEYS.ENTER.code || event.code === KEYS.SPACE.code) {
      this.selectViaInteraction();

      // Prevent the page from scrolling down and form submits.
      event.preventDefault();
    }
  }

  /**
   * Select the option
   */
  public select(): void {
    if (!this.selected) {
      this.selected = true;
      this.changeDetectorRef.markForCheck();
      this.emitSelectionChangeEvent();
    }

    // Trigger update for the optgroup if a child changes
    // istanbul ignore else
    if (this.group && this.allowMultiple) {
      this.group.triggerChangeDetection(this.id);
    }
  }

  /**
   * Selects the option while indicating the selection came from the user.
   *
   * Used to determine if the select's view -> model callback should be invoked.
   */
  public selectViaInteraction(): void {
    // istanbul ignore else
    if (!this.isDisabled) {
      this.selected = this.allowMultiple ? !this.selected : true;
      this.changeDetectorRef.markForCheck();
      this.emitSelectionChangeEvent(true);
    }
  }

  /**
   * This method sets display styles on the option to make it appear active. This is used by the ActiveDescendantKeyManager so key events
   * will display the proper options as active on arrow key events.
   */
  public setActiveStyles(): void {
    // istanbul ignore else
    if (!this.active) {
      this.active = true;
      this.changeDetectorRef.markForCheck();
    }
  }

  /**
   * This method removes display styles on the option that made it appear active. This is used by the ActiveDescendantKeyManager so key
   * events will display the proper options as active on arrow key events.
   */
  public setInactiveStyles(): void {
    if (this.active) {
      // HACK: For some reason, triggering change detection works in `setActiveStyles` above, but not here.
      // Same issue seems preset in TsSelectComponent `autocompleteDeselectItem`.
      setTimeout(() => {
        this.active = false;
        this.changeDetectorRef.markForCheck();
      });
    }
  }

  /**
   * Emit the selection change event
   *
   * @param isUserInput
   */
  private emitSelectionChangeEvent(isUserInput = false): void {
    this.selectionChange.emit(new TsOptionSelectionChange(this, isUserInput));
  }
}

result-matching ""

    No results matching ""