Translation of the article Pedro Palma Ramos " Building Web applications with Scala.js and React - Part 1 "
As a Scala programmer developing web applications, I usually find it unpleasant to switch from a neat, functional, and type-safe Scala backend to a front end written in JavaScript. Fortunately, there is a powerful and mature alternative to our (not always) favorite standard language for the Web.
Scala.js is the Scala implementation by Sébastien Doeraene , which compiles the Scala code in JavaScript, not the JVM bytecode. It supports full two-way interoperability between Scala and JavaScript code and, therefore, allows you to develop Scala front-end web applications using JavaScript libraries and frameworks. It also helps reduce code duplication compared to a regular Scala web application, because it allows you to reuse models and business logic developed for the server side on the front-end.
React , on the other hand, is a web-framework for creating user interfaces in JavaScript, developed and maintained by Facebook and other companies. It promotes a clear separation between updating the state of an application in response to user events and visualizing views based on a specified state. Therefore, the React framework is particularly suitable for the functional paradigm that is used in Scala programming.
We could use React directly from Scala.js, but fortunately, David Barri created the scalajs-react : Scala library, which provides a set of wrappers for React to make it safe and more convenient to use in Scala.js. It also defines some useful abstractions, such as the Callback class: a composite, repeatable, side-by-side calculation that should be performed by the React framework.
This article is the first part of a tutorial describing how we create a front-end web application using scalajs-react on the e.near website. It focuses on creating a clean project on Scala.js, and the second part will combine Scala.js and the “standard” Scala code for the JVM. I assume that you are an experienced Scala user and at least familiar with HTML and the basics of Bootstrap . Previous experience with JavaScript or the React framework is not required.
The end result will be a simple web application using the open API Spotify to search for artists and display their albums and tracks (which you can watch here ). Despite its simplicity, this example should give you an idea of ​​how to develop web applications in Scala.js React, including the response to user input, the REST API call through Ajax, and the display update.
The code, fragments of which are used in this article, is fully accessible at https://github.com/enear/scalajs-react-guide-part1 .
A quick way to get started with the Scala.js project is to clone an application template written by Sébastien Doeraene using GIT.
You will need to add the link to scalajs-react
to the build.sbt file:
libraryDependencies ++= Seq( "com.github.japgolly.scalajs-react" %%% "core" % "0.11.3" ) jsDependencies ++= Seq( "org.webjars.bower" % "react" % "15.3.2" / "react-with-addons.js" minified "react-with-addons.min.js" commonJSName "React", "org.webjars.bower" % "react" % "15.3.2" / "react-dom.js" minified "react-dom.min.js" dependsOn "react-with-addons.js" commonJSName "ReactDOM", "org.webjars.bower" % "react" % "15.3.2" / "react-dom-server.js" minified "react-dom-server.min.js" dependsOn "react-dom.js" commonJSName "ReactDOMServer" )
The Scala.js plugin for SBT adds the jsDependencies
parameter. It allows SBT to manage JavaScript dependencies using WebJars. <project-name>-jsdeps.js
they are compiled into the file <project-name>-jsdeps.js
.
To compile the code, we can use the fastOptJS
command (moderate optimization for development) or fullOptJS
(full optimization for production) inside SBT. The artifacts <project-name>-fastopt/fullopt.js
and <project-name>-launcher.js
. The first one contains our compiled code, and the second one is a script that simply calls our main method.
We will also need an HTML file with an empty <div>
tag where React will insert the rendered content.
<!DOCTYPE html> <html> <head> <title>Example Scala.js application</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> </head> <body> <div class="app-container" id="playground"> </div> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> <script type="text/javascript" src="./target/scala-2.12/scala-js-react-guide-jsdeps.js"></script> <script type="text/javascript" src="./target/scala-2.12/scala-js-react-guide-fastopt.js"></script> <script type="text/javascript" src="./target/scala-2.12/scala-js-react-guide-launcher.js"></script> </body> </html>
The entry point for Scala.js is determined by the object that inherits the JSApp JSApp
. This ensures that the object and its main method will be exported to JavaScript with their full names.
object App extends JSApp { @JSExport override def main(): Unit = { ReactDOM.render(TrackListingApp.component(), dom.document.getElementById("playground")) } }
scalajs-react
provides the Router class to manage multiple React components on a one-page application, but this is beyond the scope of this tutorial, since our application consists of only one React component, which we will display inside the tag with the "playground"
identifier.
object TrackListingApp { val component = ReactComponentB[Unit]("Spotify Track Listing") .initialState(TrackListingState.empty) .renderBackend[TrackListingOps] .build
All React components must define the render
method, which returns HTML as a function of its arguments and / or states. Our component does not require arguments, so the parameter of type Unit
, but it requires an object with the state of type TrackListingState
. We delegate the drawing of this component to the TrackListingOps
class, where we can also describe the methods that control the state of the component.
The state of our application will be stored as follows:
case class TrackListingState( artistInput: String, // albums: Seq[Album], // tracks: Seq[Track] // ) object TrackListingState { val empty = TrackListingState("", Nil, Nil) }
The Album
and Track
classes are defined in the next section.
You can look at other ways to create React components here .
We will use three methods of the Spotify public API :
Method | Point of entry | Purpose | Return value |
---|---|---|---|
Get | / v1 / search? type = artist | Find an artist | artists |
Get | / v1 / artists / {id} / albums | Get artist albums | albums * |
Get | / v1 / albums / {id} / tracks | Get songs from the album | tracks * |
This API returns objects in JSON format, and they can be parsed using JavaScript. We can use this in Scala.js by defining the types of facades that will become the interface between Scala models and JavaScript. To do this, we mark the traits using @js.native
and inherit them from js.Object
.
@js.native trait SearchResults extends js.Object { def artists: ItemListing[Artist] } @js.native trait ItemListing[T] extends js.Object { def items: js.Array[T] } @js.native trait Artist extends js.Object { def id: String def name: String } @js.native trait Album extends js.Object { def id: String def name: String } @js.native trait Track extends js.Object { def id: String def name: String def track_number: Int def duration_ms: Int def preview_url: String }
Finally, we can asynchronously call the Spotify API using the Ajax Scala.js object (which for convenience returns Future, thus ensuring that you are not confused in all these callbacks ).
object SpotifyAPI { def fetchArtist(name: String): Future[Option[Artist]] = { Ajax.get(artistSearchURL(name)) map { xhr => val searchResults = JSON.parse(xhr.responseText).asInstanceOf[SearchResults] searchResults.artists.items.headOption } } def fetchAlbums(artistId: String): Future[Seq[Album]] = { Ajax.get(albumsURL(artistId)) map { xhr => val albumListing = JSON.parse(xhr.responseText).asInstanceOf[ItemListing[Album]] albumListing.items } } def fetchTracks(albumId: String): Future[Seq[Track]] = { Ajax.get(tracksURL(albumId)) map { xhr => val trackListing = JSON.parse(xhr.responseText).asInstanceOf[ItemListing[Track]] trackListing.items } } def artistSearchURL(name: String) = s"https://api.spotify.com/v1/search?type=artist&q=${URIUtils.encodeURIComponent(name)}" def albumsURL(artistId: String) = s"https://api.spotify.com/v1/artists/$artistId/albums?limit=50&market=PT&album_type=album" def tracksURL(albumId: String) = s"https://api.spotify.com/v1/albums/$albumId/tracks?limit=50" }
To learn more ways to interact with JavaScript code, you can refer to the Scala.js documentation .
Now we define the render
method of the TrackListingOps
class as a function of the state:
class TrackListingOps($: BackendScope[Unit, TrackListingState]) { def render(s: TrackListingState) = { <.div(^.cls := "container", <.h1("Spotify Track Listing"), <.div(^.cls := "form-group", <.label(^.`for` := "artist", "Artist"), <.div(^.cls := "row", ^.id := "artist", <.div(^.cls := "col-xs-10", <.input(^.`type` := "text", ^.cls := "form-control", ^.value := s.artistInput, ^.onChange ==> updateArtistInput ) ), <.div(^.cls := "col-xs-2", <.button(^.`type` := "button", ^.cls := "btn btn-primary custom-button-width", ^.onClick --> searchForArtist(s.artistInput), ^.disabled := s.artistInput.isEmpty, "Search" ) ) ) ), <.div(^.cls := "form-group", <.label(^.`for` := "album", "Album"), <.select(^.cls := "form-control", ^.id := "album", ^.onChange ==> updateTracks, s.albums.map { album => <.option(^.value := album.id, album.name) } ) ), <.hr, <.ul(s.tracks map { track => <.li( <.div( <.p(s"${track.track_number}. ${track.name} (${formatDuration(track.duration_ms)})"), <.audio(^.controls := true, ^.key := track.preview_url, <.source(^.src := track.preview_url) ) ) ) }) ) }
The code may seem complicated, especially if you are not familiar with Bootstrap, but keep in mind that this is nothing more than typed HTML. Tags and attributes are written as methods of objects <
and ^
respectively (you first need to import japgolly.scalajs.react.vdom.prefix_<^._
).
Strange arrows ( -->
and ==>
) are used to bind event handlers that are defined as Callback callbacks:
-->
takes a simple Callback
argument,==>
accepts a function (ReactEvent => Callback)
, which is useful when you need to handle a value that was captured from a triggered event.You can refer to the scalajs-react documentation for more details on how to create a virtual DOM.
It remains only to define event handlers.
Let's take another look at the definition of the class TrackListingOps
:
class TrackListingOps($: BackendScope[Unit, TrackListingState]) {
The $ constructor argument provides an interface for updating the state of an application using the setState
and modState
. We can define lenses for all state fields for a shorter record of their updates.
val artistInputState = $.zoom(_.artistInput)((s, x) => s.copy(artistInput = x)) val albumsState = $.zoom(_.albums)((s, x) => s.copy(albums = x)) val tracksState = $.zoom(_.tracks)((s, x) => s.copy(tracks = x))
As you remember, we use three event handlers:
updateArtistInput
, when the artist's name changes,updateTracks
when a new album is selected,searchForArtist
when the search button is pressed.Let's start with updateArtistInput
:
def updateArtistInput(event: ReactEventI): Callback = { artistInputState.setState(event.target.value) }
The setState
and modState
do not perform the update immediately, but return the corresponding callback callback, so they are suitable here.
For the updateTracks method, we need to use an asynchronous callback, since we have to load the list of songs in the album. Fortunately, we can convert Future[Callback]
to asynchronous Callback
using the Callback.future
method:
def updateTracks(event: ReactEventI) = Callback.future { val albumId = event.target.asInstanceOf[HTMLSelectElement].value SpotifyAPI.fetchTracks(albumId) map { tracks => tracksState.setState(tracks) } }
Finally, let's define the searchForArtist
method, which uses all three API methods and completely updates the state:
def searchForArtist(name: String) = Callback.future { for { artistOpt <- SpotifyAPI.fetchArtist(name) albums <- artistOpt map (artist => SpotifyAPI.fetchAlbums(artist.id)) getOrElse Future.successful(Nil) tracks <- albums.headOption map (album => SpotifyAPI.fetchTracks(album.id)) getOrElse Future.successful(Nil) } yield { artistOpt match { case None => Callback(window.alert("No artist found")) case Some(artist) => $.setState(TrackListingState(artist.name, albums, tracks)) } } }
Once you have reached this point, you should now be able to simulate the front-end web application using purely functional constructs in Scala.js. If you are interested, be sure to read the documentation on Scala.js and on scalajs-react .
Expect the second part of the tutorial, which will be devoted to creating a full-fledged web application on Scala and how you can reuse the data model and general business logic in the backend and frontend.
Source: https://habr.com/ru/post/324260/
All Articles