Last active
August 29, 2015 14:04
-
-
Save mcmire/332010f7013d51a4660d to your computer and use it in GitHub Desktop.
Like DecentExposure, but less magical
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is a module that provides two methods, `expose` and `let`, which | |
# allow you to cleanly and clearly define lazily-evaluated, memoized | |
# values that you can use anywhere in your controllers and views. It's | |
# very similar to the decent_exposure[1] gem, but it works more simply | |
# (and doesn't have a default action when you don't pass a block to | |
# #expose, to keep things very very simple). | |
# | |
# You will want to mix this into ApplicationController. | |
# | |
# == Rationale | |
# | |
# Oftentimes you will need to share some value between controller actions, | |
# or set the value in the controller and propagate it to the view so you | |
# can use it there. The conventional way to solve this is by setting an | |
# instance variable in a before filter. However this has some problems: | |
# | |
# * Rails is magical when it comes to controllers. Instance variables | |
# propagate to views? They propagate to partials too? Really? How does | |
# that work? | |
# * Rendering a template that relies on an instance variable whose name | |
# does not match the action name (for instance, rendering 'new' from the | |
# create action) is a pain because you have to remember to include | |
# :create in the before_filter, or call the before_filter manually. | |
# * Partials that rely on instance variables are a pain to share between | |
# different controllers because you have to remember to copy over the | |
# before_filters. | |
# * If an instance variable set in before_filter A depends on the instance | |
# variable set in before_filter B, you must remember to place | |
# before_filter A *before* B, otherwise your app will break. If you | |
# have a lot of dependencies then this gets pretty hairy. | |
# | |
# The `expose` method this module provides solves these problems: | |
# | |
# * `expose` simply makes a (memoized) helper method which the view has | |
# access to. There's no magic involved -- you can figure out how it | |
# works very easily. | |
# * Because the value returned by your `expose` is not evaluated until you | |
# request it, you are free to refer to other `expose`d values -- there | |
# is no need for certain `expose`s to be defined before others. | |
# * Sharing `expose`d values between controllers is easy -- just stuff | |
# the `expose`s in a module and share it. | |
# * Sharing these values between actions is easy too -- you've already | |
# defined them in your controller, so it's dead simple to use them in | |
# any view, including partials -- they're already available. | |
# * Having a list of `expose`d values makes your controller more OO -- it | |
# says, "here is a list of values that I as a controller am exposing" | |
# and it lets other developers know what values they are free to use in | |
# views. | |
# | |
# [1]: http://github.com/voxdolo/decent_exposure | |
# | |
module LimitedExposure | |
extend ActiveSupport::Concern | |
module ClassMethods | |
# Public: Declare a controller variable which will also be available in | |
# views. | |
# | |
# The variable is implemented as a memoized controller method, and may | |
# have an optional value (evaluated at call time). A writer method is | |
# also created so the value can be overridden at runtime. A helper | |
# method is also created. | |
# | |
# name - The Symbol name of the method. | |
# block - Should return the initial value of the variable. When the | |
# method is called, the block will be evaluated and its return | |
# value will be cached. If the block is omitted, the method | |
# and variable return nothing. | |
# | |
# Example | |
# | |
# # === Controller === | |
# | |
# expose(:posts) { Post.all } | |
# expose(:post) | |
# def new | |
# self.post = Post.new | |
# end | |
# def edit | |
# self.post = Post.find(params[:id]) | |
# end | |
# | |
# # === View ('new' action) === | |
# | |
# <%= posts.inspect %> <%# "[<Post id: 1, ...>, <Post id: 2, ...>]" %%> | |
# <%= post.persisted? %> <%# false %> | |
# | |
# # === View ('edit' action) === | |
# | |
# <%= posts.inspect %> <%# "[<Post id: 1, ...>, <Post id: 2, ...>]" %> | |
# <%= post.persisted? %> <%# true %> | |
# | |
def expose(name, options={}, &block) | |
let(name, &block) | |
helper_method name | |
end | |
# Public: Declare a memoized controller variable. | |
# | |
# The variable is implemented as a memoized controller method, and may | |
# have an optional value (evaluated at call time). A writer method is | |
# also created so the value can be overridden at runtime. No helper | |
# method is created. | |
# | |
# name - The Symbol name of the method. | |
# block - Should return the initial value of the variable. When the | |
# method is called, the block will be evaluated and its | |
# return value will be cached. If the block is omitted, the | |
# method and variable return nothing. | |
# | |
# Examples | |
# | |
# # 1. Declare a memoized variable with a value | |
# let(:foo) { some_expensive_operation } | |
# def some_action | |
# foo #=> (result of some_expensive_operation) | |
# foo #=> (result from the last call) | |
# end | |
# | |
# # 2. Declare a memoized variable with a value, then override it | |
# let(:foo) { some_expensive_operation } | |
# def some_action | |
# self.foo = "bar" | |
# foo #=> "bar" | |
# end | |
# | |
# # 3. Declare an undefined variable set in the action | |
# let(:foo) | |
# def some_action | |
# foo #=> nil | |
# self.foo = "bar" | |
# foo #=> "bar" | |
# end | |
# | |
# Inspiration | |
# | |
# * <http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html> | |
# * <http://gist.github.com/453389> | |
# | |
def let(name, options={}, &block) | |
name = name.to_sym | |
define_method(name) do | |
__memoized[name] ||= (instance_eval(&block) if block) | |
end | |
define_method("#{name}=") do |value| | |
__memoized[name] = value | |
end | |
# Don't make this protected so that we can easily refer to this | |
# method in tests | |
hide_action name, "#{name}=" if respond_to?(:hide_action) | |
end | |
end | |
def __memoized | |
@__memoized ||= {} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment