Last updated on

Unguided lab: Webapps!

Welcome to the last lab of CS-214! To cap off the semester, you will be working in teams of three or four students to build your very own multi-user web application.

This lab aims to exercise the following concepts and techniques:

App suggestions

The following would all make for reasonable webapps to build in a team of three or four. Be creative! Let’s not have 50 clones of chess, please.

These are just a few examples—build something that you’re excited about!

Before you start

Setting things up

This lab is written entirely from scratch, so the set up is a bit different. Follow the steps below carefully to get started. If you encounter issues, create a public post on Ed or ask a staff member in person for help so that you can get started as soon as possible.

Clone the repository that we created for you

  1. Navigate to the following Gitlab group: https://gitlab.epfl.ch/cs214/ul2024/teams.

  2. Find your team’s repository: it will be named app followed by your team number. Do not create your own repository: you must use the one we created for you.

  3. Make sure that all of your team members (and only they) have access to it.

  4. Clone the repository.

Create an SBT project

The repository that we created for you is empty. To set it up for webapp development, follow the directions below.

These steps should only be performed once, by a single member of your team.

  1. Navigate into your the directory that you cloned.

    cd app*
    
  2. Create a new webapp project (this operation may take a while):

    sbt new https://gitlab.epfl.ch/cs214/ul2024/webapp-template.g8.git
    

    This command initializes a new project (this is a common pattern in complex projects: other frameworks use commands such as npx create …, yarn create …, spring boot new …, cargo init …, etc.).

  3. Start tracking your app with Git:

    git add .
    git commit -m "Initial commit"
    git push
    

Writing your proposal

  1. At the end of the existing README file, add the following:

    ## Proposal
    
    ### User stories
    
    …
    
    ### Requirements
    
    …
    
    ### Roles
    
    …
    
    ### Mock-ups
    
    ![](mockups/app.png)
    
  2. Fill in each section of the template above by replacing the .

  3. Create a mock-up image and save it under mockups/app.png. You may add more images if needed.

  4. Commit and push. Make sure that the README displays properly on https://gitlab.epfl.ch.

Getting started

This lab uses the same library (webapp-lib) as the Rock-Paper-Scissors lab (webapp-rps) does. To implement a webapp and register it to the framework, the high-level steps are the following:

  1. Implement and register your app’s logic (server side: state machine transitions and state projections).

  2. Implement and register your app’s UI (client side: rendering views and sending events).

  3. Implement the required (de)serialization code as a common “wire” to exchange data between the client and the server.

If you skipped the last lab (and hence did not implement the demo rock-paper-scissors app), now may be a good time to do so. It may also help to review the RPS architecture diagram.

First, you’ll need to figure your appId.

Figuring out your app ID

The webapp-lib library uses unique “app identifiers” (appId below) to connect the logic of your application to its frontend. Your appId should be of the form app<teamNumber>, where <teamNumber> is the number of your team as found on Moodle.

For example, the appId of team 123 is app123. The name of the repository we created for you matches your appId.

You must use this exact appId in the following places:

Using the wrong appId, or failing to register your app as described below, will cause all sorts of hard-to-diagnose issues.

Project architecture

Creating a new project with sbt new above will set up the following project structure (file names in angle brackets <...> are files that you will create during the implementation of your app):

Project root/
├── apps/
│   ├── js/
│   │   └── src/main/scala/
│   │       └── apps/
│   │           ├── <appId>/
│   │           │   └── <UI.scala>
│   │           └── MainJS.scala
│   ├── jvm/
│   │   └── src/
│   │       ├── main/
│   │       │   ├── scala/
│   │       │   │   └── apps/
│   │       │   │       ├── <appId>/
│   │       │   │       │   └── <Logic.scala>
│   │       │   │       └── MainJVM.scala
│   │       │   └── resources/www/static/
│   │       │       └── <appId.png> (app thumbnail)
│   │       └── test/
│   │           └── scala/
│   │               └── apps/
│   │                   └── <appId>/
│   │                       └── <Tests.scala>
│   └── shared/
│       └── src/main/scala/
│           └── apps/
│               └── <appId>/
│                   ├── <Wire.scala>
│                   └── <types.scala>
└── build.sbt

