Play framework 2 : How to pass object between routes, views, and controller? - scala

I'm trying to pass a book object from views to routes, and then send it to calculate in a controller. My code is following:
bookList.scala.html
#(books: java.lang.Iterable[Book])
#main("BookList"){
<div class="row">
#for(book <- books.iterator()){
<div class="col-sm-6 col-md-4">
<div class="thumbnail" style="height: 435px">
...
<p><a href="#routes.Application.buy(book)" class="btn btn-primary" role="button"
style="vertical-align:bottom">Order now!</a>
</div>
</div>
</div>
}
</div>
}
routes
...
GET /order controllers.Application.buy(book: models.Book)
...
However, It gave me an error : No QueryString binder found for type models.Book. Try to implement an implicit QueryStringBindable for this type.
I tried to change the routes as :
GET /order controllers.Application.buy(book)
It also returned an error :
type mismatch; found : String required: models.Book

That's not how Play routing works. The Play router parses variables from the URL or query string, and converts them to native types via the QueryBindable typeclass. You should have something more like this:
routes
GET /order/:bookid controllers.Application.buy(bookid: String)
And the action should be like:
Application.scala
def buy(bookid: String) = Action { request =>
// Look up book by id here.
Ok(views.html.thanks("you bought a book!"))
}
And the template like this:
bookList.scala.html
#for(book <- books.iterator()) {
...
<a href="#routes.Application.buy(book.id)" class="btn btn-primary"
}
Of course if your model's ID is other than String you need to modify the route's param type
Update -- alternative using form/POST
A form with a POST method is a better solution, or the user will buy another book each time they click the URL, and the id will be exposed. Check out the forms documentation. Your template would be like this:
#for(book <- books.iterator()) {
...
<form method="post">
<div>#book.name</div>
<input type="hidden" value="#book.id"/><button type="submit">buy</button>
</form>
}

You can't simple pass object (bean) as a url query parameter.
Default you can only define simple types as a parameter types. Please read carefully play's rounting documentation -> http://www.playframework.com/documentation/2.2.x/ScalaRouting especially Parameter types
But play framework has possibility to "learn" how to interpret specific url to bean.
You get info about this in error witch you posted. Responsible for this QueryStringBindable -> http://www.playframework.com/documentation/2.2.1/api/java/play/mvc/QueryStringBindable.html
In short like in documentation, when you define class like this:
class Book implements QueryStringBindable<Book> {
public String title;
public int numpages;
public Option<Pager> bind(String key, Map<String, String[]> data) {
if (data.contains(key + ".title" && data.contains(key + ".numpages") {
try {
title = data.get(key + ".title")[0];
numpages = Integer.parseInt(data.get(key + ".numpages")[0]);
return Some(this);
} catch (NumberFormatException e) {
return None();
}
} else {
return None();
}
}
public String unbind(String key) {
return key + ".title=" + title + "&" + key + ".numpages=" + numpages;
}
public String javascriptUnbind() {
return "function(k,v) {\n" +
" return encodeURIComponent(k+'.title')+'='+v.title+'&'+encodeURIComponent(k+'.numpages')+'='+v.numpages;\n" +
"}";
}
}
Then you can define route like:
GET /order controllers.Application.buy(p: Book)
You can then run in your browser e.g link:
localhost:9000/?p.title=SomeTitle&p.numpages=235
And in buy controller you will get p parameter as a Book class instance.
I did't test this code and this is in java. But you should get the idea.

You can define a custom PathBindable to automatically convert an id to a book object and pass it to buy method in controller. See PathBindable, another example
NOTE: I would define the PathBindable in same file as the Book model.

Simple example I used to parse in UUID's using PathBindable.
I'd rather not have to parse UUID's and ensure they're well formed in all controller/action code, so I use this at the router level. I used a UUIDP (P for param) to not pollute the UUID namespace.
Case Class and Parser
case class UUIDP(key: String, value: String, uuid: UUID)
object UUIDP {
implicit def pathBinder(implicit intBinder: PathBindable[String]) = new PathBindable[UUIDP] {
val uuidRegex = "([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})".r
override def bind(key: String, value: String): Either[String, UUIDP] = {
value match {
// sanity check, prevent errors, is this really a UUID?
case uuidRegex(c) => {
//error check too, binary value can be invalid
try { Right(UUIDP(key,value,UUID.fromString(value))) }
catch {
case ex : IllegalArgumentException => Left("Unparsable UUID")
}
}
case _ => Left("Invalid UUID Format")
}
}
// in case we need to remove this from the request
override def unbind(key: String, user: UUIDP): String = {
intBinder.unbind(key, user.value)
}
}
}
conf/routes
GET /items/:id #com.foo.MyController.getItem(id: UUIDP)
Controller
def getItem(sessionId: UUIDP) = Action(parse.text) { request =>
Ok(s"Fetching item [${sessionId.uuid}]")
}

Related

Changing a value in a asp-route

With the code below is there a way to check if VendorName is not null and if so then asp-route-VendorName should be empty, the reason why I am asking is sometimes VendorName can be NULL and therefore the ToUpper() will fail. Hoping I can just do a conditional asp-route-VendorName
<a class="nav-item" asp-page="./Index" asp-route-VendorName="#vendor.VendorName.ToUpper()">
<i>#Html.DisplayFor(modelItem => vendor.VendorName)</i>
</a>
You can use IOutboundParmeterTransformer for this task (https://www.learnrazorpages.com/advanced/route-parameter-transformers). Create a class like this:
public class UpperCaseParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
return value?.ToString().ToUpper();
}
}
Register it with the ServiceContainer:
builder.Services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("uppercase", typeof(UpperCaseParameterTransformer));
});
Then apply it to a route value in the route template for the page:
#page "{VendorName:uppercase}"
That way, the routing system will take care of automatically converting any route parameter value to upper case.

