2 Client Rendering

Client Rendering

Deadline: Saturday, April 18, 2026.

Plagiarism is theft and is unacceptable. It undermines creativity, damages intellectual integrity, and destroys the purpose of learning. This also applies to contract cheating (opens in a new tab) and the mindless use of chatbots/agents.1

Classes

Our end goal is to progressively develop a flashcards application over a series of three assignments, starting with this one.

Before proceeding, spend some time sketching your own design of the classes and their relationships, then as you work through the assignment, incrementally compare and contrast your design with the one described below.

The classroom repository (opens in a new tab) provides an obfuscated class hierarchy that supports creating different types of cards, organizing cards into slides, organizing slides into decks, and persisting a collection of decks. Cards have tags, and slides and decks have titles. The entire hierarchy is both serializable and deserializable.

The following is a detailed explanation of the design and structure of the provided classes. Understanding this hierarchy is essential for extending it in the subsequent sections of the assignment.

Items

All classes in this application are serializable and deserializable, allowing the application state to persist across sessions. The foundation of this application is an identifiable and de/serializable item entity.

The Item class has an identifier and a string representation. All the other classes extend Item as they are items themselves.

UML diagram for Item classItem#idconstructor(•)getid()toString()toJSON()fromJSON(•)

The toJSON() method converts an item to a plain object; fromJSON() does the reverse, returning an instance from a plain object. Each subtype implements its own fromJSON() method to return instances of its respective type.

The constructor accepts an optional plain object (POJO) argument with an id property, like { id: "JG6JmHW2vz" }, to set #id to a specific value. If no argument is provided or the id property is missing, the identifier is generated using nanoid (opens in a new tab):

import { nanoid } from "nanoid";
nanoid(10); //=> "JG6JmHW2vz"

The implementation uses nullish coalescing (opens in a new tab) and optional chaining (opens in a new tab) or destructuring (opens in a new tab) when assigning identifiers and other class fields based on plain object argument properties. These operators facilitate writing robust and concise expressions.

The constructor throws an error when called directly to prevent creating instances of an abstract class. Instances of concrete subtypes can still be created as they indirectly call the constructor.

Cards

The application supports multiple types of cards that serve as building blocks. To capture the common properties and behavior, a base (or general) card is defined, which is then specialized into different types of cards as needed.

UML diagram for Card subtypesCardCardType1CardType2...CardTypeN

The abstract Card class has a list of tags and a corresponding getter. The getter returns a direct reference to the underlying array so that it can be accessed and updated, but not reassigned.

UML diagram for Card classCard#tagsconstructor(•)gettags()toString()toJSON()fromJSON(•)

Cards can be constructed using plain object representations that set the corresponding fields. The following statements are all valid ways of indirectly constructing cards.

new Card();
new Card({});
new Card({ id: "JG6JmHW2vz" });
new Card({ tags: [] });
new Card({ tags: ["tag1", "tag2"] });
new Card({ id: "JG6JmHW2vz", tags: ["tag"] });

The toJSON() method returns a plain object representation of the underlying instance for serialization. Subtypes implement the inherited fromJSON() method. Calling fromJSON() directly throws an error.

Specialization

The application supports three types of cards: a foreign word, a playing card, and a list of strings. A foreign word has a word, a pronunciation, and a translation; a playing card has a pip and a suit; and a list of strings, well, is just a list of strings.

Defining various properties as private fields in every subtype would lead to an explosion of fields and methods. Instead, the design uses a single data property to encode each subtype’s properties as a plain object. Less is more (opens in a new tab).

UML diagram for Card subtypesCardForeignWord#dataconstructor(•)getdata()toString()toJSON()fromJSON(•)PlayingCard#dataconstructor(•)getdata()toString()toJSON()fromJSON(•)StringList#dataconstructor(•)getdata()toString()toJSON()fromJSON(•)

The classes ForeignWord, PlayingCard, and StringList each have a data getter that returns a direct reference to the underlying object so that it can be accessed and updated, but not reassigned. The specifics of these subtypes are quite similar except for the toString() method, as each type has a different data representation. The toJSON() method includes the type of the card in the plain object representation, as it is needed to resolve the type to use when deserializing.

Why is data not defined in the supertype, then? Although encapsulation helps maintain integrity and provides a clear interface for interacting with certain properties, it can inconveniently hide the corresponding private fields from the subtypes.

