tilt json example?

Hi!

I’m trying to utilize tilt to use jbuilder to build my json. I have templates named show.json.jbuilder and I have installed gem 'tilt-jbuilder' in my Gemfile.

The documentation seems to indicate this would be enough Along with ERB, Hanami supports the full range of template engines available through Tilt, and will detect the engine based on each template’s file extension.

I have additionally tried to register the jbuilder extension in config/app.rb Tilt.register Tilt[:jbuilder], :json

It may be that I am trying to utilize one action to respond to both html and json, but when I set the response format to json I still get html output.

Can I utilize tilt in this manner? Is the expectation that the templates will only create HTML?

I know this doesn’t answer your question directly, but can you avoid jbuilder entirely by using serializers? I say this because the performance will be better (here’s an example of a device serializer). This would allow you build out API-specific actions which use your serializers. No need for templates at all!

1 Like

Hi @carolync! I had a quick play with a Hanami app and tilt-jbuilder. Starting from a brand new app, here’s how I got it to work.

Hanami View + tilt-builder

I added the gem:

# Update the Gemfile
gem "tilt-jbuilder"
# Bundle the new gems
$ bundle install

I set up tilt-jbuilder in the base view class (app/view.rb):

require "hanami/view"
require "tilt/jbuilder"

Tilt.register Tilt[:jbuilder], :json

module JSONTemplates
  class View < Hanami::View
  end
end

Then I generated a test view:

$ bundle exec hanami g view posts.index

I placed a new jbuilder template alongside the default HTML template (app/templates/posts/index.json.jbuilder):

json.array! ["hello world"] do |post_title|
  json.title post_title
end

This just outputs some static content in an array.

Then I fired up bundle exec hanami console and tested the view:

json_templates[development]> puts app["views.posts.index"].call(format: :json, layout: false).to_s
[{"title":"hello world"}]
=> nil

So that seems to be a working setup.

Important note: I had to pass layout: false when calling the view. If you’re adding JSON templates to an existing view that also renders HTML within a layout, then you’ll either need pass this argument, or figure out how to set up an app/templates/layouts/app.json.jbuilder jbuilder layout that simply passes through the result of yield (which at this point is a string). I tried various approaches and couldn’t get it to work.

If your views are rendering JSON only, however, you can set config.layout = nil in your view class and then you don’t need to worry about passing layout: nil each time you call a view.

Consider yajl?

I’d actually never tried jbuilder with tilt before today! Thanks for giving me the opportunity :slight_smile: One thing I noticed is that jbuilder brings in a lot of “rails-y” gem dependencies that you might not otherwise have in your Hanami app (activesupport, actionview, rails-dom-testing, rails-html-sanitizer, etc.).

Instead of using jbuilder for JSON templates, perhaps you could consider using yajl? I’ve used this before, and IMHO it fits a lot better. Here’s how you can try it, again from a fresh Hanami app:

# Update the Gemfile
gem "yajl-ruby"
# Bundle the new gems
$ bundle install

Generate a test view:

$ bundle exec hanami g view posts.index

Create a template at app/templates/posts/index.json.yajl:

json = [
  {title: "hello world"}
]

Render it:

puts app["views.posts.index"].call(format: :json, layout: false).to_s
# [{"title":"hello world"}]

YAJL brings in far fewer dependencies compared to Jbuilder, and what I find the templating much more straightforward: all you need to do is operate on the json local variable, which is later encoded into the JSON string via the templating engine. There’s no DSL like JBuilder, it’s just plain any plain Ruby you like, which I personally prefer.

Idea: conditional layout per format

One thing you might have picked up from the yajl example above is that I also had to pass layout: false when rendering the view. I couldn’t figure out a sensble passthrough layout that would work for it either.

For yajl, this would work as an app/templates/layouts/app.json.yajl layout, but it’s inefficient, because it’s decoding the JSON from the template only for it to be re-encoded again once the layout is rendered:

json = JSON.parse(yield)

The issue here is that Hanami View will already render the view’s main template to a string before yield-ing it to the layout, and these JSON templating systems don’t really give you a nice way to work with raw strings.

So if you are building views where you’re rendering both HTML and JSON and still want to keep a layout configured, here’s a way to make it more ergonomic. You could add a #call override to your relevant base view class that determines whether or not to preserve the layout based on your given format:

# auto_register: false
# frozen_string_literal: true

require "hanami/view"

module JSONTemplates
  class View < Hanami::View
    # Disables layout when rendering with `format: :json` option
    def call(format: nil, **options)
      if format == :json
        super(**options, format:, layout: nil)
      else
        super
      end
    end
  end
end

This way you won’t have to worry about passing layout: nil every time you render with format: :json.


Thanks again for using Hanami! I hope this was helpful :slight_smile: Please feel free to ask more questions!

Thank you! This was a big help. I think the part I was missing was the format: on the render

response.render(valid_view, format: response.format)

AND also skipping the layout. I ended up putting the following in my base view, which is essentially your code with the default format changes to html instead of nil

    # Disables layout when rendering with `format: :json` option
    def call(format: :html, **options)
      if format == :json
        super(**options, format:, layout: nil)
      else
        super
      end
    end

I created a ticket to explore yajl later after the migration is complete.