10
votes

I've been plowing through play! so far with a few bumps in the learning curve. Right now I am unable to pass flash data from the controller to the view, which at first I thought was a trivial task, or at least it should be.

Here's what I have right now:

I have a main layout: application.scala.html

I have a view that goes in the layout: login.scala.html

and I have my controller and method: UX.authenticate() - I want this to provide flash data to the view depending on the outcome of the login attempt (successful vs fail)

This is my code in my controller method:

def authenticate = Action { implicit request =>
        val (email, password) = User.login.bindFromRequest.get
        // Validation
        // -- Make sure nothing is empty
        if(email.isEmpty || password.isEmpty) {
            flash + ("message" -> "Fields cannot be empty") + ("state" -> "error")
            Redirect(routes.UX.login())
        }
        // -- Make sure email address entered is a service email
        val domain = email.split("@")
        if(domain(1) != "example.com" || !"""(\w+)@([\w\.]+)""".r.unapplySeq(email).isDefined) {
            flash + ("message" -> "You are not permitted to access this service") + ("state" -> "error")
            Redirect(routes.UX.login())
        } else {
            // Attempt login
            if(AuthHelper.login(email, password)) {
                // Login successful
                val user = User.findByEmail(email)
                flash + ("message" -> "Login successful") + ("state" -> "success")
                Redirect(routes.UX.manager()).withSession(
                  session + (
                    "user"      -> user.id.toString
                  )
                )
            } else {
                // Bad login
                flash + ("message" -> "Login failed") + ("state" -> "error")
                Redirect(routes.UX.login())
            }
        }
    }

In my login view I have a parameter: @(implicit flash: Flash)

When I try to use flash nothing appears using @flash.get("message")

Ideally I would want to set @(implicit flash: Flash) in the layout, so that I can flash data from any controller and it will reach my view. But whenever I do that, login view throws errors.

In my login view right now I have this:

def login = Action { implicit request =>
        flash + ("message" -> "test")
        Ok(views.html.ux.login(flash))
    }

What is the ideal way of passing flash data to the view, and are there examples anywhere? The examples on the Play! framework docs do not help whatsoever and are limited to two examples that show no interaction with the view at all (found here at the bottom: http://www.playframework.com/documentation/2.0/ScalaSessionFlash).

Is there an easier alternative? What am i doing wrong? How can I pass flash data directly to my layout view?

2

2 Answers

8
votes

If you look in the documentation for Session and Flash scopes you'll see this code snippet:

def save = Action {
  Redirect("/home").flashing(
    "success" -> "The item has been created"
  )
}

Now, compare that to your use of the flash scope:

flash + ("message" -> "Login successful") + ("state" -> "success")

The issue with this usage is that flash is immutable, you can't reassign it. Moreover, with your usage here you're actually creating a new flash variable, it just isn't being used.

If you had modified that slightly to become:

implicit val newFlash = flash + ("message" -> "Login successful") + ("state" -> "success")
Redirect(...)

It would've worked. However, the preferred usage is to use the .flashing() method on your result. This method comes from play.api.mvc.WithHeaders, a trait that is mixed in to play.api.mvc.PlainResult which the various result methods (Ok, Redirect, etc.) inherit from.

Then, as shown in the documentation, you can access the flash scope in your template:

@()(implicit flash: Flash) ... 
@flash.get("success").getOrElse("Welcome!") ...

edit: Ah, okay. I've reviewed your sample code and now I see what you're trying to do. I think what you're really looking for is the canonical way of handling form submissions. Review the constraint definitions here in the documentation and I think you'll see there's a better way to accomplish this. Essentially you'll want to use the verifying method on the tuple backing your form so that bindFromRequest will fail to bind and the validation errors can be passed back to the view:

loginForm.bindFromRequest.fold(
  formWithErrors => // binding failure, you retrieve the form containing errors,
    BadRequest(views.html.login(formWithErrors)),
  value => // binding success, you get the actual value 
    Redirect(routes.HomeController.home).flashing("message" -> "Welcome!" + value.firstName)
)
5
votes

Wanted to add one more thing to this discussion, to help people avoid this error:

could not find implicit value for parameter flash: play.api.mvc.Flash

I know a lot of this is redundant, but there's a technicality at the end that stole half of my workday and I feel I can help people out with. Appending .flashing(/* your flash scoped info */), such as:

Redirect(routes.ImageEditApp.renderFormAction(assetId)).
  flashing("error" -> "New filename must be unused and cannot be empty.")

... does defines the implicit "flash" variable that you can use in your template, if you have a "base" template that you want to handle flash scope with (such as errors), single usage of the base template is easy, since you already have the implicit flash variable defined via .flashing() ... This is an example of one of my "includes" of a base template.

@views.html.base("Editing: "+asset.id, scripts = Some(scripts),
  extraNav = Some(nav))

You do not have to pass the "flash" variable to the base template. It's an implicit. The base template still has to define it. The parameters for my base template is this:

@(title: String, stylesheets: Option[Html] = None,
  scripts: Option[Html] = None,
  extraNav: Option[Html] = None)(content: Html)(implicit flash: Flash)

Yeah, I know a lot of that is unnecessary, but this is a real world example I'm copy n' pasting from. Anyway, it's likely you need to have other templates that use your base template, and you do not always use .flashing() to load them. Since you're surely loading these using Controllers, if you forget to start your Action for each with implicit request => such as:

def controllerMethodName() = Action { implicit request =>

then the "flash" implicit will not be defined. Then when that template tries to include your base template, you'll be flummoxed because you don't have the default flash implicit variable defined, while the base template requires it. Hence that error.

So again, the fix.. go to all of your controller methods, and make sure you put in that implicit request => !