Better Rails view helpers, or how to blocks and awesome too

20 Apr 2016, by Sia Sajjadi

Elevating Rails Views with Helpers

It's nice to have flexible view helpers. There are 3 ways I can think of doing it: ## Use A Context :D:D!!! (the gooda way) Similar to a form builder in Rails, we can use a 'sub view' context. ```ruby module InformationCardHelper Context = Struct.new(:h, :heading_data, :body_data, :overlay_data) do def overlay(&block) overlay_data[:content] = h.capture(&block) end def heading(&block) heading_data[:content] = h.capture(&block) end def body(&block) body_data[:content] = h.capture(&block) end end def information_card(type, opts = {}, &block) context = Context.new(self, {content: "default content"}, {content: ""}, {content: ""}) opts_css = opts.fetch(:wrapper, {}).fetch(:class, nil) id = opts.fetch(:wrapper, {}).fetch :id, "" css = ["#{type}-information-card information-card", opts_css].compact.join " " capture { block.call context } content_tag(:div, nil, class: css, id: id) do capture do concat content_tag :div, nil, class: "overlay", &-> do content_tag(:div, context.overlay_data[:content], class: "nested-divs-and-whatnot") end concat content_tag:div, nil, class: "text-content", &-> do capture do concat content_tag(:div, context.heading_data[:content], class: "card-heading") concat content_tag(:hr) concat content_tag(:div, context.body_data[:content], class: "card-body") end end end end end end ``` And ```slim = information_card "red", class: "awesome" do |c| - c.overlay do = link_to "some/other/path" i.alert-icon> | Really important overlay! - c.heading do = t(".verified_title") - c.body do = link_to "some/path" do = t(".verified_html") ``` It's understandable, it's flexible, it's encapsulated, it's sexy and I love how ruby lets me do this kind of stuff so easily. Every problem that the alternatives further down have is addressed. In information_card there is ```capture { block.call context }``` which calls the main 'do' block containing the context that is passed in, and the calls to overlay, heading etc. None of the '=' in that block actually prints to the view's output buffer. All of that is caught by capture and thrown out. But it allows the context object to capture each block and store the strings the blocks produce (again via capture). Unfortunately this is necessary because '=' doesn't __return__ anything, it has a side effect of mutating the 'buffer' that is relevant, which is usually the main view buffer. Capture will wrap up '=' so that it prints into a temporary buffer, and returns that as a string. Finally we build the output for the method that gets sent to the view buffer via the '=' in ```= information_card "red", class: "awesome" do |c|``` And we play a similar trick to build our content via content_tags via capture. It's possible to do ```ruby content_tag(:div, nil, class: css, id: id) do buf = concat content_tag :div, nil, class: "overlay", do content_tag(:div, context.overlay_data[:content], class: "nested-divs-and-whatnot") end buf += content_tag:div, nil, class: "text-content" do buf = content_tag(:div, context.heading_data[:content], class: "card-heading") buf += content_tag(:hr) buf += content_tag(:div, context.body_data[:content], class: "card-body") buf end buf end ``` But i prefer to use capture and concat. Concat will add anything passed to it to the view buffer in scope, so we get it to add to a temporary buffer via capture, which returns the concatenated buffer as it's return value, letting content_tags work proprely. Incidentally, if you do ```ruby concat content_tag :div, nil, class: "stuff" do "other stuff" end ``` ruby will think the block is for concat, NOT content_tag. But we can tell ruby to send a lambda as a block argument to content_tag by using & and ->, ie ```ruby concat content_tag :div, nil, class: "stuff", &-> do "other stuff" end ``` which will work well :D ## Alternativ: Partials (a sucky way) ```slim = render partial: "some_partial", locals: {alert: alert, type: "red", opts: {class: "awesome"}} ``` then in _some_partial.html.slim its plausible to do something along the lines of ```slim - type_class = "#{type}-information-card" - css_class = defined?(opts) && opts.has_key(:class) ? opts[:class] : "default" - css = [type_class, css_class].join " " .information_card class= css .text-content .card-heading = alert.title hr .card-body = alert.description ``` And if you want to do something fancy like throwing in a link in the body you can create a decorator class and throw that into the render partial instead. ```ruby class AlertDescriptionDecorator < SimpleDelegator include ActionView::Helpers::UrlHelper include ActionView::Helpers::TagHelper def description content_tag :div, nil, class: "awesome-looking-link-container" do link_to super, "some/path" end end end ``` ```slim = render partial: "some_partial", locals: {alert: AlertDescriptionDecorator.new(alert), type: "red", opts: {class: "awesome"}} ``` Now do that every time you need to pass in something different. This is annoying, veering off course semantically. Why should we have to build a decorator specialized to Alert handle link_to for one use case? It's a *small* step in an arguably less **easy to understand and maintain** direction. ## Alternative: Naive Helper Methods (a less sucky, still sucky way) What about ```ruby def information_card(type, opts = {}, &block) opts_css = opts.fetch(:wrapper, {}).fetch(:class, "default") css = ["#{type}-information-card information-card", opts_css].compact.join " " content_tag(:div, nil, class: css) do content_tag(:div, nil, class: "text-content") do block.call self end end end def information_card_heading(&block) content_tag(:div, nil, class: "card-heading", &block) + content_tag(:hr) end def information_card_body(&block) content_tag(:div, nil, class: "card-body", &block) end ``` ```slim = information_card "orange-content", class: "awesome" do = information_card_heading do = alert.title = information_card_body do = alert.body ``` Now it's trivial for us to do things like ```slim = information_card "orange-content", class: "awesome" do = information_card_heading do = alert.title = information_card_body do .awesome-looking-link-container = link_to "some/path" do = alert.body ``` So information_card_heading and information_card_body are a bit verbose in naming, we can't define body and heading; imagine if there were 5 different card helpers each needing heading & body. Each module would have def body; ...; end, overriding each other. A little ugly, relying on method names to sort out namespacing issues. And the 'flexibility' actually kinda sucks, we are coupled! What if we changed the card one day to have an overlay that needed to be outside the 'text-content' div for css purposes? Something more like ```slim .information_card.red-infomation-card.awesome .overlay .nested-divs-and-whatnot = local_variable_containing_text || "default text" .text-content .card-heading = alert.title hr .card-body = alert.description ``` You could try to do something like ```slim = information_card "orange-content", class: "awesome" do = information_card_overlay do | Some stuff = information_card_heading do = alert.title = information_card_body do .awesome-looking-link-container = link_to "some/path" do = alert.body ``` Which looks ok but breaks, information_card helper is wrapping *all* of that stuff in the 'text-content' div. But overlay should be outside. One way to solve this is to just manually do it; ```slim = information_card "orange-content", class: "awesome" do = information_card_overlay do | Some stuff .text-content = information_card_heading do = alert.title = information_card_body do .awesome-looking-link-container = link_to "some/path" do = alert.body ``` Which slowly degrades any encapsulation, and *requires* you to go back and modify each usage of information_card. Another way is to work out some fancy pants string regex/html parse stuff in the information_card helper to do this automatically which is an activity I hope most people are unwilling to engage in. In this use case it might not be so hard but the practice is not good. We **always** want to implement a sensible data type and a straight forward routine rather than a dumb data type and a clever (read **less understandable and maintainable**) routine. Using a partial more or less solves this, but since the partial has locals being passed to it, any HTML in the overlay content needs to be passed in like so ```slim render partial: 'some_partial', locals: {alert: alert, type: 'red', class: "awesome", overlay: "

#{sanitize my_overlay_content}

"} # or render partial: 'some_partial', locals: {alert: alert, type: 'red', class: "awesome", overlay: (render partial: "complex_overlay")} ``` Here you pass a string of html, completely throwing away the usefulness of your templating language (slim in this case) or you create yet another file that contains your content. If I were hard pressed to do it this way, the second option is preferable to me, and I've done this kind of thing before. But could you imagine having to do it this way for each input in a form, rather than using a form builder? Could you imagine how many partials you'd have to create and keep track of?


Cookies help us deliver our services. By using our services, you agree to our use of cookies.