jelhan/croodle

View on GitHub
app/components/create-options.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { TrackedArray, TrackedSet } from 'tracked-built-ins';
import IntlMessage from '../utils/intl-message';
import { tracked } from '@glimmer/tracking';
import type RouterService from '@ember/routing/router-service';
import type { CreateOptionsRouteModel } from '../routes/create/options';
import type Transition from '@ember/routing/transition';

export class FormDataOption {
  @tracked value;
  formData;

  get valueValidation() {
    const { formData, value } = this;

    // Every option must have a label
    if (!value) {
      return new IntlMessage('create.options.error.valueMissing');
    }

    // Options must be unique. There must not be another option having the
    // same value before
    const isUnique = !formData.options
      .slice(0, this.formData.options.indexOf(this))
      .some((option) => option.value === this.value);
    if (!isUnique) {
      return new IntlMessage('create.options.error.duplicatedOption');
    }

    return null;
  }

  get isValid() {
    return this.valueValidation === null;
  }

  constructor(formData: FormData, value: string) {
    this.formData = formData;
    this.value = value;
  }
}

class FormData {
  @tracked options;

  get optionsValidation() {
    const { options } = this;

    if (options.length < 1) {
      // UI enforces that there is at least one option if poll type is `MakeAPoll`.
      // This validation error can only happen if poll type is `FindADate`.
      return new IntlMessage('create.options.error.notEnoughDates');
    }

    if (options.some((option) => !option.isValid)) {
      return new IntlMessage('create.options.error.invalidOption');
    }

    return null;
  }

  @action
  updateOptions(values: string[]) {
    this.options = new TrackedArray(
      values.map((value) => new FormDataOption(this, value)),
    );
  }

  @action
  addOption(value: string, afterPosition = this.options.length - 1) {
    const option = new FormDataOption(this, value);

    this.options.splice(afterPosition + 1, 0, option);
  }

  @action
  deleteOption(option: FormDataOption) {
    this.options.splice(this.options.indexOf(option), 1);
  }

  constructor(
    { options }: { options: Set<string> },
    { defaultOptionCount }: { defaultOptionCount: number },
  ) {
    const normalizedOptions =
      options.size === 0 && defaultOptionCount > 0
        ? ['', '']
        : Array.from(options);

    this.options = new TrackedArray(
      normalizedOptions.map((value) => new FormDataOption(this, value)),
    );
  }
}

export interface CreateOptionsSignature {
  Args: {
    poll: CreateOptionsRouteModel;
  };
}

export default class CreateOptions extends Component<CreateOptionsSignature> {
  @service declare router: RouterService;

  formData = new FormData(
    { options: this.options },
    { defaultOptionCount: this.args.poll.pollType === 'MakeAPoll' ? 2 : 0 },
  );

  get options() {
    const { poll } = this.args;
    const { dateOptions, freetextOptions, pollType } = poll;

    return pollType === 'FindADate' ? dateOptions : freetextOptions;
  }

  @action
  previousPage() {
    this.router.transitionTo('create.meta');
  }

  @action
  submit() {
    const { pollType } = this.args.poll;

    if (pollType === 'FindADate') {
      this.router.transitionTo('create.options-datetime');
    } else {
      this.router.transitionTo('create.settings');
    }
  }

  @action handleTransition(transition: Transition) {
    if (transition.from?.name === 'create.options') {
      this.updatePoll();
      this.router.off('routeWillChange', this.handleTransition);
    }
  }

  updatePoll() {
    const { poll } = this.args;
    const { pollType } = poll;
    const { options } = this.formData;
    const pollOptions = options.map(({ value }) => value);

    if (pollType === 'FindADate') {
      poll.dateOptions = new TrackedSet(pollOptions.sort());
    } else {
      poll.freetextOptions = new TrackedSet(pollOptions);
    }
  }

  constructor(owner: unknown, args: CreateOptionsSignature['Args']) {
    super(owner, args);

    // Cannot use a destructor because that one runs _after_ the other component
    // rendered by the next route is initialized.
    this.router.on('routeWillChange', this.handleTransition);
  }
}

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry {
    CreateOptions: typeof CreateOptions;
  }
}