cells 3.10.1 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)