The data property is still visible through the getter, but it cannot be reassigned; a copy would have to be performed every time, which can be problematic in some situations. Protected fields solve this problem but are not supported by the language, so the most straightforward solution that preserves encapsulation is to move #data from the supertype to the subtypes.

The following statements are all valid ways of creating cards.

new StringList();
new PlayingCard({
  id: "JG6JmHW2vz",
  data: { pip: "Q", suit: "Hearts" },
});
new StringList({
  id: "JG6JmHW2vz",
  tags: ["todo"],
  data: { items: ["Task 1", "Task 2"] },
});
new ForeignWord({
  tags: ["japanese"],
  data: {
    word: "先生",
    pronunciation: "sensei",
    translation: "teacher",
  },
});
PlayingCard.fromJSON({ data: { pip: "A", suit: "Spades" } });
ForeignWord.fromJSON({
  tags: ["arabic", "basics"],
  data: { word: "شكرًا", pronunciation: "shukran", translation: "thank you" },
});

Repositories

Cards are grouped into slides, and slides are grouped into decks. This hierarchical structure requires CRUD (Create/Read/Update/Delete) repositories to manage and interact with these entities, as was done in the Bank class from 6.2 Classes.

On the most basic level, a data repository implements a CRUD interface that supports creating (inserting/adding), reading (selecting/getting), updating (modifying/patching), and deleting (removing) items.

UML diagram for CRUD interfaceCRUDcreate(•)read(•)update(•)delete(•)

However, interfaces are not natively supported, nor are parameterized types (generics), as there is little need for them with a dynamic and weakly-typed (opens in a new tab) language. Instead, a parametrized template Repository class is defined and its design is followed whenever a repository is needed. The Repository class is not instantiated directly and serves only as a reference.

UML diagram for Repository classRepositoryItem#itemsconstructor(•)getitems()get(•)add(•)remove(•)toJSON()fromJSON(•)#toItem(•)

The get(), add(), and remove() methods return a reference to the item that is read, created, or deleted, respectively. Updates are implemented indirectly through the get() method, which returns a direct reference to the item instance, allowing its modification.

The items() getter returns a copy of the list of items. This allows updating individual items without affecting the structure of the original list. This getter could live inside get() when called without arguments but is defined separately for convenience.

New items can be added by reference or created from plain object representations, and the two argument types are distinguished using the instanceof (opens in a new tab) operator in #toItem(). The #toItem() method is used by both the constructor() and the add() methods. Note that different types can be created based on the plain object representation.

The Slide and Deck classes each serve as a container of cards/slides and provide the corresponding CRUD repository methods. Both follow the Repository class design and have a read-write title property and a read-only tags property.

UML diagram for Slide classSlide#title#cardsconstructor(•)gettitle()settitle(•)getcards()gettags()get(•)add(•)remove(•)toString()toJSON()fromJSON(•)#toCard(•)UML diagram for Deck classDeck#title#slidesconstructor(•)gettitle()settitle(•)getslides()gettags()get(•)add(•)remove(•)toString()toJSON()fromJSON(•)#toSlide(•)

The default title is set to "Untitled" when none is provided. The tags of a slide/deck are a computed property that returns the union (without duplicates) of the tags of its cards/slides.

The following statements are all valid ways of creating decks and slides.

const deck = new Deck();
const slide = new Slide({ title: "Slide" });
 
slide.add({ type: "playing-card", data: { pip: "Q", suit: "Hearts" } });
 
const card = new PlayingCard({
  data: { pip: "J", suit: "Diamonds" },
});
slide.add(card);
 
slide.add(
  new PlayingCard({
    data: { pip: "K", suit: "Spades" },
  }),
);
deck.add(slide);
 
new Deck({
  id: "JG6JmHW2vz",
  title: "Deck",
  slides: [
    {
      title: "Slide 1",
      cards: [
        { type: "playing-card", data: { pip: "A", suit: "Diamonds" } },
        { type: "playing-card", data: { pip: "2", suit: "Spades" } },
      ],
    },
    {
      title: "Slide 2",
      cards: [
        {
          type: "string-list",
          tags: ["ideas"],
          data: {
            items: ["Idea 1", "Idea 2", "Idea 3"],
          },
        },
      ],
    },
  ],
});

Collection

A collection holds a list of decks and provides methods to display and persist that list. Although it could be designed as a repository, a basic module replaces it. There is no Collection class.