Playframework [Scala]: Creating forms

I'm beginner to Scala and Playframework. I have some code in Java and I have a problem to translate it into Scala. Can you suggest something how can I do it? I check documentation about ScalaForms but still I can't understand how to do it. My code is as following:
// Injecting FormFactory
public Result create(){
Form<Book> bookForm = formFactory.form(Book.class);
return ok(create.render(bookForm));
}
public Result save(){
Form<Book> bookForm = FormFactory.form(Book.class).bindFromRequest();
Book book = bookForm.get();
Book.add(book);
return redirect(routes.BooksController.index());
}
Ok so I have something like this:
def index = Action {
val books: Set[Book] = Book.allBooks
Ok(views.html.index(books))
}
def create = Action{
Ok(views.html.create())
}
def save = Action {
implicit request =>
val (id, title, price, author) = bookForm.bindFromRequest.get
Book.addBook(id = id, title = title, price = price, author = author)
Redirect(routes.BooksController.index())
}
My routes:
GET /books controllers.BooksController.index
GET /books/create controllers.BooksController.create
POST /books/create controllers.BooksController.save
And the problem is with Redirect, I have an error: "object java.lang.ProcessBuilder.Redirect is not a value"
Ok so for Scala version of dealing with form, this is what I do:
You use bindFromRequest to do, most of the times, two things: 1. If there is an error within the form return it back to the user and 2. If there is no error within the form, use the given data by the user to do some other operations (e.g., call a third party API, or internal API, etc.)
If things are fine (second sub step of the above first step), I return an HTTP response to the user.
So, I would have something like this in my controller method:
def save() = Action {
implict request =>
bookForm.bindFromRequest.fold(
formWithErrors => {
BadRequest(views.html.whateverTheViewIs(bookForm))
}, formData =>
//Do whatever you need to do
Ok("saved")//Again, here you can use any views; and pass the required parameters.
)
}
It might be the case, that you have difficulty on defining the bookForm itself, so in Scala, you first define your data model using a case class. Lets define a very simple one:
case class Book(title: String, author: String, isbn: String)
Then you define your form:
val bookForm = Form(
mapping(
"title": String -> NonEmptyText
"author" : String -> NonEmptyText
"isbn" : String -> NonEmptyText
)(Book.apply)(Book.unapply)
)
Note on Redirection: For the redirect, as you mentioned in the comment, you could use the URL used. For example ,if I have:
GET /confirmation controllers.Book.confirmation (bookId: String)
Then I want to redirect to the confirmation URL, knowing that I need to also provide the `userId', I can do the following:
val bookId = "1" //You need to get the Id, from a database or an API.
Redirect(s"/confirmation?bookId=$bookId")
Further Step [Use Future API]: The important thing here is that, for simplicity; I did not use the Future API, for asynchronously respond to the request. This should be your aim, because you want to write non-blocking response, especially if you want to call other APIs, for processing/storing the form's valid input.

Meteor Publication and Subscription order in React

I'm trying to write a React component which subscribes to data on the server by passing the collection name as a prop. (in the example the name is still hardcoded to keep things simple) Since the collection names can change dynamically, I'm calling a method to construct the publication and server collection variable. Now the theory is to construct the data publication, server and client mongo collection variable in a parent component and the subscribe to this publication in the child component. The parent component is set up like this:
export default class TilesSingle extends TrackerReact(Component) {
newDataCollection(){
Meteor.call("setupData","randomcollectionname");
if(!window["randomcollectionname"]){
window["randomcollectionname"] = new Meteor.Collection("randomcollectionname");
}
}
render(){
this.newDataCollection();
return(
<div>
<DataObject />
</div>
)
}
}
The setupData method looks like this:
Meteor.methods({
setupData(collectionname){
if(!global[collectionname]){
global[collectionname] = new Meteor.Collection(collectionname);
Meteor.publish("pub."+ collectionname,function() {
return global[collectionname].find();
});
}
}
})
And the child DataObject component looks like this:
export default class DataObject extends TrackerReact(Component) {
constructor(){
super();
this.state = {
subscription: {
tiledata: Meteor.subscribe("pub.randomcollectionname"),
}
}
}
componentWillUnmount(){
this.state.subscription.tiledata.stop();
}
findData(){
post = window['randomcollectionname'].findOne();
console.log(post)
return post;
}
render(){
obj = this.findData();
if(!obj){
obj = {};
obj["score"] = 0;
}
return(
<div>
<h4>{obj.score}</h4>
</div>
)
}
}
Notice that I'm logging post in the findData() function. When I refresh, the console logs:
undefined
undefined
> Object {..}
> Object {..}
undefined
undefined
It is almost as if the subscription works and then stops working. I'm thinking that it has to do with the order in which react renders these components, and that the method is not finished on the server by the time the client tries to render the child component. Any Ideas?

How to support multiple forms that target the same Create action on the same page?

I want to have two separate forms on a single create page and one action in the controller for each form.
In the view:
<% using (Html.BeginForm()) { %>
// Contents of the first (EditorFor(Model.Product) form.
<input type="submit" />
<% } %>
<% using (Html.BeginForm()) { %>
// Contents of the second (generic input) form.
<input type="submit" />
<% } %>
In the controller:
// Empty for GET request
public ActionResult Create() {
return View(new ProductViewModel("", new Product()));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Product product) {
return View(new ProductViewModel("", product));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(string genericInput) {
if (/* problems with the generic input */) {
ModelState.AddModelError("genericInput", "you donkey");
}
if (ModelState.IsValid) {
// Create a product from the generic input and add to database
return RedirectToAction("Details", "Products", new { id = product.ID });
}
return View(new ProductViewModel(genericInput, new Product()));
}
Results in "The current request for action 'MyMethod' on controller type 'MyController' is ambiguous between the following action methods" - error or the wrong Create action is called.
Solutions?
Combine those two POST Create actions
into one public ActionResult
Create(Product product, string
genericInput);
Name one of the POST Create actions differently and add the new name to the corresponding Html.BeginForm()
I have no idea what are the caveats in these. How would you solve this?
You cannot have two actions with the same name and verb that differ only with argument types. IMHO naming your two actions differently would be a good idea assuming that they perform different tasks and take different inputs.
Actually, I believe you can do this if you are more specific with your BeginForm() call.
Using(Html.BeginForm<ControllerName>(c => c.Create((Product)null)) { }
Using(Html.BeginForm<ControllerName>(c => c.Create((string)null)) { }

Grails: JSONP callback without id and class in JSON file

I am working on a REST based interface where people get a json file. The client needs to access the file from another Domain. I use jsonp which works so far. My problem is the rendering in Grails. At the moment I use the 'as JSON' to marshalling the object:
render "${params.jsoncallback}(${user as JSON})"
The Json file getting to the client inclused all attributes, incluing the id and class, which I do not want to have in there. In case it is not jsonp, I do it this way, which works great:
render(contentType:'text/json'){
userName user.userName
userImage user.userImage
:
:
}
So how do I get the id and class attributes out of the json when rendering "user as JSON"? Any idea?
best regards,
Klaas
You can get rid of the class and id properties in the JSON result by creating a custom ObjectMarshaller.
// CustomDomainMarshaller.groovy in src/groovy:
import grails.converters.JSON;
import org.codehaus.groovy.grails.web.converters.ConverterUtil;
import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException;
import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller;
import org.codehaus.groovy.grails.web.json.JSONWriter;
import org.springframework.beans.BeanUtils;
public class CustomDomainMarshaller implements ObjectMarshaller<JSON> {
static EXCLUDED = ['metaClass','class','id','version']
public boolean supports(Object object) {
return ConverterUtil.isDomainClass(object.getClass());
}
public void marshalObject(Object o, JSON json) throws ConverterException {
JSONWriter writer = json.getWriter();
try {
writer.object();
def properties = BeanUtils.getPropertyDescriptors(o.getClass());
for (property in properties) {
String name = property.getName();
if(!EXCLUDED.contains(name)) {
def readMethod = property.getReadMethod();
if (readMethod != null) {
def value = readMethod.invoke(o, (Object[]) null);
writer.key(name);
json.convertAnother(value);
}
}
}
writer.endObject();
} catch (Exception e) {
throw new ConverterException("Exception in CustomDomainMarshaller", e);
}
}
}
You'll need to register in you grails-app/conf/BootStrap.groovy:
class BootStrap {
def init = { servletContext ->
grails.converters.JSON.registerObjectMarshaller(new CustomDomainMarshaller())
}
def destroy = {}
}
This should work in Grails >= 1.1
thanks for the 'quick' reply!
Man, it looks so easy in the end and took so long to figure out.
I got it working doing a map out of the values I needed and rendered them 'as json' like this:
def userProfile = user.get(randomUser)
def jsonData = [
username: userProfile.userName,
userimage: userProfile.userImage,
userstreet: userProfile.userStreet,
:
:
] as JSON
println jsonData
voila, there was the json I needed :)
It doesn't seem to me as if the JSON auto marshaller supports this.
You could use FlexJSON which allows you to exlude certain properties and wrap it into a custom Codec.
Also see here.