cells 3.10.1 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/.travis.yml +7 -0
  2. data/CHANGES.md +18 -0
  3. data/Gemfile +1 -1
  4. data/README.md +182 -57
  5. data/TODO.md +9 -0
  6. data/cells.gemspec +3 -1
  7. data/gemfiles/Gemfile.rails4-0 +2 -1
  8. data/gemfiles/Gemfile.rails4-1 +5 -3
  9. data/lib/cell/base.rb +33 -64
  10. data/lib/cell/base/prefixes.rb +27 -0
  11. data/lib/cell/base/self_contained.rb +13 -0
  12. data/lib/cell/base/view.rb +15 -0
  13. data/lib/cell/builder.rb +52 -53
  14. data/lib/cell/caching.rb +21 -4
  15. data/lib/cell/concept.rb +85 -0
  16. data/lib/cell/rails.rb +13 -6
  17. data/lib/cell/rails/view_model.rb +47 -6
  18. data/lib/cell/rendering.rb +13 -13
  19. data/lib/cell/test_case.rb +3 -3
  20. data/lib/cell/view_model.rb +151 -0
  21. data/lib/cells.rb +0 -60
  22. data/lib/cells/rails.rb +10 -5
  23. data/lib/cells/railtie.rb +2 -5
  24. data/lib/cells/version.rb +1 -1
  25. data/lib/generators/erb/concept_generator.rb +17 -0
  26. data/lib/generators/haml/concept_generator.rb +17 -0
  27. data/lib/generators/rails/concept_generator.rb +16 -0
  28. data/lib/generators/templates/concept/cell.rb +9 -0
  29. data/lib/generators/templates/concept/view.erb +7 -0
  30. data/lib/generators/templates/concept/view.haml +4 -0
  31. data/lib/generators/trailblazer/base.rb +21 -0
  32. data/lib/generators/trailblazer/view_generator.rb +18 -0
  33. data/test/app/cells/bassist_cell.rb +1 -1
  34. data/test/app/cells/record/views/layout.haml +2 -0
  35. data/test/app/cells/record/views/show.erb +1 -0
  36. data/test/app/cells/record/views/song.erb +1 -0
  37. data/test/app/cells/song/scale.haml +1 -0
  38. data/test/cell_module_test.rb +18 -37
  39. data/test/cells_module_test.rb +1 -1
  40. data/test/concept_generator_test.rb +26 -0
  41. data/test/concept_test.rb +78 -0
  42. data/test/dummy/Rakefile +1 -1
  43. data/test/dummy/app/assets/javascripts/application.js +2 -0
  44. data/test/dummy/app/cells/album/assets/album.js +1 -0
  45. data/test/dummy/app/concepts/song/assets/songs.js +1 -0
  46. data/test/dummy/config/application.rb +11 -6
  47. data/test/dummy/label/label.gemspec +2 -2
  48. data/test/helper_test.rb +2 -2
  49. data/test/prefixes_test.rb +75 -0
  50. data/test/rails/asset_pipeline_test.rb +20 -0
  51. data/test/rails/caching_test.rb +1 -9
  52. data/test/rails/cells_test.rb +2 -17
  53. data/test/rails/forms_test.rb +2 -1
  54. data/test/rails/integration_test.rb +199 -8
  55. data/test/rails/render_test.rb +2 -10
  56. data/test/rails/view_model_test.rb +135 -60
  57. data/test/rails_helper_api_test.rb +2 -1
  58. data/test/self_contained_test.rb +9 -2
  59. data/test/test_case_test.rb +2 -2
  60. data/test/test_helper.rb +2 -3
  61. metadata +117 -33
  62. checksums.yaml +0 -7
  63. data/test/dummy/app/controllers/musician_controller.rb +0 -36
  64. data/test/rails/router_test.rb +0 -45