UML diagram for Collection moduleCollection+decks+load(•)+save(•)+toString()

Rendering

The toString() method maps every deck to its string representation and concatenates the results. The tags for each deck and slide are sorted.

## Deck {cd, ci, japanese, tag1, tag2}
 
### Words {japanese}
生きがい (ikigai): the reason for being
懐かしい (natsukashi): nostalgic
侘び寂び (wabi-sabi): beauty in imperfection
浮世 (ukiyo): fleeting life
積読 (tsundoku): book hoarder
食い倒れ (kuidaore): eat until you become bankrupt
勿体ない (mottainai): waste not, want not
 
### Cards {tag1, tag2}
A of Spades
Q of Hearts
K of Diamonds
10 of Clubs
 
### List {cd, ci}
+ Plan
+ Code
+ Build
+ Test
+ Release
+ Deploy
+ Operate
+ Measure

Persistence

A JSON file is used to persist the list of decks. Files are asynchronously read and written using Bun.file and Bun.write.

await Bun.write(file, JSON.stringify(decks)); // saving
decks = JSON.parse(await Bun.file(file).json()); // loading

These statements are part of the save() and load() methods, which handle serializing and deserializing the list of decks, respectively. Any errors thrown when reading and writing files are caught and handled appropriately.

A sample collection.json file is provided, with three types of cards: ForeignWord, PlayingCard, and StringList.

Diagram for JSON file structureid9aLzr7LiY0titleDeckslidesid6EzYE5IyLCtitleWordscardsidkHuyiZmp40tagstypeforeign-worddatajapaneseword生きがいpronunciationikigaitranslationthe reason for beingidp57tSVTfZqtagstypeforeign-worddatajapaneseword懐かしいpronunciationnatsukashitranslationnostalgicid17_L-t7C-Itagstypeforeign-worddatajapaneseword侘び寂びpronunciationwabi-sabitranslationbeauty in imperfectionidbc9R_0SzKhtagstypeforeign-worddatajapaneseword浮世pronunciationukiyotranslationfleeting lifeidwIaKYu7s0ntagstypeforeign-worddatajapaneseword積読pronunciationtsundokutranslationbook hoarderidhQgKI8msG-tagstypeforeign-worddatajapaneseword食い倒れpronunciationkuidaoretranslationeat until you become bankruptid4-ifng8wRFtagstypeforeign-worddatajapaneseword勿体ないpronunciationmottainaitranslationwaste not, want notiduoqCP1IuVvtitleCardscardsidAgFhdvbv6Wtagstypeplaying-carddatatag1pipAsuitSpadesidC9VuGPK4fYtagstypeplaying-carddatatag1tag2pipQsuitHeartsidlwnvM2rMbNtagstypeplaying-carddatapipKsuitDiamondsidEd9-i2U2fTtagstypeplaying-carddatatag2pip10suitClubsidj1onaOB0aMtitleListcardsidBjtpa9cehqtagstypestring-listdatacicditemsPlanCodeBuildTestReleaseDeployOperateMeasure

Requirements

Now that the class hierarchy is clear, here is what you will build on top of it. You will extend the provided base classes with rendering, persistence, and interactivity.

The directory base/scripts in the classroom repository (opens in a new tab) contains an obfuscated solution that is meant to be used as a black -box (opens in a new tab) and should not be modified. You will extend this solution to implement the required functionality.

Explicit iteration statements, such as for, while, and do, are not allowed. Only the nanoid and faker packages are allowed to be imported and used.

Testing

The directory test/scripts in the classroom repository (opens in a new tab) should be used for testing.

Use Bun/Jest (opens in a new tab) to test the following methods of Slide:

MethodTest
add(•)Adding a card using add(•) does add it to #cards.
remove(•)Removing a card using remove(•) does remove it from #cards.
get(•)Updating a card returned by get(•) does update the corresponding card in #cards.
cards()Pushing a card to the array returned by the getter of cards does not modify #cards.
tags()The elements of the array returned by the getter of tags are unique.
#toCard(•)Creating a card using #toCard(•) with a plain object does return the correct instance type.
> bun test
bun test v1.3.11 (af24e281)
 
