3

I have looked at dozens of stack overflow posts and haven't found a solution that works which is why I'm reaching out for an otherwise well documented use case.

I have a button that that should do this.

  • When clicked, call a custom controller method that updates the model and does other things
  • Call a javascript function to update the page without reloading it (ajax)

Right now, the only way I am able to call a custom controller method is via this way which feels really hacky. I have stripped this down to as simple as possible.

routes.rb

match 'admin/:id/toggleAdmin' => 'admin#toggleAdmin', via: [:patch, :put], as: :toggleAdmin

list.html.erb

<td><%= link_to "Toggle Admin", toggleAdmin_path(id: user.id), method: :patch %></td>

admin_controller.rb

class AdminController < ApplicationController

  def toggleAdmin
    idToToggle = User.find(params[:id]).id
    if idToToggle == current_user.id
      redirect_to admin_list_path, danger: "You tried to make yourself a normal user! Don't do that!"
    else
      User.find(params[:id]).updateToAdmin()
      redirect_to admin_list_path, info: "The user with an ID of #{idToToggle} has had their admin attribute toggled!"
    end
  end  

end

What I would like to do instead of reloading the page when an admin is toggled is to just use some javascript to rewrite that part of the dom.

What is a better way to go about this?

Here are just a few of the various resources I have already tried.

Thanks for the help.

--- Edit for more information.

Using a clean rails app I now have the ability to call a controller method more cleanly but I am not getting an ajax request to go through that updates the page to show that the action was completed. (I am expecting a boolean value change and a flash). Here is the following relevant code:

users.js

$("#edit-form").html("<%= j render partial: 'form', locals: { user: @user } %>")

_form.html.erb

<%= form_for user do |form| %>
  <%= user.admin %>
<% end %>
<%= link_to "Toggle Admin", toggle_admin_user_path(user), method: :put, remote: true %>

edit.html.erb

<h1>Editing User</h1>

<div id="edit-form">
  <%= render partial: 'form', locals: { user: @user } %>
</div>

<%= link_to 'Show', @user %> |
<%= link_to 'Back', users_path %>

users_controller.rb Note that I have only included the toggle_admin and a couple other methods as the rest are just scaffolding.

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy, :toggle_admin]

  def toggle_admin
    if 1 == 1
      logger.info "This is from info"
      u = User.find(params[:id])
      u.admin = !(u.admin)
      u.save
      respond_to do |format|
        format.js { flash[:info] = "The user with an ID of #{@user.id} has had their admin attribute toggled!" }
      end
    else
      redirect_to admin_list_path, danger: "You tried to make yourself a normal user! Don't do that!"
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def user_params
      params.fetch(:user, {})
    end
end
frillybob
  • 600
  • 2
  • 12
  • 26

1 Answers1

3

The routing

First thing, if you need a nested custom route inside a resource you can define it as a member of this resource:

resources :users do
  put 'toggle_admin', on: :member
end

which returns:

toggle_admin    PUT    /users/:id/toggle(.:format)    users#toggle

Second, your method names should be in snake_case : def toggle_admin
See "Instance Methods" section

The controller

If you just want to update a user to admin (guessing by this method here: User.find(params[:id]).updateToAdmin()), you can define the custom toggle_adminmethod inside the UsersController.

Your custom method for triggering ajax request should respond to the js format and point to the corresponding js view (toggle_admin.js.erb)
As a member of the resource User, you can add it to the before_action callback to return the right user when you call the method.

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy, :toggle_admin]
  #...
  def toggle_admin
    if #some condition
      # do some logic here
      respond_to do |format|
        format.js { flash[:info] = "The user with an ID of #{@user.id} has had their admin attribute toggled!" }
      end
    else
      redirect_to admin_list_path, danger: "You tried to make yourself a normal user! Don't do that!"
    end
  end
end

The view

In rails views, when you want to use ajax to update a view without reload, you need to use partial. It's this partial which will be refreshed inside the .js.erb file on a specific DOM element.

