The normal use case for Tyrian is to build a Single Page App/Application (SPA). You can also use Tyrian for Server-side Rendering (SSR), but this page will focus on SPAs.
A guided example
Let's walk through an example to see what goes into a Tyrian App.
The 'counter' is a very common example you'll come across for many frameworks, it's a handy Rosetta stone for when you need to orientate yourself in a new framework quickly.
The example is comprised of two buttons,
-, and some text that shows a count that goes up and dow when you press the buttons.
The version of this in the examples is already quite lean, but the version below has been stripped back to the minimum.
import tyrian.Html.* import tyrian.* import cats.effect.IO import scala.scalajs.js.annotation.* // @JSExportTopLevel("TyrianApp") // Commented out to appease mdoc.. object Main extends TyrianApp[Msg, Model]: def router: Location => Msg = Routing.none(Msg.NoOp) def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) = (0, Cmd.None) def update(model: Model): Msg => (Model, Cmd[IO, Msg]) = case Msg.Increment => (model + 1, Cmd.None) case Msg.Decrement => (model - 1, Cmd.None) case Msg.NoOp => (model, Cmd.None) def view(model: Model): Html[Msg] = div()( button(onClick(Msg.Decrement))("-"), div()(model.toString), button(onClick(Msg.Increment))("+") ) def subscriptions(model: Model): Sub[IO, Msg] = Sub.None type Model = Int enum Msg: case Increment, Decrement, NoOp
<div id="mdoc-html-run0" data-mdoc-js></div>
Lets go through it...
Starting at the top, we have the most common imports that bring in all the basics you'll need to build your SPA.
All Tyrian SPAs must extend
TyrianApp which is parameterized by a message type and a model type. These types can be anything you like, but typically
Msg is an enum or ADT, and
Model is probably a case class (in our case we're just using an
Int, but we'll come back to that).
TyrianApp[Msg, Model] will produce helpful compile errors that will tell you all the functions you need to implement, i.e.
The other thing you must do is export the app using Scala.js's
@JSExportTopLevel("TyrianApp"). You can call it anything you like, but all the examples expect the name "TyrianApp".
type Model = Int
<div id="mdoc-html-run1" data-mdoc-js></div>
Our app is a counter, so we need a number we can increment and decrement. In this super simple example, an
Int is all that we need for our whole model. Normally you'd probably have a
case class or something instead. To make it fit nicely, we've allocated our
Int to a
Model type alias.
The version in the examples uses an opaque type, but here we've reduced it to a type alias.
To use our model, we're going to have to initialize it!
import tyrian.* import tyrian.Html.* import cats.effect.IO type Model = Int
def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) = (0, Cmd.None)
<div id="mdoc-html-run3" data-mdoc-js></div>
There's a few things going on here, the only bit we really care about here is the
0 because that is going to be the starting value of our 'model'.
Some of the other things you can see here:
flags- Flags can be passed into the app at launch time, think of them like command line arguments.
Cmd[IO, Msg]- Commands aren't used in the example, but they allow you to capture and run side effects and emit resulting events. They are a requirement for the function signature, and here we satisfy that with
Rendering the page
Let's draw the page. All the functions in Tyrian are encouraged to be pure, which means they operate solely on their arguments to produce a value.
view takes the latest immutable (read-only) model, and produces some HTML in the form of
def view(model: Model): Html[Msg] = div( button("-"), div(model.toString), button("+") )
<div id="mdoc-html-run4" data-mdoc-js></div>
Here we make a div, add a
- button, the another div containing the count (i.e. the model) as plain text, and finally another
+ button. If you're familiar with HTML this should all look pretty familiar.
If you wanted to add an
id attribute to the div, you would do so like this:
def view(model: Model): Html[Msg] = div(id := "my container")( button("-"), div(model.toString), button("+") )
<div id="mdoc-html-run5" data-mdoc-js></div>
Of course a button isn't much use unless it does something, and what we can do is emit an event, called a message, when the button is clicked. For that we need to declare our message type which we'll do as a simple enum that represents the two actions we want to perform:
enum Msg: case Increment, Decrement
...and add our click events:
def view(model: Model): Html[Msg] = div( button(onClick(Msg.Decrement))("-"), div(model.toString), button(onClick(Msg.Increment))("+") )
<div id="mdoc-html-run7" data-mdoc-js></div>
Note the return type of view is
onClickis not directly instigating a normal callback, the HTML elements are mapped through and produce messages as values that are passed back to Tyrian.
Updating the counter's value
The final thing we need to do is react to the messages the view is sending, as follows:
def update(model: Model): Msg => (Model, Cmd[IO, Msg]) = case Msg.Increment => (model + 1, Cmd.None) case Msg.Decrement => (model - 1, Cmd.None)
<div id="mdoc-html-run8" data-mdoc-js></div>
Recall that our 'model' is just a type alias for an
Int, so all we do is match on the
Msg enum type, and either increment or decrement the model - done!