These files depend on the CS214 webapp library, which has the following structure:

Project Lib/
├── js/
│   └── src/main/scala/
│       └── cs214.webapp.client
├── jvm/
│   └── src/main/scala/
│       └── cs214.webapp.server
└── shared/
    └── src/main/scala/
        └── cs214.webapp

Build configuration

This lab uses a more complex build configuration than usual, which causes issues with completion and imports in VS Code when using the default IDE configuration. We have already included a VS Code configuration setting to remediate this in the project. However:

You will need to use sbt --client instead of sbt to start SBT on the command line. If nothing appears when you type commands into SBT, try sbt -Djline.terminal=none --client instead.

You can read more about this issue in the “Troubleshooting” section of the degrees lab.

Writing your app

By the end of this lab, you should have implemented:

That’s a lot, so it may be tempting to start coding right away. Don’t do that! Take time to reflect and plan, at the beginning of the project, on the architecture of your app, the types it will use, and the role of each team member.

Make sure to add package apps.<appId> (replace <appId> with the value your wrote down earlier) at the top of every file that you create!

Types

webapp-lib needs to know the types of your app’s state (stored on the server), events (data sent by clients to the server), and views (projections of the state sent to clients in response to events). We recommend defining these types in shared/src/main/scala/apps/<appId>/types.scala.

App logic (Backend)

Your app’s logic includes an initial state, a transition function (which computes a new state from the current state of the application and an event the received by the server), and a projection function (which computes a client-specific view from the state of the application and the client’s identifier).

The first two components (initial state and transition function) put together are all you need to implement your application’s state machine, by extending one of the two abstract classes provided by webapp-lib:

  1. Default: StateMachine: This the default implementation of an application’s state machine. The transition function is triggered each time the server receives an event from one of the clients.

  2. Variant: ClockDrivenStateMachine: This is a variant implementation of the default StateMachine, augmented with a clock. The clock acts as a mock client that emits a Tick(systemMillis: Long) event with a specified fixed frequency.

    If you choose this variant, you will need to specify:

    • The period of the clock through the clockPeriodMs attribute of the state machine object.
    • How to handle Tick events in the transition function of the state machine object.

We recommend defining the state machine in apps/jvm/src/main/scala/apps/<appId>/Logic.scala. In that file, create a class that extends the abstract class that you chose (StateMachine or ClockDrivenStateMachine). Doing so will require you to implement the following class members:

class Logic extends YourChoiceOfStateMachine[Event, State, View]:

  // ClockDrivenStateMachine specific:
  override val clockPeriodMs: Int = 100 // The period of your clock
  //---

  //
  // or override val clockDrivenWire: AppWire[Event, View] = Wire

  override val appInfo: AppInfo = AppInfo(
    id = "<appId>",
    name = "The name of your app (max 32 chars)",
    description = "A description of your app (Max 160 chars)",
    year = 2024
  )

  override def init(clients: Seq[UserId]): State = ???

  override def transition(state: State)(
    userId: UserId,
    event: Event
    // or event: Either[Tick, Event] (which means the type of event can be either a Tick or Event)
  ): Try[Seq[Action[State]]] = ???

  override def project(state: State)(userId: UserId): View = ???

After creating the class corresponding to your app’s logic, you might get a warning from your IDE saying that the class is unused. This is to be expected: the webapp-lib framework loads this class in a way that can’t be directly detected by linters or other static analysis tools.

App UI (Frontend)

Your app’s UI translates abstract views into concrete webpages. It does so by extending one of webapp-lib’s two UI base classes (from the cs214.webapp.client.graphics package):

Both of these have the following abstract fields:

The remaining fields depend on your choice of API (text or HTML based):

WebClientAppInstance