Let's make an example for the user#edit view.
First you need to render the view inside a partial form for example, and wrap it into a div with specific id:
edit.html.erb

<div id="edit-form">
  <%= render partial: 'form', locals: { user: @user } %>
</div>

The partial:
_form.html.erb

<%= form_for user do |form| %>
  # the form
<% end %>
<%= link_to "Toggle Admin", toggle_admin_user_path(user), method: :put, remote: true %> 
# don't forget the remote: true for the ajax call

Finally the .js.erb for the custom action (assuming you have jquery):
toggle_admin.js.erb

$("#edit-form").html("<%= j render partial: 'form', locals: { user: @user } %>")

And voilà ! when click the link, you refresh the edit view with new informations from the controller without reloading the view.

Of course, this is an example for a classic resource's edit view but you can adapt it for any case. Just keep the good naming between routes, controllers, controller methods and views (AdminController, list view etc...)


Edit

To handle the flash messages, you need to add inside your layout > application.html.erb :

  <body>
    <% flash.each do |key, value| %>
      <%= content_tag :div, value, class: "classname" %>
    <% end %>
    <%= yield %>
  </body>
Sovalina
  • 5,410
  • 4
  • 22
  • 39
  • Thank you very much for the detailed response. However, I am unable to get it working on a fresh project. I continue getting a syntax error in the controller part. `/home/frillybob/demo/app/controllers/users_controller.rb:8: syntax error, unexpected ':', expecting '}' format.js { info: "The user with an ID of #{idT ^ /home/frillybob/demo/app/controllers/users_controller.rb:8: syntax error, unexpected '}', expecting keyword_end ir admin attribute toggled!" } ^ /home/frillybob/demo/app/controllers/users_controller.rb:86: syntax error, unexpected keyword_end, expecting end-of-input` – frillybob May 12 '18 at 03:23
  • When starting from scratch I get the above error. When commenting out the `info:` part at line 8, I then get an error in the controller `Couldn't find User with id=edit`. Searching online had me go back to devise like in my main app. That was a lost cause, however, as I have no idea how to fix the myriad of errors there. – frillybob May 12 '18 at 03:45
  • 1
    @frillybob for the first error, i fixed it inside my answer. For the `Couldn't find User with id=edit` error, it's just about params. It came from the user here : `toggle_admin_user_path(user)`. What is the view's url ? what params do you have inside your logs when you click on the link ? can you edit your question with the view code to see what params you send to the controller ? – Sovalina May 12 '18 at 09:12
  • Thanks for the response. When I use /users/1/edit I am able to click toggle_admin and have that method execute. That was a simple mistake on me. However, I do not re-render the page without a reload. In the console I get an error No template found for UsersController#toggle_admin, rendering head :no_content I also get no flash message. I will update the original post with some more info. Thanks for the help! – frillybob May 13 '18 at 01:01
  • @frillybob your template must have the same name as the method. Just rename your `users.js` to `toggle_admin.js.erb` (the js file need the erb suffix to be able handle the embedded ruby syntax (e.g. `<%= ... %>`) – Sovalina May 13 '18 at 01:32
  • Thanks for the response. That fixes the console error about the partial, however, the flash and attribute aren't updating. (The attribute does updates upon a page refresh). The console does say it rendered the js page `Rendered users/toggle_admin.js.erb (2.7ms)`. I was able to get javascript to trigger on click with just a simple `alert("hello world")` instead of the re-render of the form. – frillybob May 13 '18 at 02:12
  • 1
    to handle the flash messages i made an edit of my post. For the refresh toggle, i made a demo app and i didn't be able to reproduce your error. My edit partial refresh without reload the `<%= user.admin %>` to true/false everytime i click on "Toggle" – Sovalina May 13 '18 at 02:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/170929/discussion-between-frillybob-and-sovalina). – frillybob May 13 '18 at 02:26
  • I think SO is having issues. I cannot comment on our thread anymore. I still cannot get it figured out. I will pick it up tomorrow. – frillybob May 13 '18 at 03:20