Coding the System Model, Part 1

Lincoln Anderson
Lincoln Anderson
May 2, 2022

When I code the system model, I first want to define the shape of the data (the models and characteristics), and create a way to see and manipulate what data exists. I want to do this on a global scale so I have 100% oversight of what’s going on, with no filters. As a lover of video games, I like to call this “god mode”.

Note: Our app uses Firebase as the central data store and it comes with a web-based console where the data views are updated real time. That’s super helpful for debugging, but the views aren’t customizable and aren’t always the most ergonomic.

Models & Relationships

Since I’m using Typescript, I can define our models as “interfaces”. And since I’m using Firebase, I’ll create methods and hooks for loading, adding, updating, and removing data using those interfaces, nesting the document structure to imply the hierarchical relationship between models. (Here I leverage the firebase/firestore  and react-firebase-hooks/firestore libraries, so that each method is only a couple of lines.) I’m also using a set of generic factory functions I’ve written which populate records with an “id” field, so I’ll add those fields to the interface models as well.

/* timerSegment.ts */

export interface TimerSegment {
  id: string;
  label: string;
  duration: number;
}

export const addTimerSegment = (
  timerId: string,
  data: DataInput<TimerSegment>
) => add(`timers/${timerId}/segments`, data);
export const useAllTimerSegments = (timerId: string) =>
  useAll<TimerSegment>(`timers/${timerId}/segments`);

Note: Further down the line when I’m confident in my approach, I could add what’s called a “anti-corruption layer” to make sure the data being loaded truly conforms to the interface. But for now, let’s plow ahead!

From here I can create pages to represent our models, and display the data on those pages, using some very basic formatting so that I can understand what I’m looking at. At first, the pages will be blank because there is no data, but I can simply manufacture some data through the Firebase console so there is something to look at. This is also a good way to confirm that the way I’m structuring things in Firebase matches our interface models. I can do this for all our models, then link them together by adding an “id” field to each one and leveraging next-link and  next-router.

/* pages/system/timer/index.tsx */

const TimersPage: NextPage = () => {
  const [timers, loading, errorMessage] = useAllTimers();

  if (loading) return <div>Loading...</div>;
  if (errorMessage) return <div>Error: {errorMessage}</div>;

  return (
    <div>
      <h1>Timers</h1>
      <ul>
        {timers.map((timer) => {
          return (
            <li key={timer.id}>
              <NextLink href={`timer/${timer.id}`}>{timer.id}</NextLink>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

The Timer Detail page captures the timer Id in the route segment and loads the data relevant to that timer. I’ll also place the relevant Segments and Events on the Timer Detail page as subcomponents so I can see everything in one place.

/* pages/system/timer/[timerId].tsx */

const TimerDetailPage: NextPage = () => {
  const router = useRouter();
  const { timerId } = router.query;

  if (typeof timerId !== 'string') {
    return <div>Error: invalid timer ID.</div>;
  }

  return (
    <div>
      <h1>Timer {timerId}</h1>
      <TimerEvents timerId={timerId} />
      <TimerSegments timerId={timerId} />
    </div>
  );
};

Tasks

At this point, I can implement our system’s tasks. Since they are all “adding” data, I can build a simple form for each one. This is a good time to start using our design system instead of plain HTML. I’ll go ahead and implement ChakraUI with a quick find-and-replace of basic elements, and then flesh out the Add Timer Event form using react-hook-form.

/* pages/system/timer/[timerId].tsx */

const AddTimerEventForm: React.FC<{ timerId: string }> = ({ timerId }) => {
  const form = useForm<DataInput<TimerEvent>>();
  const onSubmit: SubmitHandler<DataInput<TimerEvent>> = (data) => {
    addTimerEvent(timerId, { ...data, time: Date.now() });
  };

  return (
    <UI.Box border="1px solid black" p={4}>
      <UI.Heading size="sm">Add Timer Event</UI.Heading>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <UI.FormControl>
          <UI.FormLabel htmlFor="type">Type</UI.FormLabel>
          <UI.Select name="type">
            <option value="start">start</option>
            <option value="stop">stop</option>
          </UI.Select>
        </UI.FormControl>
        <UI.Button type="submit">Add</UI.Button>
      </form>
    </UI.Box>
  );
};

Note: I’m often tempted to create all the possible tasks for adding, updating, and removing each model. But this would mean building stuff that doesn’t yet have a need. Unless you’re using a framework that does all this automatically, I would recommend keeping overhead low by only implementing these tasks as needed by your system model.

Now that I can see our data and add new records, a couple of awkward areas have become obvious:

  • The “duration” field is just a big number (in milliseconds). While mathematically handy, it’s hard to intuitively input and read.
  • The segments seem to be in a random order. This is because there is no inherent ordering mechanism, so the system orders by the generated ID that Firebase assigns.

For the duration field, I can create a custom React component that has a more intuitive user experience. I implemented three inputs (hours, minutes and seconds). To support this, I created two helper methods for converting the value from one number into three separate ones and back again.

For the segment ordering, I simply added a new field to its model called “order” and assigned the current time. (This is just a simple timestamp, but it’s going to make it possible in the future to adjust the “order” field with a custom value.)

Next steps

At this point, our models and relationships are visible and manageable.

In the next post, I’ll build upon our models & relationships by adding calculations (ie. derived values), and some basic processes.

« previous postnext post »