The WebClientAppInstance abstract class has just one abstract member:

override def render(userId: UserId, view: View): Frag = ???

render take a view and the identifier of the corresponding user, and should render the given view into a ScalaJS Frag component (this is the ScalaJS’s way of representing an HTML DocumentFragment). The render function is invoked every time the server sends a new view.

If you want to build and HTML-based UI, you can use the course’s examples for inspiration, and read the scalatags documentation for more details on creating Frag objects. For students who do not have prior experience with HTML / CSS, we recommend starting with a text-based UI.

TextClientAppInstance

The TextClientAppInstance abstract class has two abstract members:

override def renderView(userId: UserId, view: View): Vector[TextSegment] = ???

override def handleTextInput(view: View, text: String): Option[Event] = ???

The first, renderView, is similar to WebClientAppInstance’s render except that it expects to get back a Vector[TextSegment]. TextSegment can be thought of as similar to HTML’s <span> tags and represents spans of text.

For example, in your UI, you may wish to render the text “This is the word blue and this is the word red”, with each word is colored accordingly. In the renderView function, you would render the view as follows:

override def renderView(userId: UserId, view: View): Vector[TextSegment] =
  Vector(
    TextSegment("This is the word "),
    TextSegment(text = "blue", cssProperties = Map("color" -> "blue")),
    TextSegment(" and this is the word "),
    TextSegment(text = "red", cssProperties = Map("color" -> "red")),
  )

Taking a look at TextSegment’s documentation, you will see that other properties can be specified for a given segment, including onMouseEvent which allows you to respond to mouse events triggered on a segment.

The second function, handleTextInput, gives you the current view of your app, with the text the user has typed in the UI’s input field upon pressing the Enter key of their keyboard.

It should return None if no events should be triggered, or Some(Event) if an event should be sent to the server.

Implementing the frontend

To implement your application’s UI, create the file apps/js/src/main/scala/apps/<appId>/UI.scala, making sure to use the right appId.

In this file, create an object that extends cs214.webapp.client.WSClientApp, then implement and annotate this object as follows:

@JSExportTopLevel("<appId>") // <-- /!\ Important
object UI extends WSClientApp:

  def appId: String = "<appId>"

  def init(userId: UserId, sendMessage: ujson.Value => Unit, target: dom.Element): ClientAppInstance =
    Instance(userId, sendMessage, target)

class Instance(userId: UserId, sendMessage: ujson.Value => Unit, target: dom.Element)
  extends YourChoiceOfAppInstance[Event, View](userId, sendMessage, target):

  override val wire: AppWire[Event, View] = Wire

  // Your implementation...

After creating the class corresponding to your app’s UI, you might also get a warning from your IDE saying that the class is unused, same as the app’s Logic. This time, your app’s UI registration in the framework is done through the use of the @JSExportTopLevel annotation of the ScalaJS library.

Running your app

  1. Make sure you have read the note above about build configuration.

  2. In the top-level directory (not the subproject directories!), run sbt --client.

  3. At the SBT prompt, use run to start the server on port 8080.

  4. In your browser, go to localhost:8080. This will start a client in that browser tab.

  5. You will be asked to choose an app; pick your app and enter one or more user IDs (e.g. me;myFriend) in the User IDs field.

  6. Click Start!, then select one of these user IDs for yourself on the next screen.

  7. To use your app with a friend, send them the URL that is displayed on the final screen. You can also open this URL in a separate browser or tab to play with yourself.

Your friends need to be connected to the same local network as you (WiFi or LAN).

You can also run any automated tests that you wrote with test at the SBT prompt.

Every time you make any changes, to restart the webapp:

  1. Close all browser tabs where you have started the client.

  2. Stop the server by pressing Ctrl+C twice.

  3. Redo the steps above to restart the server and client.

Wrapping things up

Once you’re done with programming, take the time to add a screenshot and update your README.

Once that’s done, have one team member submit your code as a Git bundle on Moodle as explained in the unguided lab policies.