FastScala | About

What is the FastScala framework?

The FastScala web framework allows you to do fast web application development in Scala.

You can very easily create callbacks that you send to the client side, which will be executed in the server and will return back the next action to be executed on the server (rerender part of the page, run some javascript, redirect, alert, anything).

Create a callback

val callbackJs = fsc.callback(() => JS.alert(s"Current date/time on server: ${new Date().toString}"))
<p>Clicking the button runs the javacript:</p>
<pre>{callbackJs.cmd}</pre>
<button class="btn btn-primary d-block mx-auto" onclick={callbackJs.cmd}>Check time on server</button>

Clicking the button runs the javacript:

window._fs.callback('',"1","2",false,true,true);

Builing on top of this basic callback funcionality, we can create a great development experience which allows you to build web applications much faster.

See bellow the available library to build Bootstrap buttons easily:

Building on top of the basics

BSBtn().BtnPrimary.lg.lbl("Check time on server")
  .ajax(_ => JS.alert(s"Current date/time on server: ${new Date().toString}"))
  .btn.m_3.shadow.mx_auto.d_block

Your imagination is the limit - easily control the client side from the client side:

Check time on server basic example

<div id="current-time"><span>{new Date().toString}</span></div> ++
  BSBtn().BtnPrimary.sm.lbl("Update time")
    .ajax(_ => JS.setContents("current-time", <span>{new Date().toString}</span>)).sm.btn
Tue Oct 15 23:25:16 WEST 2024

Building on these fundations we introduce more advanced components which make you go even faster and safer:

Check time on server example 2

val rerenderable = JS.rerenderable(_ => _ => <span>{new Date().toString}</span>)
rerenderable.render() ++
  BSBtn().BtnPrimary.sm.lbl("Update time")
    .ajax(_ => rerenderable.rerender()).sm.btn.ms_2
Tue Oct 15 23:25:16 WEST 2024

Check time on server example 3

JS.rerenderableContents(rerenderer => implicit fsc => {
  <span>{new Date().toString}</span> ++
    BSBtn().BtnPrimary.sm.lbl("Update time")
      .ajax(_ => rerenderer.rerender()).sm.btn.ms_2
}).render()
Tue Oct 15 23:25:16 WEST 2024

Lets create an even more complex scenario:

Variable number of input buttons

var numBtns = 3
JS.rerenderableContents(rerenderer => implicit fsc => {
  val buttons = (0 until numBtns).map(btnNum => {
    val points = btnNum + 1
    BSBtn().BtnSecondary.lg.lbl(points.toString)
      .onclick(JS.alert(points + " points")).btn.mx_3
  })
  d_flex.justify_content_center.apply(buttons: _*).mb_2 ++
    d_flex.justify_content_center.apply {
      BSBtn().BtnPrimary.sm.lbl("-").ajax(_ => {
        numBtns -= 1
        rerenderer.rerender()
      }).btn ++
        BSBtn().BtnPrimary.sm.lbl("+").ajax(_ => {
          numBtns += 1
          rerenderer.rerender()
        }).btn.ms_2
    }
}).render()

Variable number of input buttons - more elegant/functional approach, with JS.rerenderableContentsP

JS.rerenderableContentsP[Int](rerenderer => implicit fsc => numBtns => {
  val buttons = (0 until numBtns).map(_ + 1).map(points => {
    BSBtn().BtnSecondary.lg.lbl(points.toString)
      .onclick(JS.alert(points + " points")).btn.mx_3
  })
  d_flex.justify_content_center.apply(buttons: _*).mb_2 ++
    d_flex.justify_content_center.apply {
      BSBtn().BtnPrimary.sm.lbl("-").ajax(_ => {
        rerenderer.rerender(math.max(1, numBtns - 1))
      }).btn ++
        BSBtn().BtnPrimary.sm.lbl("+").ajax(_ => {
          rerenderer.rerender(math.min(10, numBtns + 1))
        }).btn.ms_2
    }
}).render(3)

We continously build on top of abstractions to create even more complex experiences, that are really simple and low-code to develop:

Easily create advanced forms

import DefaultFSDemoBSForm7Renderers._
val nameField = new F7StringField().label("Name").required(true)
val emailField = new F7StringField().label("Email").inputType("email").required(true)

new DefaultForm7() {
  override def postSubmitForm()(implicit fsc: FSContext): Js =
    BSModal5.verySimple("Your input data", "Done")(modal => implicit fsc => {
      fs_4.apply(s"Your entered the name '${nameField.currentValue}' and email '${emailField.currentValue}'")
    })

  override lazy val rootField: F7Field = F7VerticalField()(
    nameField
    , emailField
    , new F7SaveButtonField(implicit fsc => BSBtn().BtnPrimary.lbl("Submit").btn.d_block)
  )
}.render()

Support advanced interactions with a few lines of code

import DefaultFSDemoBSForm7Renderers._
case class Definition(definition: Option[String], example: Option[String], synonyms: List[String], antonyms: List[String]) {
  def render(): NodeSeq = definition.map(definition => <li><i>{definition}</i>{example.map(": " + _).getOrElse("")}</li>).getOrElse(NodeSeq.Empty)
}
case class Meaning(partOfSpeech: String, definitions: List[Definition]) {
  def render(): NodeSeq = <li><i>{partOfSpeech}</i></li> ++
    <li>Definitions: <ul class="ms-2">{definitions.flatMap(_.render())}</ul></li>.showIf(definitions.nonEmpty)
}
case class Response(word: String, phonetic: String, origin: Option[String], meanings: List[Meaning]) {
  def render(): NodeSeq = <h6>{word}</h6> ++
    <ul class="ms-2">
      <li>Phonetic: {phonetic}</li>{origin.map(origin => <li>Origin: {origin}</li>).getOrElse(NodeSeq.Empty)}
      {<li>Meanings: <ul class="ms-2">{meanings.flatMap(_.render())}</ul></li>.showIf(meanings.nonEmpty)}
    </ul>.ms_2
}
implicit val definitionDecoder: Decoder[Definition] = semiauto.deriveDecoder[Definition]
implicit val meaningDecoder: Decoder[Meaning] = semiauto.deriveDecoder[Meaning]
implicit val responseDecoder: Decoder[Response] = semiauto.deriveDecoder[Response]

def renderResponses(responses:  List[Response]) = responses.flatMap(_.render())

val resultsRenderer = JS.rerenderableContentsP[Option[String]](_ => implicit fsc => queryOpt => {
  queryOpt match {
    case Some(query) =>
      val url = new URL(s"https://api.dictionaryapi.dev/api/v2/entries/en/${URLEncoder.encode(query)}")
      val con = url.openConnection().asInstanceOf[HttpURLConnection]
      con.setRequestMethod("GET")
      try {
        Try(Source.fromInputStream(con.getInputStream).mkString).toEither.flatMap(json => {
          io.circe.parser.decode[List[Response]](json)
        }) match {
          case Right(responses) => renderResponses(responses)
          case Left(_: java.io.FileNotFoundException) => span.text_danger.apply("No results found.")
          case Left(value) => span.apply("Error when calling the API: " + value)
        }
      } finally {
        Try(con.getInputStream.close())
      }
    case None => div.apply("...").text_center
  }
})

val queryField = new F7StringField().label("Search query").required(true)

resultsRenderer.render(None) ++
  new DefaultForm7() {
    override def postSubmitForm()(implicit fsc: FSContext): Js = resultsRenderer.rerender(Some(queryField.currentValue))

    override lazy val rootField: F7Field = F7VerticalField()(
      queryField
      , new F7SaveButtonField(implicit fsc => BSBtn().BtnPrimary.lbl("Submit").btn.d_block)
    )
  }.render()
...