test/slide.spec.js:
 class Slide > add(card) > adds the card to #cards [1.75ms]
 class Slide > remove(id) > removes the card with id from #cards [0.09ms]
 class Slide > get(id) > returns a reference to a card in #cards [0.07ms]
 class Slide > cards() > returns a copy of #cards and not a reference to it [0.04ms]
 class Slide > tags() > returns an array of unique elements [0.05ms]
 class Slide > #toCard(card) > uses the correct type when creating a card [0.11ms]
 
 6 pass
 0 fail
 7 expect() calls
Ran 6 tests across 1 file. [13.00ms]

Application

Extend the provided implementation with methods that use the class hierarchy to render a persistent collection of decks, slides, and cards, and implement event handlers for adding and removing items.

The main HTML page index.html imports scripts/index.js as a module, <script type="module">, which allows us to reuse the provided (obfuscated) solution as is. All the ESM package dependencies have to be imported remotely though, for example, the nanoid package is imported using import { nanoid } from "https://esm.sh/nanoid/nanoid".

Load and render the collection of decks inside the entry point, scripts/index.js, of your application.

Persistence

Collection exports two functions, load() and save(), for loading and saving the list of decks, respectively. The load() function either loads the collection from local storage or, when it fails to find it locally, fetches the collection from data/collection.json and saves it in local storage using the save() function.

UML diagram for overall application designCollectionDeckSlideCard

Install live-server (opens in a new tab) using bun add --dev live-server and run it in verbose mode to log all file modifications and server requests, which can be helpful when debugging. It is also required for serving the collection JSON file. Add a script to package.json with "scripts": { "start": "live-server --verbose --no-browser" } and use bun run start to launch the server:

> bun run start
$ live-server --verbose --no-browser
Serving "02-client-rendering" at
    http://127.0.0.1:8080
Ready for changes
GET / 200 3.194 ms - 13626
GET /styles.css 200 0.776 ms - 12363
GET /scripts/index.js 200 0.481 ms - 1414
GET /scripts/collection.js 200 0.925 ms - 7242
GET /scripts/base/item.js 200 0.242 ms - 1701
GET /scripts/deck.js 200 0.256 ms - 4508
GET /scripts/base/deck.js 200 0.377 ms - 3236
GET /scripts/slide.js 200 0.412 ms - 6655
GET /scripts/base/slide.js 200 2.656 ms - 3834
GET /scripts/card.js 200 0.355 ms - 3277
GET /scripts/base/card.js 200 0.502 ms - 1760
GET /scripts/cards/foreign-word.js 200 0.714 ms - 1833
GET /scripts/base/cards/foreign-word.js 200 0.284 ms - 1795
GET /scripts/cards/playing-card.js 200 0.758 ms - 1870
GET /scripts/base/cards/playing-card.js 200 0.372 ms - 1650
GET /scripts/cards/string-list.js 200 0.693 ms - 1621
GET /scripts/base/cards/string-list.js 200 0.447 ms - 1778
GET /scripts/utils/tags.js 200 0.477 ms - 1751
GET /data/collection.json 200 0.942 ms - 1776
GET /favicon.ico 200 1.289 ms - 111780

Keep in mind that the collection should be saved (serialized) whenever it is modified so that the changes persist between sessions.

Rendering

Collection also exports a render() function to render its list of decks. The main HTML page has a commented block with an outerHTML (opens in a new tab) output of the rendered collection from data/collection.json. Define a non-exported tags() function that generates all the tags of the collection, and use it when rendering.

Use the same HTML structure and CSS classes when rendering the collection using the DOM API. All the needed classes and styles are defined in styles.css and already imported in the main page. Do not use inline styles or define new ones.

The render() receives a DOM element which it appends its output to. The <body> element of the document is referenced using document.body and is used when rendering the collection. To be able to reuse the provided base classes, we will monkey (opens in a new tab) patch (opens in a new tab) them, that is, dynamically add properties to their prototype, with a render() method each:

scripts/deck.js
import Deck from "./base/deck.js";
 
Deck.prototype.render = function (element) {
  const container = document.createElement(•••);
  •••
  const callback = (event, id) => {
    •••
  };
  •••
  element.appendChild(container);
  return container;
};
 
export default Deck;

this and all its properties are available in render() as if it were defined inside the base class. Call lucide.createIcons() once in every render() method, after appending the container with the corresponding <span> elements to the document, to create the icons.

Actions

Decks, slides, and cards are added or removed using event handlers that call the corresponding repository methods. We will not render the whole collection after every change, like we did during the lab sessions, but incrementally update its DOM representation instead and render only the affected parts.

