I'm trying to build a little todo app with rails 4 and react. Nothing really hard, it's just a beginning to learn how to use react with rails, but I have difficulties on how to code my rails controllers.
The two urls reachable at the moment are "mysite.local" to display all the todos, and to "mysite.local/todos/1" to display a specific todo with more details.
I have a Todos controller that I use to respond to the AJAX calls related to the todos.
The view that contains the react app is not a "Todos" view because I will add more than just todos in the futur and the react app will have to handle everything.
What I want to do is simple: I want rails to always render the view with the react app regardless of the url used to reach the website. All the controllers in my app (like the Todos controller) are just used to retrieve JSON datas that will be manipulated by react.
The solution I found is to put the react app in the application layout. That way, it will always be present when someone go on the website for the first time. Since it's in the layout it will not be rendered again, the react app can do its job.
My controllers then looks like this:
class TodosController < ApplicationController
def index
respond_to do |format|
format.html
format.json { Todo.all }
end
end
def create
respond_to do |format|
format.html
format.json { Todo.create(todo_params) }
end
end
def show
respond_to do |format|
format.html
format.json { Todo.find(params[:id]) }
end
end
private
def todo_params
params.require(:todo).permit(:content)
end
end
With a controller like this, I can make my AJAX calls and get a JSON, which is the only datas my controller will send, and at the same time I can still reach "mysite.local/todos/1" and rails will do nothing except rendering an empty view.
This method works, but I don't like it because the controller render a view for each action. When someone reach the website for the first time, an empty view will be rendered. Even if the view is empty and it will not display anything on the browser, rails still have to do all the process to render the view. I don't know if this process really cost something, but I don't like the fact that my application is doing something useless.
Is there any way to tell rails to literally do nothing if the format action is html? (= not rendering the "index", or "show" view?)
Or is there a better way to do what I want to do?
Thanks
EDIT
Thanks to gobluego, I modified my application a little. I created a Front controller to handle the client part. Then I moved my Todos controller in an api folder.
Here is my routes.rb file now:
root "front#index"
namespace :api, constaints: { format: 'json' } do
resources :todos, only: [:index, :create, :delete, :show]
end
get '*path' => "front#index", via: :all
and my new Todos controller:
class Api::TodosController < ApplicationController
respond_to :json
before_action :ensure_json_request
def index
respond_with Todo.all
end
def create
respond_with Todo.create(todo_params)
end
def show
respond_with Todo.find(params[:id])
end
private
def todo_params
params.require(:todo).permit(:content)
end
def ensure_json_request
return if request.format == :json
render :nothing => true, :status => 406
end
end
That way, any url is handled by front#index, except all the apis urls, which is what I want. To ensure that nothing is rendered if, for example, someone tries to reach mysite.local/api/todos in the browser, a before_action is used and it render nothing if the format is not json.
Why not use pages#index instead as the root path? That way, TodosController is only responsible for being the back end service for your app.
As an additional measure you can enforce that both requests and responses are only of JSON format. As for the implementation of that, this is a a good starting point.
Related
I am creating a Rails app to which its users have two ways of interaction.
Through a web interface and through an API (mobile app and other software).
The functions for the web and the api access are the same, for example a user can write a comment via the web interface (views) or through the API.
What I would do now is create all the controllers with views, and then create a namespace /API/ with its own controllers. The problem now is of course that I have to write the function to write write a comment twice. Once in my PostController and once in my API/PostController.
I learned that Rails = DRY, so I guess I am doing something wrong.
How would I make the same functions available for my views and at the same time for my API (JSON response).
And how would the routes and namespaces look like? I think even if I find a way to not repeat myself it would be nice to have API routes like api/v1/...
I've done something similar, both using an API namespaced controller and using a single controller.
Since you seem convinced that you can do everything equally within your Web interface as your API interface it would make sense to merge the two, though note that this is not always true.
If it makes more sense for you to use a single controller, then what you need to do is play with the repond_to format of requests coming to a single controller action.
Ex:
class ArticlesController < ApplicationController
def index
#articles = Article.all
respond_to do |format|
format.html # will render a view by default
format.json { render json: #articles }
end
end
def create
article = current_user.articles.build(article_params)
respond_to do |format|
if article.save
flash[:notice] = "success!"
else
flash[:error] = "uhoh!"
end
format.html # renders a view by default
format.json { render json: { errors: #articles.errors }
end
end
private
def article_params
params.require(:article).permit(:title, :content)
end
end
I have a Foo resource that has_many Bars. I'm using nested resources for a limited number of actions, but otherwise prefer to keep my routing for bars shallow. There are two ways to navigate to the edit view for the Bar object - either from the nested path that includes foo, or from the shallower bar path that isn't nested inside foo. For example, a user might click the edit button from the page at /foos/[:foo_id]/bar/[:bar_id]; or from /bars/[:bar_id].
In the first case, I want the controller to redirect the user back to the parent foo page: /foos/[:foo_id] after the record is updated. In the second case, I want it to redirect to the index view for bars: /bars. I believe I need some sort of conditional in the #edit action in the bars controller that will tell Rails where to go after #update executes.
# config/routes.rb
resources :foos do
resources :bars, only: [:new, :edit]
end
resources :bars
# bin/rake routes:
foo_bars POST /foos/:foo_id/bars(.:format) bars#create
new_foo_bar GET /foos/:foo_id/bars/new(.:format) bars#new
edit_foo_bar GET /foos/:foo_id/bars/:id/edit(.:format) bars#edit
bars GET /bars(.:format) bars#index
POST /bars(.:format) bars#create
new_bar GET /bars/new(.:format) bars#new
edit_bar GET /bars/:id/edit(.:format) bars#edit
bar GET /bars/:id(.:format) bars#show
PATCH /bars/:id(.:format) bars#update
PUT /bars/:id(.:format) bars#update
DELETE /bars/:id(.:format) bars#destroy
The controller for bars:
# app/controllers/bar_controller.rb
def edit
#bar = bar.find(params[:id])
#foo = #bar.foo
end
def update
#bar = bar.find(params[:id])
#foo = #bar.foo
respond_to do |format|
if #bar.update_attributes(bar_params)
format.html { redirect_to #foo, notice: "bar successfully updated" }
else
format.html { render action: "edit" }
end
end
end
I'm trying to change the redirect_to #foo line in the #update action so there is conditional logic that switches out #foo for #bars depending on where the #edit action was initiated. I've tried something like the following to test whether params[:foo] is present when the #edit action is called, setting an instance variable for the redirect.
def edit
if params[:foo]
#redirect_page = #foo
else
#redirect_page = #bars
end
#bar = bar.find(params[:id])
#foo = #bar.foo
end
def update
# code omitted...
format.html { redirect_to #redirect_page, notice: "bar successfully updated" }
# code omitted...
end
This doesn't work. Rails states cannot redirect to nil!. I've also tried something using a test based on URI(request.referer).path in the #edit action, without success.
I'm still not entirely clear how the Rails magic happens in the controller. I believe the #edit action is the proper place to define the conditional for the redirect (or through a method called in the #edit action), as that's where the controller will "see" the incoming request and know where it came from. But I can't quite figure out to capture that information, and pass it along to #update. Appreciate any guidance.
In your edit forms, add a hidden_field_tag:
<%= hidden_field_tag "route", request.env['PATH_INFO'] %>
Then in your controller, you can have an if statement and use a redirect_to based on what the params[:route] is.
I figured it out. The params[:route] method using request.env['PATH_INFO] wasn't working for me, because the 'PATH_INFO' variable in the form was providing the path handed off to the bars#update action, instead of the path where the bars#edit action was initiated.
After clicking "Edit" from the parent foo page at /foos/[:id] the params hash is:
>> params
=> {"controller"=>"bars", "action"=>"edit", "foo_id"=>"3786", "id"=>"16"}
There is no value for params[:route] when the form is first accessed - the hidden field is only added to the params hash after clicking "Update" in the edit form:
>> params[:route]
=> "/foos/3786/bars/16/edit"
This could work, but would require building logic to parse the route in order to redirect to /foos/[:foo_id]
It turned out to be simpler to use the Rails flash method to store the path for redirecting back to the source page. I did this by calling a custom method set_redirect_path in the BarsController, and calling it in bars#edit. This sets a value for the source in the flash, which is available in bars#update. Maybe there's a better/more conventional way to achieve this, but this seems to be a clean and simple way to do what I want.
# app/controllers/bars_controller.rb
def edit
set_redirect_path
#bar = bar.find(params[:id])
#foo = #bar.foo
end
def update
#bar = bar.find(params[:id])
#foo = #bar.foo
respond_to do |format|
if #bar.update_attributes(bar_params)
format.html { redirect_to flash[:source], notice: "bar successfully updated" }
format.xml { head :ok }
else
format.html { render action: "edit" }
format.xml { render xml: #bar.errors, status: :unprocessable_entity }
end
end
end
private
def set_redirect_path
flash[:source] = URI(request.referer).path
end
One advantage of this approach is I can now get rid of conditional logic in the shared partial app/views/bars/_list.html.haml that was required to determine whether clicking the "Edit" button should route to edit_foo_bar_path or to edit_bar_path (i.e. the former is chosen if #foo exists). Consequently, I can delete :edit for the nested resource :bars. Since the flash captures the incoming source of the request and stores it for reference in the #update action, all edit requests can use the same edit_bar_path, regardless of where they originate from. After update Rails redirects the user to the point where they initiated the #edit action.
Is it possible to have a controller that interacts in a standard way at both the top level and also the nested level? Or will static routes need to be configured?
When I visit the first address /list/:list_id/items I want it to follow the nested_index method to display only a subset of the listed items (The items that belong to the list).
http://localhost:3000/list/:list_id/items
When I visit the below (/items) address I want it to show the whole list of items.
http://localhost:3000/items
/app/controllers/items_controller.rb
def index
#Item = Item.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #Item }
end
end
def nested_index
#list = List.find(params[:list_id])
#items = #list.items.paginate(page: params[:page], per_page: 5)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #list }
end
end
/config/routes.rb
AppName::Application.routes.draw do
resources :list do
resources :items
end
end
# Do I need to add further routes here?
Personally, I think you should split this out into two separate controllers.
The index method of your controller should be designed to do just one thing. In the case of your nested route it should be fetching all the items appropriate for the selected list and passing them to the appropriate view. In the other instance it is fetching all items and (probably) passing them to a completely different view.
It seems you're trying to get one controller to do the job of two, simply for the sake of the controller's name.
I'd suggest creating an apps_controller and use that to collect all your items and display them, and leave your items_controller for its nested use.
Remember you don't need to name a controller after the model it interacts with ... rather, you should name it after the function it is responsible for. A controller which receives an activation code for a user account might update an is_active boolean on a User model, but you would call this controller Activations since that is what it does.
If you have lots of overlap between controllers you can move their code into modules and then include those modules in both controllers. This way you can DRY up your code whilst keeping the logic separate where necessary.
Take a look at these links for some ideas on code extraction:
http://railscasts.com/episodes/398-service-objects
http://railscasts.com/episodes/416-form-objects
But before you start refactoring all of your code into modules ... consider whether it adds anything to your codebase. Does it make things simpler? Does it make things more readable? Does it save you anything other than typing out a few more lines? If there's no benefit to refactoring ... just don't do it.
#Jon is right. This should be split into several different controllers:
# app/controllers/items_controller.rb
class ItemsController < ApplicationController
# default RESTful actions to operate on lists, for example #index
def index
#Item = Item.all
respond_to do |format|
format.html
format.json { render json: #item }
end
end
end
# app/controllers/lists_controller.rb
class ListsController < ApplicationController
# default RESTful actions to operate on lists
end
# app/controllers/lists/items_controllers.rb
class Lists::ItemsController < ApplicationController
def show
#list = List.find(params[:list_id])
#items = #list.items.paginate(page: params[:page], per_page: 5)
respond_to do |format|
format.html
format.json { render json: #items }
end
end
end
Routes:
AppName::Application.routes.draw do
resources :items
resources :lists do
resources :items
end
end
I am trying to build something really simple but I am bit confused. I have a customer controller with 2 actions, index and identification. The views of these actions simply display an image each. What I want to do is be able to change from the index view to the identification view when an ios app sends a simple json get request (it could be post, doesn"t really matter as I only want to change the view). My customer_controller.rb is
def index
respond_to do |format|
format.html # index.html.erb
format.json {render 'identification'}
end
end
def identification
#date = Time.now.strftime("%A, %d %B %Y")
#time = Time.now.strftime("%H:%M %p")
end
My routes.rb is
get "customer/index"
post "customer/index"
get "customer/identification"
Can anyone point me to the right direction as to what I am doing wrong? Whenever the ios app sends a json nothing happens.
When you call render 'identification' that only tells rails to use the 'identification' template, it does not actually call the #identification method.
I've got a form with remote => true.
And right now my controller looks like:
# POST /items
# POST /items.json
def create
#item = #store.items.build(params[:item])
respond_to do |format|
if #item.save
format.html { redirect_to edit_admin_item_path(#item), :flash => {:success => "#{#item.name} was successfully created."} }
format.js { render :js => "window.location.href = ('#{edit_admin_item_path(#item)}');"}
format.json { render json: #item, status: :created, location: #item }
else
format.html { render action: "new" }
format.js { render :partial => 'fail_create.js.erb', :locals => { :ajax_errors => #item.errors.full_messages } }
format.json { render json: #item.errors, status: :unprocessable_entity }
end
end
end
Which works but feels very clumsy. It also doesn't allow me to use a flash notice, which is sad time indeed.
Ideally I feel like I should be able to simply use "format.js { redirect_to...} or check against the request headers and redirect_to. Sheesh!
I'm not sure what the best solution is. Any advice would be super awesome, thanks in advance!
-- PS -- I know this has been asked somewhat before but to no avail: How to redirect after a successful AJAX form submission. There seems to many questions similar floating around, but no real solutions.
I think it might be impossible. The response to a Ajax request is processed by XMLHttpRequest. If a 3xx response is returned, XMLHttpRequest will follow the redirect itself, if the URL is of same origin. No matter how you set the headers, the browser cannot be aware of that. So the only way could be changing window.location with some Javascript.
I use a combination of Rails responders to generate my response messages and some content in my <action>.js file.
The content of — say update.js would look something like this:
// Checks if the article slug has changed.
// If it has the entire page should be reloaded at that new location.
<%= reload_if_slug_changed #article, params[:id] %>
// Displays the flash notices
// See ApplicationHelper#js_flash_response
<%= js_flash_response %>
Where the different methods are defined in some helper (in my case my ApplicationHelper). The content of the different methods are as follows:
def js_flash_response
if flash.now[:notice].present?
js = "$('#notice').html('#{flash.now[:notice]}').change();"
elsif flash.now[:alert].present?
js = "$('#alert').html('#{flash.now[:alert]}').change();"
end
end
def reload_if_slug_changed object, expected_value
"window.location.href = '#{url_for [:edit, object]}';" if object.slug != expected_value
end
The content of the flash messages are generated automatically by Rails responders and displayed with the now scope that deletes the from the flash hash, ensuring that if the user reloads (after the flash has been displayed) they will not reappear.
I don't believe that you should ever make a form pointing to a restful create action a remote one, because you would always expect critical redirect, so in my case I only need to redirect if the url slug has changed.
I hope that this helps. It's not a solution, but simply the way that I handled some of the same problems.
Best regards.
Under your scenario, here's how I would inject javascript into the page from a controller action. After you've completed the logic section of your action insert something like this:
render :update do |page|
page << "javascript_here"
end
This should allow you to insert you window.location or create a javascript flash method and call it when your create method executes correctly.
If you're looking to DRY up your controller actions, I would recommend looking into this Railscast about make_resourceful. Make_resourceful automagically performs each core activity for each action. It also allows you to tap into the hooks that they've created such as before :create, after :create, response_for :create, and after :create_fails. By using this gem, you can run code based on the success or failure of your methods and have finer grained control over them.
In addition to this, you should be able to initialize a create.js.erb and create_fails.js.erb in your view file, include a format.js without anything passed to it in your controller, and Rails will automagically run that file that contains javascript depending on if the controller action executed successfully.