@@ -0,0 +1,27 @@
1
+ # TODO: merge into Rails core.
2
+ # TODO: cache _prefixes on class layer.
3
+ module Cell::Base::Prefixes
4
+ extend ActiveSupport::Concern
5
+
6
+ def _prefixes
7
+ self.class._prefixes
8
+ end
9
+
10
+ module ClassMethods
11
+ def _prefixes
12
+ return [] if abstract?
13
+ _local_prefixes + superclass._prefixes
14
+ end
15
+
16
+ def _local_prefixes
17
+ [controller_path]
18
+ end
19
+
20
+ # Instructs Cells to inherit views from a parent cell without having to inherit class code.
21
+ def inherit_views(parent)
22
+ define_method :_prefixes do
23
+ super() + parent._prefixes
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ # Enforces the new trailblazer directory layout where cells (or concepts in general) are
2
+ # fully self-contained in its own directory.
3
+ module Cell::Base::SelfContained
4
+ def self_contained!
5
+ extend Prefixes
6
+ end
7
+
8
+ module Prefixes
9
+ def _local_prefixes
10
+ super.collect { |prefix| "#{prefix}/views" }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class Cell::Base::View < ActionView::Base
2
+ def self.prepare(modules)
3
+ # TODO: remove for 4.0 if PR https://github.com/rails/rails/pull/6826 is merged.
4
+ Class.new(self) do # DISCUSS: why are we mixing that stuff into this _anonymous_ class at all? that makes things super complicated.
5
+ include *modules.reverse
6
+ end
7
+ end
8
+
9
+ def render(*args, &block)
10
+ options = args.first.is_a?(::Hash) ? args.first : {} # this is copied from #render by intention.
11
+
12
+ return controller.render(*args, &block) if options[:state] or options[:view]
13
+ super
14
+ end
15
+ end
@@ -1,72 +1,71 @@
1
1
  module Cell
2
2
  # Contains all methods for dynamically building a cell instance by using decider blocks.
3
- module Builder
3
+ #
4
+ # Design notes:
5
+ # * totally generic, doesn't know about parent_controller etc.
6
+ # * only dependency: constant.builders (I wanted to hide this from Cell::Base)
7
+ # * can easily be replaced or removed.
8
+ class Builder
9
+ def initialize(constant, exec_context) # TODO: evaluate usage of builders and implement using Uber::Options::Value.
10
+ @constant = constant
11
+ @exec_context = exec_context
12
+ @builders = @constant.builders # only dependency, must be a Cell::Base subclass.
13
+ end
14
+
4
15
  # Creates a cell instance. Note that this method calls builders which were attached to the
5
16
  # class with Cell::Base.build - this might lead to a different cell being returned.
6
- def create_cell_for(name, *args)
7
- class_from_cell_name(name).build_for(*args)
8
- end # TODO: rename to #cell_for.
9
- alias_method :cell_for, :create_cell_for
10
-
11
- def build_for(*args) # DISCUSS: remove?
17
+ def call(*args)
12
18
  build_class_for(*args).
13
- create_cell(*args)
14
- end
15
-
16
- # Adds a builder to the cell class. Builders are used in #render_cell to find out the concrete
17
- # class for rendering. This is helpful if you frequently want to render subclasses according
18
- # to different circumstances (e.g. login situations) and you don't want to place these deciders in
19
- # your view code.
20
- #
21
- # Passes the opts hash from #render_cell into the block. The block is executed in controller context.
22
- # Multiple build blocks are ORed, if no builder matches the building cell is used.
23
- #
24
- # Example:
25
- #
26
- # Consider two different user box cells in your app.
27
- #
28
- # class AuthorizedUserBox < UserInfoBox
29
- # end
30
- #
31
- # class AdminUserBox < UserInfoBox
32
- # end
33
- #
34
- # Now you don't want to have deciders all over your views - use a declarative builder.
35
- #
36
- # UserInfoBox.build do |opts|
37
- # AuthorizedUserBox if user_signed_in?
38
- # AdminUserBox if admin_signed_in?
39
- # end
40
- #
41
- # In your view #render_cell will instantiate the right cell for you now.
42
- def build(&block)
43
- builders << block
44
- end
45
-
46
- # The cell class constant for +cell_name+.
47
- def class_from_cell_name(cell_name)
48
- "#{cell_name}_cell".classify.constantize
49
- end
50
-
51
- # Override this if you want to receive arguments right in the cell constructor.
52
- def create_cell(*args)
53
19
  new(*args)
54
20
  end
55
21
 
56
22
  private
57
23
  def build_class_for(*args)
58
- builders.each do |blk|
24
+ @builders.each do |blk|
59
25
  klass = run_builder_block(blk, *args) and return klass
60
26
  end
61
- self
27
+ @constant
62
28
  end
63
29
 
64
30
  def run_builder_block(block, *args)
65
- block.call(*args)
31
+ @exec_context.instance_exec(*args, &block)
66
32
  end
67
33
 
