📜 ⬆️ ⬇️

Creating Web Applications with Scala.js and React - Part 1

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 .


Customization


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> 

Building React Components


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 .


REST API call


We will use three methods of the Spotify public API :


MethodPoint of entryPurposeReturn value
Get/ v1 / search? type = artistFind an artistartists
Get/ v1 / artists / {id} / albumsGet artist albumsalbums *
Get/ v1 / albums / {id} / tracksGet songs from the albumtracks *

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 .


HTML drawing


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:



You can refer to the scalajs-react documentation for more details on how to create a virtual DOM.


Reaction to events


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:



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)) } } } 

Conclusion


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