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:
- Designing an implementing a complete application from scratch.
- Working on a software project as a team, using collaboration and versioning tools like Git and Gitlab.
- Leveraging libraries and frameworks to develop complex applications.
- Modeling and implementing systems using state machines and views.
- Using state in a safe and encapsulated way.
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.
-
Turn-based board games: Games with geometric boards, such as memory, reversi, go, etc. work best, because they have simple UIs. Card games work great too, especially if you don’t overthink the UI (emojis work great: ♣️♠️♥️♦️🃏♠♥♦♣♤♡♢♧🂱🂲🂳🂴🂵🂶🂷🂸🂹🂺🂻🂼🂽🂾🂡🂢🂣🂤🂥🂦🂧🂨🂩🂪🂫🂬🂭🂮🃁🃂🃃🃄🃅🃆🃇🃈🃉🃊🃋🃌🃍🃎🃑🃒🃓🃔🃕🃖🃗🃘🃙🃚🃛🃜🃝🃞🂠🃏🃟).
-
Adventure games: Interactive fiction and roguelikes (e.g.) work best. Make sure that your game is meaningfully multiplayer, and don’t spend all your time on the art.
-
Productivity and planning: Task lists, shopping lists, apartment chores schedulers, restaurant bill splitters, real-time voting and quiz apps, personal calendars, party planners, trip planners, tournament planners…
-
Sharing and collaboration: Private photo albums, book clubs, collaborative music/art/dance/theater/… apps, flashcards for collaborative study, neighborhood borrow/exchange/buy/sell/give away platforms…
These are just a few examples—build something that you’re excited about!
Before you start
-
Review our usual course policies about lab assignments, plagiarism, grading, etc.
-
Read the policies specific to the unguided lab, which include details on deadlines, grading, etc. (This lab is graded via a checkoff similar to the unguided callback.)
-
Complete the
webapp-rps
lab, which gives a gentle introduction to the CS214 webapp library (or at least skim through it). -
Review the lecture notes from week 9, which include a complete example of the app development process:
memory
. You can watch the recording on MediaSpace or read and follow along the lecture notes as you build your own app.
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
-
Navigate to the following Gitlab group: https://gitlab.epfl.ch/cs214/ul2024/teams.
-
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. -
Make sure that all of your team members (and only they) have access to it.
-
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.
-
Navigate into your the directory that you cloned.
cd app*
-
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.). -
Start tracking your app with Git:
git add . git commit -m "Initial commit" git push
Writing your proposal
-
At the end of the existing README file, add the following:
## Proposal ### User stories … ### Requirements … ### Roles … ### Mock-ups ![](mockups/app.png)
-
Fill in each section of the template above by replacing the
…
. -
Create a mock-up image and save it under
mockups/app.png
. You may add more images if needed. -
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:
-
Implement and register your app’s logic (server side: state machine transitions and state projections).
-
Implement and register your app’s UI (client side: rendering views and sending events).
-
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:
-
The name of your packages: all of your files should start with
package apps.<appId>
(For example, team123
would name the packages containing their app’s logic and UIapps.app123
.) -
The name of the thumbnail file as shown in the tree below;
(For example, team 123 would name their thumbnailapp123.png
.) -
The frontend (UI) part of your application, in the
appId
field of your UI class; -
The backend (Server) part of your application, in the
appInfo
field of your state machine.
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 import
s 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:
- Types to describe your app’s state, views, and events;
- Your app’s server-side logic;
- Your app’s client-side UI;
- Your app’s (de)serialization code (“wires”);
- Tests for you app’s server-side logic (Tests for the UI are out of scope for this course).
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
:
-
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. -
Variant:
ClockDrivenStateMachine
: This is a variant implementation of the defaultStateMachine
, augmented with a clock. The clock acts as a mock client that emits aTick(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.
- The period of the clock through the
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):
-
TextClientAppInstance
: This class offers an simple API to create text-based applications with no knowledge of web technologies. It allows for limited graphical customization. We recommend starting here if you have no web design experience. -
WebClientAppInstance
: This class offers a rich API to create HTML+CSS applications, using the scalatags library. It is more flexible than the text-based API, but it might have a steeper learning curve if no member of your team has ever used Web languages before (HTML/CSS).
Both of these have the following abstract fields:
-
wire
, your application’sWire
, which allows the client to communicate with the server. Its value should be consistent with the one set in your application’s logic. -
css
, a string containing CSS code to be applied to the whole page. This field is optional and can be used to customize the rendering of the page, even when using the text-based API. For example (note howsuper.css
is kept as a prefix, to preserve the super class’ own css declarations):override def css: String = super.css + """ html { font-family: sans-serif; background: #add8e6; } """
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
-
Make sure you have read the note above about build configuration.
-
In the top-level directory (not the subproject directories!), run
sbt --client
. -
At the SBT prompt, use
run
to start the server on port 8080. -
In your browser, go to
localhost:8080
. This will start a client in that browser tab. -
You will be asked to choose an app; pick your app and enter one or more user IDs (e.g.
me;myFriend
) in theUser IDs
field. -
Click Start!, then select one of these user IDs for yourself on the next screen.
-
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:
-
Close all browser tabs where you have started the client.
-
Stop the server by pressing Ctrl+C twice.
-
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.
-
To show a thumbnail of your app on the welcome screen, take a screenshot of your app and save it under
apps/jvm/src/main/resources/www/static/<appId>.png
. -
To help users navigate your app, edit your README to add a brief (a few sentences) description explaining how to start the app and use it.
Once that’s done, have one team member submit your code as a Git bundle on Moodle as explained in the unguided lab policies.