68
- def builders
69
- @builders ||= []
70
- end
34
+
35
+ module ClassMethods
36
+ # Adds a builder to the cell class. Builders are used in #render_cell to find out the concrete
37
+ # class for rendering. This is helpful if you frequently want to render subclasses according
38
+ # to different circumstances (e.g. login situations) and you don't want to place these deciders in
39
+ # your view code.
40
+ #
41
+ # Passes the opts hash from #render_cell into the block. The block is executed in controller context.
42
+ # Multiple build blocks are ORed, if no builder matches the building cell is used.
43
+ #
44
+ # Example:
45
+ #
46
+ # Consider two different user box cells in your app.
47
+ #
48
+ # class AuthorizedUserBox < UserInfoBox
49
+ # end
50
+ #
51
+ # class AdminUserBox < UserInfoBox
52
+ # end
53
+ #
54
+ # Now you don't want to have deciders all over your views - use a declarative builder.
55
+ #
56
+ # UserInfoBox.build do |opts|
57
+ # AuthorizedUserBox if user_signed_in?
58
+ # AdminUserBox if admin_signed_in?
59
+ # end
60
+ #
61
+ # In your view #render_cell will instantiate the right cell for you now.
62
+ def build(&block)
63
+ builders << block
64
+ end
65
+
66
+ def builders
67
+ @builders ||= []
68
+ end
69
+ end # ClassMethods
71
70
  end
72
71
  end
@@ -50,9 +50,7 @@ module Cell
50
50
  key = self.class.state_cache_key(state, self.class.version_procs[state].evaluate(self, *args))
51
51
  options = self.class.cache_options.eval(state, self, *args)
52
52
 
53
- cache_store.fetch(key, options) do
54
- super(state, *args)
55
- end
53
+ fetch_from_cache_for(key, options) { super(state, *args) }
56
54
  end
57
55
 
58
56
  def cache_configured?
@@ -67,8 +65,27 @@ module Cell
67
65
  end
68
66
 
69
67
  private
68
+ def fetch_from_cache_for(key, options)
69
+ cache_store.fetch(key, options) do
70
+ yield
71
+ end
72
+ end
73
+
70
74
  def state_cached?(state)
71
75
  self.class.version_procs.has_key?(state)
72
76
  end
73
- end
77
+
78
+
79
+ module Notifications
80
+ def fetch_from_cache_for(key, options)
81
+ ActiveSupport::Notifications.instrument("read_fragment.action_controller", :key => key) do
82
+ cache_store.fetch(key, options) do
83
+ ActiveSupport::Notifications.instrument("write_fragment.action_controller", :key => key) do
84
+ yield
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end # Caching
74
91
  end
@@ -0,0 +1,85 @@
1
+ # TODO: deprecate parent_controller, ViewModel
2
+ class Cell::Concept < Cell::ViewModel
3
+ abstract!
4
+
5
+ # TODO: this should be in Helper or something. this should be the only entry point from controller/view.
6
+ class << self
7
+ def cell_for(name, controller, *args)
8
+ Cell::Builder.new(name.classify.constantize, controller).call(controller, *args)
9
+ end
10
+
11
+ def controller_path
12
+ # TODO: cache on class level
13
+ # DISCUSS: only works with trailblazer style directories. this is a bit risky but i like it.
14
+ # applies to Comment::Cell, Comment::Cell::Form, etc.
15
+ name.sub(/::Cell/, '').underscore unless anonymous?
16
+ end
17
+ end
18
+
19
+ def concept(name, *args, &block)
20
+ self.class.cell(name, parent_controller, *args, &block)
21
+ end
22
+
23
+ self_contained!
24
+
25
+ # DISCUSS: experimental, allows to render layouts from the partial view directory instead of a global one.
26
+ module Rendering
27
+ def view_renderer
28
+ @_view_renderer ||= Renderer.new(lookup_context)
29
+ end
30
+
31
+ if Cell.rails_version >= 3.2
32
+ def _normalize_layout(value) # 3.2+
33
+ value
34
+ end
35
+ else
36
+ def _normalize_options(options) # FIXME: for rails 3.1, only. in 3.2+ it's _normalize_layout.
37
+ super
38
+
39
+ if options[:layout]
40
+ options[:layout].sub!("layouts/", "")
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ class Renderer < ActionView::Renderer
47
+ def render_template(context, options) # Rails 4.0 # FIXME: make that simpler to override in rails core.
48
+ TemplateRenderer.new(@lookup_context).render(context, options)
49
+ end
50
+
51
+ def _template_renderer # Rails 3.x
52
+ @_template_renderer ||= TemplateRenderer.new(@lookup_context)
53
+ end
54
+
55
+
56
+ class TemplateRenderer < ActionView::TemplateRenderer
57
+ def render(context, options)
58
+ @options = options
59
+ super
60
+ end
61
+
62
+ def find_layout(layout, keys)
63
+ resolve_layout(layout, keys, [formats.first])
64
+ end
65
+
66
+ def resolve_layout(layout, keys, formats)
67
+ details = @details ? @details.dup : {} # FIXME: provide the entire Renderer layer here. this is to make it compatible with Rails 3.1.
68
+ details[:formats] = formats
69
+
70
+ case layout
71
+ when String
72
+ find_args = [layout, @options[:prefixes], false, keys, details]
73
+ find_args = [layout, @options[:prefixes], false, keys] if Cell.rails_version.~ 3.1
74
+ find_template(*find_args)
75
+ when Proc
76
+ resolve_layout(layout.call, keys, formats)
77
+ else
78
+ layout
79
+ end
80
+ end
81
+ end
82
+ end # Rendering
83
+
84
+ include Rendering
85
+ end
@@ -8,6 +8,12 @@ module Cell
8
8
  abstract!
