I'm writing Shop using Sinatra. I implemented adding to Basket, but I can't make deleting from Basket work.
My class App:
get "/basket" do #working
products_in_basket = FetchBasket.new.call
erb :"basket/show", locals: { basket: products_in_basket }
end
post "/basket" do #working
AddToBasket.new(params).call
redirect "/"
end
delete "basket/:id" do # doesn't work
DeleteBasket.new(params).call
redirect "/"
end
My DeleteBasket:
module Shop
class DeleteBasket
attr_reader :product_id, :id
def initialize(params)
#id = params.fetch("id").to_i
#product_id = params.fetch("product_id").to_i
end
def call
basket = FetchBaskets(id) # finds Basket instance with given id
return unless basket
reduce_basket_quantity(basket)
def reduce_basket_quantity(basket)
if basket.quantity >= 1
basket.quantity -= 1
#warehouse = FetchWarehouseProduct.new.call(product_id)
#warehouse.quantity += quantity
else
BASKET.delete(basket)
end
end
end
end
end
Delete in views:
<td> <form action="/basket/<%=b.id%>" method="post">
<input type="hidden" name="_method" value="delete">
<input type="hidden" name="product_id" value=<%= b.product_id %>>
<input type="hidden" name="id" value=<%= b.id %>>
<button type="submit">Delete</button>
</form>
It doesn't redirect to home page as it should, and it doesn't change basket quantity by 1. It simply does nothing.
I think to most obvious reason is you are not calling the delete http method, but post instead:
<form action="/basket/<%=b.id%>" method="post">
Normally you would fix this by using
<form action="/basket/<%=b.id%>" method="delete">
but this is not yet supported according to this answer.
I think your best bet is to define your delete route as a post
post "delete-basket/:id" do
DeleteBasket.new(params).call
redirect "/"
end
and then write
<form action="/delete-basket/<%=b.id%>" method="post">
Remember how we said that in an HTML form we can specify the HTTP verb that is supposed to be used for making the request like so:
<form action="/monstas" method="post">
...
</form>
This makes the form POST to /monstas, instead of the default GET.
Now, it’s probably fair to say that every sane person in the world would expect that it is also possible to make that a PUT, or DELETE request. Like so:
<form action="/monstas" method="delete">
...
</form>
Except that … it’s not. Today’s browsers still do not allow sending HTTP requests using any other verb than GET and POST.
The reasons for why that still is the case in 2015 are either fascinating or sad, depending how you look at it [1] But for now we’ll just need to accept that, and work around it.
Sinatra (as well as Rails, and other frameworks) therefore support “faking” requests to look as if they were PUT or DELETE requests on the application side, even though in reality they’re all POST requests.
This works by adding a hidden form input tag to the form, like so:
<input name="_method" type="hidden" value="delete" />
Source: https://webapps-for-beginners.rubymonstas.org/resources/fake_methods.html
Related
This question has been rewritten because the way I originally wrote it was unclear.
How do I take the values within #form_params and pass them to get '/show_results'?
class MyApp < Sinatra::Base
configure :development do
register Sinatra::Reloader
end
get '/' do
erb :index
end
post '/form_handler' do
#form_params = params
redirect to("/show_results/?#{#form_params}")
end
get '/show_results' do
erb :display_result
end
end
index.erb
<form method="POST" action="/form_handler">
...
<input type="hidden" name="first" value="John">
<input type="hidden" name="middle" value="Q">
<input type="hidden" name="last" value="Public">
...
</form>
params (result)
{first => "John", middle => "Q", last => "Public"}
the #form_params is a hash in your code. sth like that:
{"first" => "John", "middle" => "Q", "last" => "Public"}.
actually it's a string but can be converted into hash.
when you apend it to the redirect url, it becomes,
/show_results?{"first" => "John", "middle" => "Q", "last" => "Public"}
and it is different from your expectation. as i guess you want something like that 0
/show_results?first=john&middle=Q ...
what you should do is,
take params and parse it. maybe sinatra has a built in method for this (better look documentation) but this can aslo easily be done with plain ruby.
post '/form_handler' do
redirect_url = '/show_results'
params.each { |k,v| redirect_url += "?#{k}=#{v}" }
#don't worry about above line, the rest of ?-s will be converted into &
redirect to(redirect_url)
end
apart from all these, why are you sending post request then redirect it to a get block?
if you submit your form with get, it will automatically will be parsed to
url?k1=v1&k2=v2 ..
i mean this.
index.erb
<form method="get" action="/form_handler">
<input type="text" name="first" value="John">
<input type="text" name="middle" value="Q">
<input type="text" name="last" value="Public">
<input type="submit" name="ok">
</form>
main file.
get '/' do
erb :index
end
get '/form_handler' do
erb :display_result
end
then in your display_result file, or inside get block, you can easily access the params[:first], params[:middle] and use them.
The params object is per-request. When you redirect, unless you explicitly pass them with the url they will be lost. By the way, you can use sessions, which passes data as a cookie but can be configured to use a database instead.
Here's an option if you require 'active_support/all'
redirect to("/show_name?#{my_hash.to_param}"):
In ActiveSupport (bundled with Rails, not Sinatra) the Hash#to_param method works like this:
hash = { a: 1, b: 2 }
hash.to_param
# => "a=1&b=2"
If you have learned about the components of a URL, you will know that query params can be passed after the ? in a url, which is how "/show_name?#{my_hash.to_param}" works.
It's nice that this method also works with hash and array params, which use a special syntax in the url, i.e. /path?my_list[]=1&my_list[]=2 makes param[:my_list] == ['1','2'].
Except that in some circumstances the & [ ] characters need to be escaped (replaced with special character sequences).
puts ({a: 1, b: [1,2]}.to_param)
# => a=1B%5D=1&b%5B%5D=2
URI.unescape CGI.unescape({a: 1, b: [1,2,3]}.to_param)
# => "a=1&b[]=1&b[]=2&b[]=3"
I have basic Store app - I want to be able to add product to basket and then delete it using Sinatra. My adding works, but I can't make delete to work too. I had special function for that, but now I just want to see whether delete route works at all. My main app has many routes, and part I'm talking about is:
post '/basket' do #it works, adds to basket and redirects
AddToBasket.new(params).call
redirect '/'
end
delete "/basket/delete" do #it doesn't work at all and doesn't redirect
basket = BASKET.find{|p| p.id == params["id"]}
BASKET.delete(basket)
redirect "/"
end
In HTML I have:
<% basket.each do |b| %>
<form action="basket/delete" method="post">
<input type="hidden" name="_method" value="delete">
<input type="hidden" name="id" value=<%= b.id %>>
<button type="submit">Delete</button>
</form>
<% end %>
As you can see, after clicking on "Delete" button, I'm sending "id" in my params helper.
basket = BASKET.find{|p| p.id == params["id"]}
should find one specific item with this id and delete it from my big array BASKET. But it doesn't work, after clicking on "Delete" I'm otransferred to basket/delete page and I have an error, because post for basket/delete doesn't exist. It should redirect me to my index page. What's more, it doesn't delete my basket item, it still exists. I'll appreciate any help.
You need this component in your middleware pipeline use Rack::MethodOverride
Another way seems to be put set :method_override, true in your Sinatra::Base class
See this also
I'm trying to build an e-commerce site using Sinatra, as practice. I'm getting stumped on how to implement the 'Add to Cart' Button. My thought process about it is:
User clicks 'add to cart'
The button 'add to cart' invokes a ruby method, for example clicking on the following button
<input class='btn btn-primary' type='button' value='Add To Cart'></input>
should call a ruby method like
shop.add_to_cart(product, quantity)
An example of what this method might looking like:
class Shop
attr_reader :cart
def initialize
#cart = []
end
def add_to_cart(product, quantity)
#cart << product, quantity
end
end
In Rails, I think we use the helper_method in the controller? Is there anything similar I can do in Sinatra?
Thanks!
Note:
This is if you want to do it in ruby. You could probably also do it in javascript as mentioned in the other answer, but I cannot help you with that because I don't know javascript well enough.
To run the ruby method on button click you first need to create a <form> with only the button, then have that run a route in your app file that will run the method then redirect back to the page you were on. Here is my code (have not tested):
home.erb:
<form method="post" action="/runMethod">
<input type="hidden" name="product" value="whatever">
<input type="hidden" name="quantity" value="whatever">
<input class='btn btn-primary' type='submit' value='Add To Cart'>
</form>
You would set the values of the two hidden inputs (where I wrote "whatever") to the quantity and product according to their names.
App File:
class Shop
attr_reader :cart
def initialize
#cart = []
end
def add_to_cart(product, quantity)
#cart << product, quantity
end
end
get '/' do
erb :home
end
post '/runMethod' do
shop.add_to_cart(params[:product], params[:quantity])
redirect '/'
end
This can also be accomplished with ajax so that you dont have to leave the page:
$("#hideCarousel").submit(function() {
//posts the contents of the form to /action using ajax
$.post("/action", $("#myform").serialize(), function(result){
// assuming result is a string of the updated data in html
// and assuming that your data goes in an element with the id data-table
$("#data-table").html(result)
});
return false; // prevents the form from submitting normally
});
Rails/Sinatra run on the server side. If you want stuff happening in Rails directly you probably need a form and post back data.
nowadays people use javascript and it's javascript that makes the callbacks in an asynchronous fashion for this kinds of things.
this is probably a stupid question but I cannot figure out how to do it.
So I'm new to Scala/Lift and I read the ajax form chapter in http://simply.liftweb.net/index-4.8.html#toc-Section-4.8 but the "RedirectTo" in the example does not seem to be very "ajaxian" to me. Often in case of submitting a form via ajax, you would just partially rerender the same page, right?
So that's what I'm trying to do and am completely failing right now.
How do I let Lift rerender just a part of the same page after I submit the form via ajax?
Any hints would be appreciated. Thanks.
Basically, what I have looks like this:
<div id="main" class="lift:surround?with=default;at=content">
<h2>Welcome to your project!</h2>
<div class="lift:Test">
<div>
<form class="lift:form.ajax">
<fieldset>
<label for="name">Name:</label>
<input id="name" name="name" type=text>
<p></p>
<input id="save" type="submit" value="Save">
</fieldset>
</form>
</div>
<div>
<span id="theName">Name</span>
</div>
</div>
</div>
class Test {
def render = {
var name = ""
def process(): JsCmd = {
Thread.sleep(500)
S.notice("Entered name is: %s".format(name))
Noop
}
"#theName " #> "This shall be updated with the name given in the form above" &
"#name" #> (SHtml.text(name, name = _) ++ SHtml.hidden(process))
}
}
How would I update "theName" when submitting the form?
Have a look at http://lift.la/shtmlidmemoize-simple-ajax-updating (Example Code). There is SHtml.memoize and SHtml.idMemoize which automatically caches the HTML code. Not sure why it is not used in this example in the Simply Lift book.
You have a 2 step form right? The above poster is correct.
Save your transformation in a RequestVar.
in your above example, the method you want to save is render, so 1st memoize the transform:
private def renderTest= SHtml.memoize { render }
Then, you can save this memoized transformation in a RequestVar (lasts for 1 request), or maybe a TransientRequestVar depending on your needs.
private object testTemplate extends RequestVar(renderTest)
When you want to replay the transform, from an ajax event - testTemplate.is.applyAgain.
I might have misunderstood the original question, b/c if you want to do a 2 step form, you don't really need the memoize. The memoize is if something changes on your current form, and you want to update it via an ajax event, i.e. on click or on change, b/c normally the form wouldn't update unless you did an ajax submit.
I'm building some crude CMS-like functionality (to get acquinted with Play Framework). For this test-case I've build 2 pages, 1 for listing tags and 1 for creating/editing/saving tags.
The flow is like this (routes-file):
#list tags
GET /tags Application.listTags
#view/edit existing tag
GET /tag/{<(?!new$)(.+)>name} Application.showTag
#new tag
GET /tag/new Application.showTag
the create/view/edit page displays a form which gets it's values from a tagDTO.
The normal flow works without problems, but when the form gives validation-errors (e.g: the tag-name must exist) I want to display the page again, repopulating the form with the edited values.
For this (following the Play Framework conventions) I could use the 'flash'-object which contains these last values, but the form is already bound to the tagDTO (which is null on redirect) instead of the 'flash'-object.
First the code:
Application.java
.....
public static void showTag(String name) {
TagDTO tagDTO = TagDTO.buildDTOFromModelOrNew(name);
render(tagDTO);
}
/**
* Save tag and redirect to Show
*
* #param name
* #param displayname
* #param isnew
*/
public static void saveTag(
#Required(message="Name is required") String name,
String displayname,
boolean isnew)
{
checkAuthenticity();
if(validation.hasErrors()) {
params.flash();
validation.keep();
showTag(null);
}
//fetch tagDTO based on backend or create new if not exist
TagDTO tag = TagDTO.buildDTOFromModelOrNew(name);
// Append / Overwrite values
tag.displayname = displayname;
tag.name = name;
//save result to model
TagDTO.buildAndSaveModelFromDTO(tag);
flash.success("Thanks for " + (isnew?"creating":"updating") + " tag " + tag.name);
//redirect to show
showTag(tag.name);
}
And ShowTag.html
#{extends 'main.html' /}
#{if flash.success}
<p class="success">${flash.success}</p>
#{/if}
#{ifErrors}
<p class="errors">Oops...</p>
#{/ifErrors}
#{form #Application.saveTag()}
#{authenticityToken /}
<p>
<label for="name">Name: </label>
<input type="text" name="name" id="name" value="${tagDTO.name}" />
<span class="error">#{error 'name' /}</span>
</p>
<p>
<label for="displayname">Displayname: </label>
<input type="text" name="displayname" id="displayname" value="${tagDTO.displayname}" />
<span class="error">#{error 'displayname' /}</span>
</p>
<p>
<input type="hidden" name="isnew" value="${tagDTO.isnew}" />
<input type="submit" value="Submit your comment" />
</p>
#{/form}
Now I could think of some ways to make it work, but none really elegant:
bind the form to the flash-object (or params-object) and populate the flas/params- object from the tagDTO
on validation-failure, refetch the tagDTO (not avail anymore so DB-call necessary) and overwrite values in tagDTO with values available in flash-object, bind form to tagDTO.
like 2, but using some sort of cache to quickly fetch tagDTO (so no need for db-call)
Some general mechanism to (de)serialize tagDTO from/to the session.
In short, I don't like any of them really.
What would you consider to be a best practice in this situation? Or is there any functionality in the Play Framework that I'm missing?
This is where the explicit render calls comes handy. Retain the form values from previous submission and give it back (if validation fails) as follows,
checkAuthenticity();
if(validation.hasErrors()) {
render("#showTag", name, displayname, isnew);
}
This will avoid the extra redirect (307 in case of Play!) that would have happened if you had called 'action from another action'.
Render the form again and avoid the redirect is a solution. I think it's OK if a user press F5 he will get the error again. But I think you should create a reload/cancel button, so the user can dismiss all the information.
To have always the correct URL you can do the following in the routes.conf:
GET /tag/create TagController.create
POST /tag/create TagController.insert
The flash solution has the disadvantage that your cookie can get really big.