Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server-side template rendering with Handlebars & HandlebarsAssets. #103

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
8 changes: 6 additions & 2 deletions lib/handlebars_assets/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module HandlebarsAssets
module Config
extend self

attr_writer :compiler, :compiler_path, :ember, :multiple_frameworks,
attr_writer :compiler, :compiler_runtime, :compiler_path, :ember, :multiple_frameworks,
:haml_options, :known_helpers, :known_helpers_only, :options,
:patch_files, :patch_path, :path_prefix, :slim_options, :template_namespace,
:precompile, :haml_enabled, :slim_enabled,
Expand All @@ -17,6 +17,10 @@ def compiler
@compiler || 'handlebars.js'
end

def compiler_runtime
@compiler_runtime || 'handlebars.runtime.js'
end

def self.configure
yield self
end
Expand Down Expand Up @@ -94,7 +98,7 @@ def template_namespace
end

def handlebars_extensions
@hbs_extensions ||= ['hbs', 'handlebars']
@hbs_extensions ||= %w(hbs handlebars)
end

def hamlbars_extensions
Expand Down
1 change: 0 additions & 1 deletion lib/handlebars_assets/handlebars.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
module HandlebarsAssets
class Handlebars
class << self

def precompile(*args)
context.call('Handlebars.precompile', *args)
end
Expand Down
29 changes: 29 additions & 0 deletions lib/handlebars_assets/server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'handlebars_assets'

%w(responder controller handlers/handlebars extensions).each do |lib|
require File.join('handlebars_assets', 'server', lib)
end

module HandlebarsAssets
module Server
extend self
HANDLER = HandlebarsAssets::Server::Handlers::Handlebars

def self.register_template_handlers
Config.handlebars_extensions.each do |ext|
ActionView::Template.register_template_handler(ext, HANDLER)
end
end

def self.register_mime_types
Config.handlebars_extensions.each do |ext|
Mime::Type.register_alias 'text/html', ext.to_sym
end
end
end
end

if defined?(Rails)
::HandlebarsAssets::Server.register_template_handlers
::HandlebarsAssets::Server.register_mime_types
end
61 changes: 61 additions & 0 deletions lib/handlebars_assets/server/controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module HandlebarsAssets
module Server
# Prepare a controller to use <code>respond_with</code>
# to render a Handlebars template (for GET requests, currently)
#
# You must require 'handlebar_assets/server'
# in your controller, as it is not loaded by default.
#
# require 'handlebars_assets/server'
#
# class MyApp < ActionController::Base
# include HandlebarsAssets::Server::Controller
#
# respond_to :hbs # or :handlebars
#
# def show
# @bike = Bike.find(params[:id])
# render_with(@bike) # Look ma, no 'to_hbs'!
# end
# end
#
# Your class must define #to_hbs (or #to_handlebars)
# that returns a Hash of JSON-compatible objects.
#
# You can specify the location of your templates:
#
# handlebars_templates path: 'app/assets/...'
#
# -or-
#
# handlebars_templates paths: ['array', 'of', 'paths']
#
# 'app/assets/javascripts/templates' is prepended on
# include, so you do not need to add this line
# if you keep your templates in that directory
#
# If you support other mime types (like JSON), in the same
# controller, you'll need to give your template a 'js' or
# 'jst' format in order to keep Rails from trying to use
# the view for everything:
#
# show.js.hbs # *not* show.hbs or show.html.hbs
#
module Controller
extend ActiveSupport::Concern

DEFAULT_HBS_TEMPLATE_PATH = 'app/assets/javascripts/templates'

included do
self.responder = HandlebarsAssets::Server::Responder

def self.handlebars_templates(options = {})
paths = options[:paths] || options[:path] || []
Array(paths).reverse_each { |p| prepend_view_path(p) }
end

handlebars_templates path: DEFAULT_HBS_TEMPLATE_PATH
end
end
end
end
30 changes: 30 additions & 0 deletions lib/handlebars_assets/server/extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module HandlebarsAssets
module Server
module Extensions
def context_for(template, extra = '')
tmpl = "var template = Handlebars.template(#{precompile(template)})"
context = [runtime, extra, tmpl].join('; ')

ExecJS.compile(context)
end

def render(template, *args)
locals = args.last.is_a?(Hash) ? args.pop : {}
extra = args.first.to_s
context_for(template, extra).call('template', locals)
end

protected

def runtime
@runtime ||= runtime_path.read
end

def runtime_path
@runtime_path ||= assets_path.join(HandlebarsAssets::Config.compiler_runtime)
end
end
end
end

::HandlebarsAssets::Handlebars.send(:extend, HandlebarsAssets::Server::Extensions)
31 changes: 31 additions & 0 deletions lib/handlebars_assets/server/handlers/handlebars.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module HandlebarsAssets
module Server
module Handlers
class Handlebars
def self.call(template)
new.call(template)
end

def call(template)
if template.locals.blank?
"#{template.source.inspect}.html_safe"
else
<<-HBS
variable_names = controller.instance_variable_names
variable_names -= %w[@template]
if controller.respond_to?(:protected_instance_variables)
variable_names -= controller.protected_instance_variables
end
variable_names.reject! { |name| name.starts_with? '@_' }

variables = variable_names.inject({}) { |acc,name| acc[name.sub(/^@/, "")] = controller.instance_variable_get(name); acc }
variables.merge!(local_assigns)

HandlebarsAssets::Handlebars.render(#{template.source.inspect}, variables).html_safe
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing I didn't really like about this is that the JS context has already compiled this template (especially if precompile is on); I tried hooking this into the sprockets side but it really is a hack as well... I am not sure of the runtime speed of a view like this (which is the thing I was trying to make better).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree about your speed concerns. For my purposes, in any case, the HTML is a fallback for generating pages to be consumed by clients that don't support Javascript (GoogleBot, I'm looking at you.)

The controller I've implemented it within also serves JSON that the web app uses with Handlebars to render on the client side, which is much better overall. I'll be caching the hell out of 'static' HTML generated, and expect the usage of the HTML-only versions to be rather low.

Given the numbers I'm seeing from generating the HTML server side on my development box (200-500ms in views), there's definitely a lot of room to improve before I'd recommend using it for any large scale system without an effective caching strategy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only other concern which I thought of last night (but had already been on the way to bed) was that the context might not be thread safe.

HBS
end
end
end
end
end
end
44 changes: 44 additions & 0 deletions lib/handlebars_assets/server/responder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module HandlebarsAssets
module Server
# Provides the requisite #to_{format} methods a Responder needs
# to handle <code>respond_with</code>
class Responder < ActionController::Responder
# Ask the resource to give a HandleBars-compatible
# representation and then passes it back to
# <code>render</code> with the appropriate new options.
#
# Add the HBS-compatible version of the resource
# to the locals hash, and ensure [:js, :jst] is in
# the list of requested formats.
def to_handlebars
display resource, resource_options if resourceful?
end
alias_method :to_hbs, :to_handlebars

private

# Aggregate the options for displaying this resource
# and return the new Hash.
def resource_options
{ locals: resource_locals, formats: resource_formats }
end

# Merge any user-supplied locals with the formatted resource
# and return the Hash
def resource_locals
(options.delete(:locals) || {}).merge!(resource.send(:"to_#{format}"))
end

# Merge any user-supplied formats with our
# supported format(s) and return the new Array.
def resource_formats
supported_formats | (options.delete(:formats) || [])
end

# :nodoc:
def supported_formats
[:js, :jst]
end
end
end
end