9
9
  delegate :session, :params, :request, :config, :env, :url_options, :to => :parent_controller
10
10
 
11
+ class Builder < Cell::Builder
12
+ def run_builder_block(block, controller, *args)
13
+ super(block, *args)
14
+ end
15
+ end
16
+
11
17
  class << self
12
18
  def cache_store
13
19
  # FIXME: i'd love to have an initializer in the cells gem that _sets_ the cache_store attr instead of overriding here.
@@ -20,10 +26,11 @@ module Cell
20
26
  expire_cache_key_for(key, cache_store ,*args)
21
27
  end
22
28
 
23
- private
24
- # Run builder block in controller instance context.
25
- def run_builder_block(block, controller, *args)
26
- controller.instance_exec(*args, &block)
29
+ # Main entry point for instantiating cells.
30
+
31
+ def cell_for(name, controller, *args)
32
+ # FIXME: too much redundancy from Base.
33
+ Builder.new(class_from_cell_name(name), controller).call(controller, *args) # use Cell::Rails::Builder.
27
34
  end
28
35
  end
29
36
 
@@ -51,7 +58,7 @@ module Cell
51
58
  end
52
59
  end
53
60
  include DSL
54
-
55
- autoload :ViewModel, "cell/rails/view_model"
56
61
  end
57
62
  end
63
+
64
+ require "cell/rails/view_model" # TODO: remove in 4.0.
@@ -14,14 +14,53 @@ class Cell::Rails
14
14
  # properties :title, :body
15
15
  attr_reader :model
16
16
 
17
+
18
+ def self.included(*)
19
+ ActiveSupport::Deprecation.warn("The Cell::Rails::ViewModel module is deprecated and will be removed in Cells 4.0. Please inherit: `class SongCell < Cell::ViewModel`. Thanks and don't forget to smile.")
20
+ super
21
+ end
22
+
23
+
24
+
25
+ module Helpers
26
+ # DISCUSS: highest level API method. add #cell here.
27
+ def collection(name, controller, array, method=:show, builder=Cell::Rails)
28
+ # FIXME: this is the problem in Concept cells, we don't wanna call Cell::Rails.cell_for here.
29
+ array.collect { |model| builder.cell_for(name, controller, model).call(method) }.join("\n").html_safe
30
+ end
31
+
32
+ # TODO: this should be in Helper or something. this should be the only entry point from controller/view.
33
+ def cell(name, controller, *args, &block) # classic Rails fuzzy API.
34
+ if args.first.is_a?(Hash) and array = args.first[:collection]
35
+ return collection(name, controller, array)
36
+ end
37
+
38
+ Cell::Rails.cell_for(name, controller, *args, &block)
39
+ end
40
+ end
41
+ extend Helpers # FIXME: do we really need ViewModel::cell/::collection ?
42
+
43
+
17
44
  module ClassMethods
18
45
  def property(*names)
19
46
  delegate *names, :to => :model
20
47
  end
48
+
49
+ include Helpers
21
50
  end
22
51
  extend ActiveSupport::Concern
23
52
 
24
53
 
54
+ def cell(name, *args)
55
+ self.class.cell(name, parent_controller, *args)
56
+ end
57
+
58
+
59
+ def initialize(*args)
60
+ super
61
+ _prepare_context # happens in AV::Base at the bottom.
62
+ end
63
+
25
64
  def render(options={})
