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
Modern Fullstack in Scala
By espinhogr
Modern Fullstack in Scala
How to use React, Apollo Client, GraphQL, Sangria and Play in a project using only Scala as a language.
- 421