Modern Fullstack in Scala

Cristian Arcaroli

Reasons

Static typed language

Higher productivity

DEMO

Frameworks

  • Slinky (React for Scala.js)
  • Antd
  • Apollo Client
  • Sangria
  • Play

Project Structure

schema

shared

server

web_client

SBT Build

compile
web_client

generate Apollo

queries

generate SDL

compile server

webpack
bundle

What can we do with this?

Component creation

@react class Products extends StatelessComponent {
  type Props = Unit

  def render() = {
    val wrappedProductForm = AntForm.Form.create()(wrapperToClass(ProductForm))

    LayoutContent(LayoutContent.Props())(style := js.Dynamic.literal(padding = "50px"))(
      ProductDisplay(),
      hr(style := js.Dynamic.literal(margin = "30px")),
      Row(
        Col(Col.Props(span = 24))(
          h1("Insert a new product"),
          React.createElement(wrappedProductForm, js.Dictionary())
        )
      )
    )
  }
}

External component mapping

@JSImport("antd", JSImport.Default)
@js.native
object AntInput extends js.Object {
  val Input: js.Object = js.native
}

@react object Input extends ExternalComponentWithAttributes[input.tag.type] {
  case class Props(addonAfter: UndefOr[String | ReactElement] = js.undefined,
                   addonBefore: UndefOr[String | ReactElement] = js.undefined,
                   defaultValue: UndefOr[String] = js.undefined,
                   disabled: Boolean = false,
                   ...
                   ...
                  )

  override val component = AntInput.Input
}

Sangria Schema definition

  val IdentifiableType = InterfaceType(
    fields[RequestContext, Identifiable](
      Field("id", StringType, resolve = _.value.id)))

  val PictureType =
    deriveObjectType[Unit, Picture](
      ObjectTypeDescription("The product picture"),
      DocumentField("url", "Picture CDN URL"))

  val ProductType =
    deriveObjectType[RequestContext, Product](
      Interfaces(IdentifiableType),
      AddFields(
        Field("pictures", ListType(PictureType),
          arguments = Argument("size", IntType) :: Nil,
          resolve = c => c.ctx.pictureRepo.picturesByProduct(c.value.id))
      )
    )

Generated SDL

interface Identifiable {
  id: String!
}

"The product picture"
type Picture {
  width: Int!
  height: Int!

  "Picture CDN URL"
  url: String
}

type Product implements Identifiable {
  id: String!
  name: String!
  description: String!
  pictures(size: Int!): [Picture!]!
}

Query definition in the server

  val QueryType = ObjectType("Query", fields[RequestContext, Any](
    Field("products", ListType(ProductType),
      description = Some("Returns a list of all available products."),
      resolve = _.ctx.productRepo.products)
    )
  )
type Query {
  "Returns a list of all available products."
  products: [Product!]!
}

Query with Apollo Client

query AllProducts {
  products {
    id
    name
    description
    pictures(size: 500) {
      width
      height
      url
    }
  }
}
Query(AllProductsQuery) { result =>
  if (result.loading) {
    h1("Loading!")
  } else if (result.error.isDefined) {
    h1("Error: " + result.error.get.message)
  } else {
    div(
      h1("Products display"),
      Row(Row.Props(gutter = 16, 
                    justify = "space-around", 
                    align = "middle")
      )(
        result.data.get.products.map { product =>
          Col(Col.Props(span = 6))(
            renderProductCard(product)
          )
        }: _*
      )
    )
  }
}

Usage of typed query

  private def renderProductCard(product: AllProductsQuery.Data.Product) =
    Card(Card.Props(
      cover = img(src := product.pictures
                                .headOption
                                .flatMap(_.url)
                                .getOrElse("/assets/images/default_item.jpg"))()
    ))(style := js.Dynamic.literal(maxWidth = "240px"))(
      CardMeta(
        CardMeta.Props(title = span(product.name), 
                       description = span(product.description)
        )
      )
    )

Mutation with Apollo Client

  Mutation(AddProductMutation, UpdateStrategy(refetchQueries = Seq("AllProducts"))) { 
   (addProduct, mutationStatus) =>
      Form(Form.Props(onSubmit = (e: Event) => { handleSubmit(e, addProduct) }))(
        FormItem(
          form.getFieldDecorator("productName", 
               FieldDecoratorOptions(rules = Seq(ValidationRules(
                 required = true, message = "Cannot contain numbers or be empty.", 
                 pattern = RegExp("^[^0-9]+$")))))(
                    Input(Input.Props())(placeholder := "Name",
                                         autoComplete := "off")
          )
        ),
        FormItem(
          form.getFieldDecorator("productDescription", 
               FieldDecoratorOptions(rules = Seq(ValidationRules(
                 required = true, message = "Cannot be empty."))))(
                    Input(Input.Props())(placeholder := "Description", 
                                         autoComplete := "off")
          )
        ),
        Button(Button.Props(`type` = "primary", htmlType = "submit"))("Add")
      )
  }

Next improvements

  • Improve building time
    • Some files are not cached and rebuilt every time
    • WebpackDevServer is not used
  • Setup test frameworks
  • Make Apollo Client update cache instead of refetching queries

github.com/espinhogr/scala-graphql-fullstack-seed