26
65
  if options.is_a?(Hash)
27
66
  options.reverse_merge!(:view => state_for_implicit_render)
@@ -32,8 +71,10 @@ class Cell::Rails
32
71
  super
33
72
  end
34
73
 
35
- def call
36
- render implicit_state
74
+ def call(state=:show)
75
+ # it is ok to call to_s.html_safe here as #call is a defined rendering method.
76
+ # DISCUSS: IN CONCEPT: render( view: implicit_state)
77
+ render_state(state).to_s.html_safe
37
78
  end
38
79
 
39
80
  private
@@ -45,9 +86,9 @@ class Cell::Rails
45
86
  caller[1].match(/`(\w+)/)[1]
46
87
  end
47
88
 
48
- def implicit_state
49
- controller_path.split("/").last
50
- end
89
+ # def implicit_state
90
+ # controller_path.split("/").last
91
+ # end
51
92
  end
52
93
 
53
94
 
@@ -109,7 +150,7 @@ class Cell::Rails
109
150
  end
110
151
 
111
152
  # FIXME: fix that in rails core.
112
- if Cell.rails4_0?
153
+ if Cell.rails_version.~("4.0", "4.1")
113
154
  include LinkToHelper
114
155
  else
115
156
  include ActionView::Helpers::UrlHelper
@@ -1,12 +1,12 @@
1
1
  module Cell
2
2
  module Rendering
3
3
  extend ActiveSupport::Concern
4
-
4
+
5
5
  # Invoke the state method for +state+ which usually renders something nice.
6
6
  def render_state(state, *args)
7
7
  process(state, *args)
8
8
  end
9
-
9
+
10
10
  # Renders the view for the current state and returns the markup.
11
11
  # Don't forget to return the markup itself from the state method.
12
12
  #
@@ -18,7 +18,7 @@ module Cell
18
18
  # +:inline+:: Renders an inline template as state view. See ActionView::Base#render for details.
19
19
  # +:file+:: Specifies the name of the file template to render.
20
20
  # +:nothing+:: Doesn't invoke the rendering process.
21
- # +:state+:: Instantly invokes another rendering cycle for the passed state and returns. You may pass arbitrary state-args to the called state.
21
+ # +:state+:: Instantly invokes another rendering cycle for the passed state and returns. You may pass arbitrary state-args to the called state.
22
22
  # +:format+:: Sets a different template format, e.g. +:json+. Use this option with caution as it currently modifies the global format variable. This might lead to unexpected subsequent render behaviour due to a design flaw in Rails.
23
23
  #
24
24
  # Example:
@@ -56,7 +56,7 @@ module Cell
56
56
  #
57
57
  # === Using states instead of helpers
58
58
  #
59
- # Sometimes it's useful to not only render a view but also invoke the associated state. This is
59
+ # Sometimes it's useful to not only render a view but also invoke the associated state. This is
60
60
  # especially helpful when replacing helpers. Do that with <tt>render :state</tt>.
61
61
  #
62
62
  # def show_cheap_item(item)
@@ -72,34 +72,34 @@ module Cell
72
72
  def render(*args)
73
73
  render_view_for(self.action_name, *args)
74
74
  end
75
-
75
+
76
76
  private
77
77
  # Renders the view belonging to the given state. Will raise ActionView::MissingTemplate
78
78
  # if it can't find a view.
79
79
  def render_view_for(state, *args)
80
80
  opts = args.first.is_a?(::Hash) ? args.shift : {}
81
-
81
+
82
82
  return "" if opts[:nothing]
83
-
83
+
84
84
  if opts[:state]
85
85
  opts[:text] = render_state(opts.delete(:state), *args)
86
86
  elsif (opts.keys & [:text, :inline, :file]).blank?
87
87
  process_opts_for(opts, state)
88
88
  end
89
-
89
+
90
90
  render_to_string(opts).html_safe # ActionView::Template::Text doesn't do that for us.
91
91
  end
92
-
93
-
92
+
93
+
94
94
  module ClassMethods
95
95
  # Main entry point for #render_cell.
96
96
  def render_cell_for(name, state, *args)
97
- cell = create_cell_for(name, *args)
97
+ cell = cell_for(name, *args)
98
98
  yield cell if block_given?
99
-
99
+
100
100
  render_cell_state(cell, state, *args)
101
101
  end
102
-
102
+
103
103
  private
104
104
  def render_cell_state(cell, state, *args)
105
105
  cell.render_state(state, *args)