Skip to content

DraftAssistant for a Date field

WARNING

This example is not recommended for use in production, it is mainly intended for showcase purposes.

The Date object is considered a legacy feature. It is recommended to use the Temporal API for new projects by MDN.

The following example aims to show how to define new assistants for specific needs.

Suppose that, for some reason, using a date picker for a Date field in a form is not possible.

ts
import {
  FieldDraftAssistant,
  SectionDraftAssistant,
  Assertion,
  LabelId,
  type ModelFromContainer,
} from "self-assert";

export class DateDraftAssistant<
  ContainerModel = any
> extends SectionDraftAssistant<Date, ContainerModel, [string]> {
  static readonly defaultAssertionDescription = "Invalid date";

  static for<ContainerModel = any>(
    assertionId: LabelId,
    modelFromContainer: ModelFromContainer<Date, ContainerModel>
  ): DateDraftAssistant<ContainerModel> {
    const assertionIds = assertionId === "" ? [] : [assertionId];

    /** @ts-expect-error /microsoft/TypeScript#5863 */
    return this.with(
      [this.createDateAssistant()],
      (dateAsString) => this.createDate(assertionId, dateAsString),
      modelFromContainer,
      assertionIds
    );
  }

  static forTopLevel(assertionId: LabelId) {
    return this.for(assertionId, this.topLevelModelFromContainer());
  }

  static createDate(assertionId: LabelId, dateAsString: string) {
    this.createAssertionFor(assertionId, dateAsString).mustHold();

    return new Date(dateAsString);
  }

  static createDateAssistant() {
    return FieldDraftAssistant.handling<Date>("", (date) =>
      date.toISOString().substring(0, 10)
    );
  }

  static createAssertionFor(assertionId: LabelId, dateAsString: string) {
    return Assertion.requiring(
      assertionId,
      DateDraftAssistant.defaultAssertionDescription,
      () =>
        /^\d{4}-\d{2}-\d{2}$/.test(dateAsString) &&
        !isNaN(new Date(dateAsString).getTime())
    );
  }

  innerAssistant() {
    return this.assistants[0];
  }

  setInnerModel(newModel: string) {
    this.innerAssistant().setModel(newModel);
  }

  getInnerModel() {
    return this.innerAssistant().getModel();
  }
}
ts
import { describe, expect, it } from "@jest/globals";
import { DateDraftAssistant } from "./DateDraftAssistant";
import { DraftAssistant } from "self-assert";

describe("DateDraftAssistant", () => {
  it("should handle ISO dates", (done) => {
    const assistant = DateDraftAssistant.forTopLevel("");

    assistant.setInnerModel("2020-01-01");

    assistant.withCreatedModelDo(
      (model) => {
        expect(assistant.getInnerModel()).toBe("2020-01-01");
        expect(model).toBeInstanceOf(Date);
        expect(model.toISOString().startsWith("2020-01-01")).toBe(true);
        done();
      },
      () => done("Should not be invalid")
    );
  });

  it("should be invalid if the inner model does not have a valid ISO format", (done) => {
    const assertionId = "ISODateAID";
    const assistant = DateDraftAssistant.forTopLevel(assertionId);

    assistant.setInnerModel("01/01/2020");

    assistant.withCreatedModelDo(
      () => done("Should be invalid"),
      () => {
        expect(DraftAssistant.isInvalidModel(assistant.getModel())).toBe(true);
        expect(assistant.hasOnlyOneRuleBrokenIdentifiedAs(assertionId)).toBe(
          true
        );
        expect(assistant.brokenRulesDescriptions()).toEqual([
          DateDraftAssistant.defaultAssertionDescription,
        ]);
        done();
      }
    );
  });

  it("should be invalid if the inner model has a valid format but is not an ISO date", (done) => {
    const assertionId = "ISODateAID";
    const assistant = DateDraftAssistant.forTopLevel(assertionId);

    assistant.setInnerModel("2020-13-01");

    assistant.withCreatedModelDo(
      () => done("Should be invalid"),
      () => {
        expect(DraftAssistant.isInvalidModel(assistant.getModel())).toBe(true);
        expect(assistant.hasOnlyOneRuleBrokenIdentifiedAs(assertionId)).toBe(
          true
        );
        expect(assistant.brokenRulesDescriptions()).toEqual([
          DateDraftAssistant.defaultAssertionDescription,
        ]);
        done();
      }
    );
  });

  describe("inner assistant", () => {
    it("should allow setting its value from an ISO date", () => {
      const assistant = DateDraftAssistant.forTopLevel("");

      assistant.innerAssistant().setModelFrom(new Date(2020, 0, 1));

      expect(assistant.getInnerModel()).toBe("2020-01-01");
    });
  });
});

Breakdown

This example demonstrates a custom DraftAssistant implementation for a specific case:

  • It uses a SectionDraftAssistant that wraps a FieldDraftAssistant<string>.
  • The user inputs a string expected to match the YYYY-MM-DD format (ISO date).
  • A validation rule ensures the string is a valid date in that format.
  • If the validation passes, a Date object is created and returned as the model.
  • Otherwise, the model is considered invalid and validation errors are exposed.

Purpose

This showcases how to:

  • Build custom assistants for specialized fields.
  • Wrap legacy types in a safe and testable way.
  • Validate string inputs before converting to more complex objects.
  • Compose assistants to manage draft data at multiple levels.