-
Notifications
You must be signed in to change notification settings - Fork 2
Tutorial
This tutorial is designed to show how to get a web-application running with Vaadin, MongoDB and Scala. The tutorial was inspired by the Scala, Play 2 and Mongo example @ yobriefca.se. The whole example project (containing both minimal and full implementations of the application) can be found at https://github.com/ripla/vaadin-scala-mongo
- Scala, SBT and Giter8 (I recommend installing the TypeSafe stack)
- MongoDB
All of the above can be installed via separate downloads or with a package manager. For this tutorial the following software & library versions were used
- Scala 2.9.2
- SBT 0.11.3-2
- Giter8 0.4.5
- MongoDB 2.0.7
- Casbah 2.4.1
- Salat 1.9.1
- Scaladin 2.0
- Vaadin 6.8.2
This tutorial also assumes that you have MongoDB running in the background. If not, the MongoDB website has some excellent quickstart guides.
Start off by generating your application skeleton with the Giter8 template.
g8 ripla/vaadin-scala
package [com.example]: org.vaadin.scala.example.mongo
name [Vaadin Scala project]: Mongo Example
classname [VaadinScala]: MongoExample
Applied ripla/vaadin-scala.g8 in mongo-example
This will create a working application skeleton in the mongo-example sub-dir. The application already includes Vaadin and Scaladin. Try it out!
cd mongo-example
sbt
And in the SBT console:
container:start
The application should now be started and respond at http://localhost:8080
.
In the following sections we'll be editing the files in the project. If you'd like to use Eclipse, you can type
eclipse
in the SBT console to create Eclipse .project
file in the root. Then just import the project to Eclipse.
Next we're going to add the required dependencies for to help out with Mongo. Edit the build.sbt
file at the project root and add the Sonatype snapshots repo by modifying the resolvers.
resolvers ++= Seq("Vaadin add-ons repository" at "http://maven.vaadin.com/vaadin-addons",
"Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots")
Then, add the Casbah and Salat dependencies by modifying the library dependencies.
libraryDependencies ++= Seq(
"org.mongodb" %% "casbah" % "2.4.1",
"com.novus" %% "salat" % "1.9.1-SNAPSHOT"
)
Reload the project, either by restarting SBT or typing reload
in the console.
The purpose of this software is to provide a simple mechanism for registering users, and viewing those registrations in a list. The app is so simple that it can be implemented in a single application file - and we're going to do just that (in about 100 lines or so).
First we'll create our data class as a simple Scala case class. We'll use the @BeanProperty
annotation so that our data class acts like a good citizen in the Java world (e.g. it has getters&setters for fields). We also set some default values for the fields.
case class Registration(
@BeanProperty var username: String = "username" + Random.nextInt,
@BeanProperty var password: String = "",
@BeanProperty var realName: String = "Joe Tester")
One of the great things about MongoDB is that we don't need to create stores or collections in the database manually. Simply assuming that one exists creates one for you. For the database connection, we're going to leverage Casbah.
val registrations: MongoCollection = MongoConnection()("vaadin-scala-mongo-example")("registrations")
This creates a connection to a Mongo store called vaadin-scala-mongo-example and a collection registrations. Next, we'll define a method for mapping the Casbah DBObject objects to actual registration objects with Salat and vice-versa.
def mapRegistrations: List[Registration] = registrations.map(grater[Registration].asObject(_)).toList
def saveRegistration(registration: Registration): Unit = registrations += grater[Registration].asDBObject(registration)
And that's about it. The application should look something like this https://gist.github.com/3363699.
###UI components In Scaladin, the main layout is usually composed inside the main method which returns a ComponentContainer. Just start filling the UI presented in the next sections like so:
override val main: ComponentContainer = new VerticalLayout {
... // UI components here
}
Time to start. Let's create a layout containing a Table and a Button.
val tableLayout = new VerticalLayout {
val table = new Table {
sizeFull()
container = new BeanItemContainer(mapRegistrations)
visibleColumns = Seq("username", "realName")
}
val addButton = Button("Register", showForm)
components += (table, addButton)
}
def showForm(): Unit = { /*TODO*/ }
If you're accustomed to Vaadin development, the snippet should be somewhat self-explanatory. We create a VerticalLayout, a Table and a Button and add the Table and the Button to the VerticalLayout.
The Table is full size, and its data container is a BeanItemContainer that takes all the mapped Registration objects and shows them. BeanItemContainer uses Java reflection mechanisms under the hood, so this is why we needed the BeanProperty annotations in the Registration object. By default Table shows all the properties from the Container so we need to set set the visible columns and their order. showForm
is a function that is called when the Button is clicked. We'll fill it out later.
Next, we'll create the Form for adding new registrations.
lazy val form = new Form {
caption = "Registration"
writeThrough = false
footer = new HorizontalLayout {
components += Button("Save", showList)
}
}
def showList(): Unit = { /*TODO*/ }
Again, nothing out of the ordinary. Just a lazily instantiated Form with a caption and a footer with a button that calls a function. writeThrough = false
simply means that the data entered to the the form isn't automatically set to the underlying registration object, but instead needs to be explicitly committed.
If you add the Table to the main layout:
components += tableLayout
you should see the (empty) registration list and the Button in the UI. The application should look someting like this: https://gist.github.com/3372122 (there's some extra fluff to make the UI look prettier)
###Application logic
Time to fill out those methods to get the application to actually do something. Let's start with showForm
.
def showForm(): Unit = {
form.item = new BeanItem(Registration())
form.visibleItemProperties = Seq("realName", "username", "password")
replaceComponent(tableLayout, form)
}
This function was previously bound to the Button so it gets called whenever the Button is clicked. In the function we set the backing Item of the Form and we use the ever-so-handy BeanItem and an empty registration object for this. Again, because we used the BeanProperty
annotation BeanItem finds our fields automatically. And like with the Table, we need to set the order of these fields. Last, we replace the Table with the Form in the main layout.
def showList(): Unit = {
if (form.commit.isValid) { //form handles error
val bean = form.item.get.asInstanceOf[BeanItem[Registration]].bean
saveRegistration(bean)
tableLayout.table.container = new BeanItemContainer(mapRegistrations)
tableLayout.table.visibleColumns = Seq("username", "realName")
replaceComponent(form, tableLayout)
}
}
After filling the form, we try to commit the values to the underlying bean. If the validation succeeds we can proceed. Otherwise, we let the Form show the errors. After a successful commit the bean is saved and the list of registrations is updated to the Table. The application should look like this https://gist.github.com/3372871.
###Fine-tuning the fields
The last thing to is to set the field properties. By default, Form uses DefaultFieldFactory to create the fields. They're sensible defaults, but we can modify them a bit. To be more exact, we want to make the fields required, change the password to be a PasswordField and add a password confirmation field. The easiest way is to write our own FormFieldFactory:
val registrationFormFieldFactory = FormFieldFactory(ing => {
var field: Option[Field] = ing match {
case FormFieldIngredients(_, "password", _) =>
Some(new PasswordField {
caption = DefaultFieldFactory.createCaptionByPropertyId("password")
})
case FormFieldIngredients(_, "confirmation", form: Form) =>
Some(new PasswordField {
caption = "Confirm password"
validators += Validator(value => {
if (value == form.field("password").get.value) Valid
else Invalid(List("Passwords must match"))
})
})
case otherIngredient => {
DefaultFieldFactory.createField(otherIngredient)
}
}
field.foreach(_.required = true)
field.foreach(f => f.requiredError = "%s is required".format(f.caption.get))
field
})
In essence FormFieldFactory is a function of FormFieldIngredients(propertyId,item,uiContext) => Option[Field]. And that's exactly what the match-case expression in our FormFieldFactory does. It has cases for fields named password or confirmation, and uses the DefaultFieldFactory for everything else. In addition it sets every field as required with a nice error message. The foreach
is required because field
is an Option[Field] - DefaultFieldFactory might not create a field at all and we have to prepared for that.
Lets look the password confirmation field a bit more.
validators += Validator(value => {
if (value == form.field("password").get.value) Valid
else Invalid(List("Passwords must match"))
})
Validators are another function, this time value: Any -> Valid | Invalid(reason). In our confirmation field validation we use the Form instance that's included in the FormIngredients given to us and read the password value from there. (Actually, FormIngredients contains an instance of Component but in this case we know it must be a Form.)
Since the confirmation field isn't included in the actual registration bean (it wouldn't make much sense there) it has to be explicitly added to the Form. This can be done in the showForm
method:
form.addField(Option("confirmation"), form.formFieldFactory.flatMap(_.createField(FormFieldIngredients(form.item.get, "confirmation", form))))
Last but not least, we need to actually set the Form to use the FormFieldFactory we've defined. Add this line to the Form creation function:
formFieldFactory = registrationFormFieldFactory
And that's it, we're all set. The final class can be found here. However, this is a single file solution that's more of an tool for this tutorial than a good implementation example. A better example (e.g. one with more than one class) can be found in the full package.