Attach event handlers to the forms to create new items and prevent the page from being reloaded, when submitting the form, using Event.preventDefault() (opens in a new tab). Each new item is rendered into the DOM element that contains the list of items for that collection, deck, or slide.

New items are randomly created using the Faker (opens in a new tab) library to generate the required fields for a deck, slide, or card, respectively. Import faker into your scripts using import { faker } from "https://esm.sh/@faker-js/faker" and use it.

Adding a card to a slide potentially affects the list of tags for that slide; therefore, the slide must update and render its tags and notify its deck of the change, using a callback function, so the deck can do the same. The same applies for adding a slide to a deck or a deck to a collection.

Update all render() methods to receive one additional argument, a callback function that fires whenever a repository method updates the collection. The callback() function receives the name of the event (signal) being triggered (sent) and the identifier of the associated item.

UML state diagram for a callbacksPageCollectionDeckSlideCardrender(element,callback)render(element,callback)callback(event,id)render(element,callback)callback(event,id)render(element,callback)callback(event,id)

A callback triggers a chain reaction when invoked in a nested child, a card, and bubbles up to the root element, a collection. Save the collection, only once in the Collection module, after every update.

Use the same callback chain to implement removing decks, slides, and cards. Only empty decks and slides can be removed. The callback functions, defined separately in every render() method, will support at least two events: update and remove.

Shuffling

A card is shuffled after clicking on it. Shuffling regenerates its tags and data without changing its type or position. You should refactor and reuse the random generation logic defined previously to generate random tags and data based on a specific card type. Removing a card will trigger an unnecessary shuffle event as it bubbles up. Stop it from propagating using Event.stopPropagation() (opens in a new tab).

Note that the Slide repository does not support replacing a card. You have to update the card in place and replace its container element in the document. The card should also notify its parent slide after being shuffled since the tags are being regenerated.

Filtering

Cards can be filtered based on their tags. All tags are shown by default and filtering applies to cards only; decks and slides are not affected. A card can be hidden by adding filtered to its list of CSS classes.

Filtering should be entirely handled in the Collection module using the DOM API without rendering any of the elements. Tags are toggled by clicking on them. Holding the Alt modifier key while clicking a tag toggles all other tags, effectively isolating or restoring the clicked tag.

Guidelines

  1. Push your solution to your private repository under assignments/02-client-rendering.
  2. Commit often and use meaningful message summaries and descriptions.
  3. Complete your work before the deadline; no late submissions.

Codebase

The following structure should be used to organize the codebase. There is no need to create additional top-level directories/files, but more directories/files can be created under data and results.

      • collection.json
        • foreign-word.js
        • playing-card.js
        • string-list.js
      • card.js
      • collection.js
      • deck.js
      • index.js
      • slide.js
      • slide.spec.js
    • index.html
    • readme.md
    • styles.css
  • Report

    Include a screenshot of the rendered page under results and push it along with the assignment.

    Complete the readme.md report and push it along with the assignment.

    readme.md
    # Report
     
    Xane Doe [email protected]
     
    ## 2 Client Rendering
     
    | Task        | Done? | Comments             |
    | :---------- | :---- | :------------------- |
    | Testing     | [ ]   |                      |
    | Application | [ ]   |                      |
    | Persistence | [ ]   |                      |
    | Rendering   | [ ]   |                      |
    | Actions     | [ ]   |                      |
    | Events      | [ ]   |                      |
    | Callbacks   | [ ]   |                      |
    | Shuffling   | [ ]   |                      |
    | Filtering   | [ ]   |                      |
    | Screenshot  | [ ]   |                      |
    | Report      | [x]   | Markdown is the way. |
    | Plagiarism  | [ ]   |                      |

    Rubric

    TaskPointsDetails
    Testing+10Unit tests
    Application+5Loading, Rendering
    Persistence+10Fetching, Storage
    Rendering+35API, Tags, Icons
    Actions+40Repositories, Events, Callbacks, Tags
    Shuffling+15Bonus
    Filtering+20Bonus
    Screenshot+5
    Quality+5Clean, structured, well-organized, indented code
    Report-20Evaluation, Comments
    Plagiarism-∞
    Total110

    Footnotes

    1. Student Code of Conduct Policy (opens in a new tab) / Article (1) — سياسة النظام الطلابي (opens in a new tab) \ البند (١).