Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Render javascript from multiple places: The Rails way

I have a modal form rendered through javascript. The model is called book.

# controllers/books_controller.rb

def new
  @book = Book.new
end

def create
  @book = Book.find(params[:id])
  @book.save
end

Instead of having the new and edit html, I use coffeescript:

# views/new.js.coffee

CustomModal.open "<%= j render('books/modal_form', book: @book) %>"

-

# views/create.js.coffee

<% if @book.valid? %>
CustomModal.hide()
# Other callback scripts for showing alert, etc
<% else %>
# Script for showing errors in the modal
<% end %>

And the link for triggering the modal:

= link_to "Create Book", new_book_path, remote: true

Now, the problem I face is that this link was used just on the book's list page. So the js callback when the book was created, triggered an alert and updated the list with the changes.

Now I have to add this button in another page, where there is no list, so I need a different callback (doesn't matter which callbacks really).

So, I had to add to the create.js.coffee something like:

# views/create.js.coffee

<% if @book.valid? %>
CustomModal.hide()
# if the list exists
#   show alert
#   update lists
# else
#   do different things
# end
<% else %>
# Script for showing errors in the modal
<% end %>

It seems kind of dirty, but it's not so awful. The problem is that I have more than 3 conditionals now, because the "Create Book" button is used multiple times along the webapp.

So, any ideas about a design pattern for this?

like image 346
ascherman Avatar asked May 13 '16 18:05

ascherman


1 Answers

What you're doing isn't terrible but you could a couple of things to clean it up a bit. I would recommend moving the business logic out of both the view and the controller and using the Presenter pattern and Helper patterns, instead. These patterns are pretty well documented these days and have a number of benefits including:

  • promoting skinny controllers
  • promoting smaller, more concise pieces of code
  • promoting the Law of Demeter
  • making unit testing easier

Here's a pretty good description of the Presenter pattern: https://gist.github.com/somebox/5a7ebf56e3236372eec4 or: http://eewang.github.io/blog/2013/09/26/presenting-the-rails-presenter-pattern/

Basically, the way it works is that you move your business logic into a separate Class called a 'presenter'. This class holds the logic that you would normally keep in your controller.

Helpers are also pretty well documented and work much the same way, but for views. Helpers are much easier to test than logic in views. For more information: http://api.rubyonrails.org/classes/ActionController/Helpers.html

It might look something like this (please note that this is just untested, 'pseudo' code that I'm using to illustrate the pattern):

    # app/controllers/books_controller.rb
    helper BooksHelper

    def create
        book = Book.find(params[:id])
        book.save
        @presenter = BookPresenter(book)
    end

    # app/presenters/book_presenter.rb
# move your 'fat' controller logic here

    class BookPresenter
        attr_reader :book, :page_type

        def initialize(book, options={})
            @book = book
        end

        private

        def page_type
            # custom code here for determining page type
        end

        ...
    end

# app/helpers/books_helper.rb
# move your view logic here
module BooksHelper
        def custom_modal(book_presenter)
            if book_presenter.book.is_valid
              handle_valid_book(book_presenter)
            else
               # handle invalid book
            end
        end

        def handle_valid_book(book_presenter)
          custom_list_modal if book_presenter.page_type == 'has_list'
          custom_listless_modal if book_presenter.page_type == 'listless'
          # other conditions
        end

        def custom_list_modal
          # modularized JavaScript for pages with a list
        end

        def custom_listless_modal
        # modularized JavaScript for pages without a list
        end

        ...
    end

So in this case, the business logic can be easily unit-tested in your application using RSpec or whatever testing framework you are using. JavaScript complexity is reduced and testing for it becomes simpler. Your JS outputs can be defined separately in different partials if you like, or just return the actual JS from your helper module. It's a complex pattern to adopt at first, but over time everything will likely feel more natural, modularized, and easy to maintain.

like image 130
hightempo Avatar answered Sep 20 '22 15:09

hightempo