├── test
├── fixtures
│ ├── bassist
│ │ └── play.erb
│ ├── song
│ │ ├── with_html.erb
│ │ ├── ivar.erb
│ │ ├── show.erb
│ │ ├── with_block.erb
│ │ ├── with_locals.erb
│ │ └── with_erb.erb
│ ├── concepts
│ │ └── record
│ │ │ └── views
│ │ │ ├── song.erb
│ │ │ ├── show.erb
│ │ │ └── layout.erb
│ ├── inherit_views_test
│ │ ├── tapper
│ │ │ ├── play.erb
│ │ │ └── tap.erb
│ │ └── popper
│ │ │ └── tap.erb
│ ├── song_with_layout
│ │ ├── show.erb
│ │ ├── show_with_layout.erb
│ │ ├── happy.erb
│ │ └── merry.erb
│ ├── partials
│ │ ├── _show.html.erb
│ │ └── _show.xml.erb
│ ├── partial_test
│ │ └── with_partial
│ │ │ └── show.erb
│ ├── cell_test
│ │ └── song
│ │ │ └── show_with_block.erb
│ ├── comment
│ │ ├── show
│ │ │ └── show.erb
│ │ └── layout
│ │ │ └── show.erb
│ ├── templates_caching_test
│ │ └── song
│ │ │ └── show.erb
│ └── url_helper_test
│ │ └── song
│ │ ├── with_block.erb
│ │ ├── with_capture.erb
│ │ ├── with_link_to.erb
│ │ ├── with_form_for_block.erb
│ │ ├── with_content_tag.erb
│ │ └── edit.erb
├── test_helper.rb
├── cell_test.rb
├── twin_test.rb
├── cell_benchmark.rb
├── templates_test.rb
├── context_test.rb
├── partial_test.rb
├── inspect_test.rb
├── property_test.rb
├── builder_test.rb
├── testing_test.rb
├── layout_test.rb
├── render_test.rb
├── concept_test.rb
├── cache_test.rb
├── public_test.rb
└── prefixes_test.rb
├── lib
├── cells.rb
├── cell
│ ├── version.rb
│ ├── abstract.rb
│ ├── development.rb
│ ├── self_contained.rb
│ ├── builder.rb
│ ├── twin.rb
│ ├── partial.rb
│ ├── util.rb
│ ├── concept.rb
│ ├── inspect.rb
│ ├── templates.rb
│ ├── prefixes.rb
│ ├── escaped.rb
│ ├── option.rb
│ ├── layout.rb
│ ├── collection.rb
│ ├── testing.rb
│ ├── caching.rb
│ └── view_model.rb
├── tasks
│ └── cells.rake
└── cell.rb
├── .gitignore
├── Gemfile
├── TODO.md
├── .github
└── workflows
│ └── ci.yml
├── Rakefile
├── cells.gemspec
├── README.md
└── CHANGES.md
/test/fixtures/bassist/play.erb:
--------------------------------------------------------------------------------
1 | Doo
--------------------------------------------------------------------------------
/test/fixtures/song/with_html.erb:
--------------------------------------------------------------------------------
1 |
Yew!
--------------------------------------------------------------------------------
/test/fixtures/song/ivar.erb:
--------------------------------------------------------------------------------
1 | <%= @title %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/song/show.erb:
--------------------------------------------------------------------------------
1 | <%= title %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/concepts/record/views/song.erb:
--------------------------------------------------------------------------------
1 | Lalala
--------------------------------------------------------------------------------
/lib/cells.rb:
--------------------------------------------------------------------------------
1 | require "cell/version"
2 | require "cell"
3 |
--------------------------------------------------------------------------------
/test/fixtures/inherit_views_test/tapper/play.erb:
--------------------------------------------------------------------------------
1 | Dooom!
--------------------------------------------------------------------------------
/test/fixtures/song/with_block.erb:
--------------------------------------------------------------------------------
1 | Yo! <%= yield %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/song_with_layout/show.erb:
--------------------------------------------------------------------------------
1 | <%= title %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/song_with_layout/show_with_layout.erb:
--------------------------------------------------------------------------------
1 | Friday
--------------------------------------------------------------------------------
/test/fixtures/inherit_views_test/tapper/tap.erb:
--------------------------------------------------------------------------------
1 | Tap tap tap!
--------------------------------------------------------------------------------
/test/fixtures/partials/_show.html.erb:
--------------------------------------------------------------------------------
1 | I Am Wrong And I Am Right
--------------------------------------------------------------------------------
/lib/cell/version.rb:
--------------------------------------------------------------------------------
1 | module Cell
2 | VERSION = "4.5.0"
3 | end
4 |
--------------------------------------------------------------------------------
/test/fixtures/concepts/record/views/show.erb:
--------------------------------------------------------------------------------
1 | Party on, <%= model %>!
--------------------------------------------------------------------------------
/test/fixtures/partial_test/with_partial/show.erb:
--------------------------------------------------------------------------------
1 | Adenosine Breakdown
--------------------------------------------------------------------------------
/test/fixtures/song_with_layout/happy.erb:
--------------------------------------------------------------------------------
1 | <%= "Happy #{yield}!" %>
--------------------------------------------------------------------------------
/test/fixtures/cell_test/song/show_with_block.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/comment/show/show.erb:
--------------------------------------------------------------------------------
1 | $show.erb, <%= context.inspect %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/concepts/record/views/layout.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/test/fixtures/partials/_show.xml.erb:
--------------------------------------------------------------------------------
1 | I Am Wrong And I Am Right
--------------------------------------------------------------------------------
/test/fixtures/song/with_locals.erb:
--------------------------------------------------------------------------------
1 | <%= title %>
2 | <%= length %>
3 |
--------------------------------------------------------------------------------
/test/fixtures/song_with_layout/merry.erb:
--------------------------------------------------------------------------------
1 | <%= "Merry #{what}, #{yield}" %>
--------------------------------------------------------------------------------
/test/fixtures/templates_caching_test/song/show.erb:
--------------------------------------------------------------------------------
1 | The Great Mind Eraser
--------------------------------------------------------------------------------
/test/fixtures/inherit_views_test/popper/tap.erb:
--------------------------------------------------------------------------------
1 | TTttttap I'm not good enough!
--------------------------------------------------------------------------------
/test/fixtures/url_helper_test/song/with_block.erb:
--------------------------------------------------------------------------------
1 | Nice!
2 | <%= cap { "yeah" } %>
--------------------------------------------------------------------------------
/test/fixtures/comment/layout/show.erb:
--------------------------------------------------------------------------------
1 | $layout.erb{<%= yield %>, <%= context.inspect %>}
2 |
--------------------------------------------------------------------------------
/test/fixtures/song/with_erb.erb:
--------------------------------------------------------------------------------
1 | ERB:
2 | <%= content_tag(:span) do %>
3 | <%= title %>
4 | <% end %>
--------------------------------------------------------------------------------
/test/fixtures/url_helper_test/song/with_capture.erb:
--------------------------------------------------------------------------------
1 | Nice!
2 | <%= capture do %>
3 | <%= "Great!" %>
4 | <% end %>
--------------------------------------------------------------------------------
/test/fixtures/url_helper_test/song/with_link_to.erb:
--------------------------------------------------------------------------------
1 | <%= link_to songs_path do %>
2 | <%= image_tag "all.png" %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | pkg
3 | *.gem
4 | .*~
5 | .bundle
6 | *.lock
7 | test/dummy/log/
8 | test/dummy/tmp/
9 | /.rvmrc
10 |
--------------------------------------------------------------------------------
/test/fixtures/url_helper_test/song/with_form_for_block.erb:
--------------------------------------------------------------------------------
1 | <%= form_for(OpenStruct.new, url: "/songs", as: :song) do |f| %>
2 | <%= f.text_field :id %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/lib/cell/abstract.rb:
--------------------------------------------------------------------------------
1 | module Cell::Abstract
2 | def abstract!
3 | @abstract = true
4 | end
5 |
6 | def abstract?
7 | @abstract if defined?(@abstract)
8 | end
9 | end
--------------------------------------------------------------------------------
/lib/tasks/cells.rake:
--------------------------------------------------------------------------------
1 | require "rake/testtask"
2 |
3 | namespace "test" do
4 | Rake::TestTask.new(:cells) do |t|
5 | t.libs << "test"
6 | t.pattern = 'test/cells/**/*_test.rb'
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/fixtures/url_helper_test/song/with_content_tag.erb:
--------------------------------------------------------------------------------
1 | <%= content_tag :span do %>
2 | "Title:"
3 | <%= content_tag :div do %>
4 | <%= "Still Knee Deep" %>
5 | <% end %>
6 | <%- end -%>
7 |
--------------------------------------------------------------------------------
/lib/cell/development.rb:
--------------------------------------------------------------------------------
1 | module Cell
2 | module Development
3 | def self.included(base)
4 | base.instance_eval do
5 | def templates
6 | Templates.new
7 | end
8 | end
9 | end
10 | end
11 | end
--------------------------------------------------------------------------------
/test/fixtures/url_helper_test/song/edit.erb:
--------------------------------------------------------------------------------
1 | <%= form_tag "/songs" do %>
2 | <%= label_tag :title %>
3 | <%= text_field_tag :title %>
4 |
5 | <%= content_tag :ul do %>
6 | <%= content_tag :li, "Airplays: 1" %>
7 | <%= end %>
8 | <%= end %>
9 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "benchmark-ips"
4 | gem "minitest-line"
5 |
6 | gemspec
7 |
8 | case ENV["GEMS_SOURCE"]
9 | when "local"
10 | gem "cells-erb", path: "../cells-erb"
11 | # gem "erbse", path: "../erbse"
12 | when "github"
13 | gem "cells-erb", github: "trailblazer/cells-erb"
14 | end
15 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require "minitest/autorun"
2 | require "cells"
3 | require "cells-erb"
4 |
5 | Cell::ViewModel.send(:include, Cell::Erb) if Cell.const_defined?(:Erb) # FIXME: should happen in inititalizer.
6 |
7 | MiniTest::Spec.class_eval do
8 | include Cell::Testing
9 | end
10 |
11 | class BassistCell < Cell::ViewModel
12 | self.view_paths = ['test/fixtures']
13 | end
14 |
--------------------------------------------------------------------------------
/lib/cell/self_contained.rb:
--------------------------------------------------------------------------------
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::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
--------------------------------------------------------------------------------
/lib/cell/builder.rb:
--------------------------------------------------------------------------------
1 | require "declarative/builder"
2 |
3 | module Cell
4 | module Builder
5 | def self.included(base)
6 | base.send :include, Declarative::Builder
7 | base.extend ClassMethods
8 | end
9 |
10 | module ClassMethods
11 | def build(*args)
12 | build!(self, *args).new(*args) # Declarative::Builder#build!.
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # 4.0
2 |
3 | * Add tests for with_assets config
4 |
5 |
6 | * Get rid of the annoying `ActionController` dependency that needs to be passed into each cell. We only need it for "contextual links", when people wanna link to the same page. Make them pass in a URL generator object as a normal argument instead.
7 | * Generated cells will be view models per default.
8 | * Introduce Composition as in Reform, Representable, etc, when passing in a hash.
9 | ```ruby
10 | include Composition
11 | property :id, on: :comment
12 | ```
--------------------------------------------------------------------------------
/test/cell_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CellTest < MiniTest::Spec
4 | class SongCell < Cell::ViewModel
5 | self.view_paths = ['test/fixtures']
6 |
7 | def show
8 | end
9 |
10 | def show_with_block(&block)
11 | render(&block)
12 | end
13 | end
14 |
15 | # #options
16 | it { _(SongCell.new(nil, genre: "Punkrock").send(:options)[:genre]).must_equal "Punkrock" }
17 |
18 | # #block
19 | it { _(SongCell.new(nil, genre: "Punkrock").(:show_with_block) { "hello" }).must_equal "hello\n" }
20 | end
21 |
--------------------------------------------------------------------------------
/lib/cell/twin.rb:
--------------------------------------------------------------------------------
1 | require 'disposable/twin'
2 |
3 | module Cell
4 | module Twin
5 | def self.included(base)
6 | base.send :include, Disposable::Twin::Builder
7 | base.extend ClassMethods
8 | end
9 |
10 | module ClassMethods
11 | def twin(twin_class)
12 | super(twin_class) { |dfn| property dfn.name } # create readers to twin model.
13 | end
14 | end
15 |
16 | def initialize(model, options={})
17 | super(build_twin(model, options), controller: options.delete(:controller))
18 | end
19 | end
20 | end
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | strategy:
6 | fail-fast: false
7 | matrix:
8 | # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
9 | ruby: [2.5, 2.6, 2.7, '3.0', 3.1, head]
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: ruby/setup-ruby@v1
14 | with:
15 | ruby-version: ${{ matrix.ruby }}
16 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
17 | - run: bundle exec rake
18 |
--------------------------------------------------------------------------------
/lib/cell/partial.rb:
--------------------------------------------------------------------------------
1 | # Allows to render global partials, for example.
2 | #
3 | # render partial: "../views/shared/container"
4 | module Cell::ViewModel::Partial
5 | def process_options!(options)
6 | super
7 | return unless partial = options[:partial]
8 |
9 | parts = partial.split("/")
10 | view = parts.pop
11 | view = "_#{view}"
12 | view += ".#{options[:formats].first}" if options[:formats]
13 | prefixes = self.class.view_paths.collect { |path| ([path] + parts).join("/") }
14 |
15 | options.merge!(view: view, prefixes: prefixes)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/twin_test.rb:
--------------------------------------------------------------------------------
1 | # require 'test_helper'
2 | # require 'cell/twin'
3 |
4 | # class TwinTest < MiniTest::Spec
5 | # class SongCell < Cell::ViewModel
6 | # class Twin < Disposable::Twin
7 | # property :title
8 | # option :online?
9 | # end
10 |
11 | # include Cell::Twin
12 | # twin Twin
13 |
14 | # def show
15 | # "#{title} is #{online?}"
16 | # end
17 |
18 | # def title
19 | # super.downcase
20 | # end
21 | # end
22 |
23 | # let (:model) { OpenStruct.new(title: "Kenny") }
24 |
25 | # it { SongCell.new( model, :online? => true).call.must_equal "kenny is true" }
26 | # end
27 |
--------------------------------------------------------------------------------
/lib/cell/util.rb:
--------------------------------------------------------------------------------
1 | module Cell::Util
2 | def util
3 | Inflector
4 | end
5 |
6 | class Inflector
7 | # copied from ActiveSupport.
8 | def self.underscore(constant)
9 | constant.gsub(/::/, '/').
10 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
11 | gsub(/([a-z\d])([A-Z])/,'\1_\2').
12 | tr("-", "_").
13 | downcase
14 | end
15 |
16 | # WARNING: this API might change.
17 | def self.constant_for(name)
18 | class_name = name.split("/").collect do |part|
19 | part.split('_').collect(&:capitalize).join
20 | end.join('::')
21 |
22 | Object.const_get(class_name, false)
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/cell.rb:
--------------------------------------------------------------------------------
1 | require "tilt"
2 | require "uber/inheritable_attr"
3 |
4 | module Cell
5 | autoload :Testing, "cell/testing"
6 |
7 | class TemplateMissingError < RuntimeError
8 | def initialize(prefixes, view)
9 | super("Template missing: view: `#{view.to_s}` prefixes: #{prefixes.inspect}")
10 | end
11 | end # Error
12 | end
13 |
14 | require "cell/caching"
15 | require "cell/prefixes"
16 | require "cell/layout"
17 | require "cell/templates"
18 | require "cell/abstract"
19 | require "cell/util"
20 | require "cell/inspect"
21 | require "cell/view_model"
22 | require "cell/concept"
23 | require "cell/escaped"
24 | require "cell/builder"
25 | require "cell/collection"
26 |
--------------------------------------------------------------------------------
/test/cell_benchmark.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'benchmark'
3 |
4 | Song = Struct.new(:title)
5 |
6 |
7 | class SongCell < Cell::ViewModel
8 | self.view_paths = ['test']
9 | property :title
10 |
11 | def show
12 | render
13 | end
14 | end
15 |
16 | time = Benchmark.measure do
17 | Cell::ViewModel.cell(:song, nil, collection: 1000.times.collect { Song.new("Anarchy Camp") })
18 | end
19 |
20 | puts time
21 |
22 | # 4.0
23 | # 0.310000 0.010000 0.320000 ( 0.320382)
24 |
25 | # no caching of templates, puts
26 | # 0.570000 0.030000 0.600000 ( 0.600160)
27 |
28 | # caching of templates
29 | # 0.090000 0.000000 0.090000 ( 0.085652)
30 |
31 | # wed, 17.
32 | # 0.120000 0.010000 0.130000 ( 0.127731)
33 |
--------------------------------------------------------------------------------
/lib/cell/concept.rb:
--------------------------------------------------------------------------------
1 | require "cell/self_contained"
2 |
3 | module Cell
4 | # Cell::Concept is no longer under active development. Please switch to Trailblazer::Cell.
5 | class Concept < Cell::ViewModel
6 | abstract!
7 | self.view_paths = ["app/concepts"]
8 | extend SelfContained
9 |
10 | # TODO: this should be in Helper or something. this should be the only entry point from controller/view.
11 | class << self
12 | def class_from_cell_name(name)
13 | util.constant_for(name)
14 | end
15 |
16 | def controller_path
17 | @controller_path ||= util.underscore(name.sub(/(::Cell$|Cell::)/, ''))
18 | end
19 | end
20 |
21 | alias_method :concept, :cell
22 |
23 | self_contained!
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/cell/inspect.rb:
--------------------------------------------------------------------------------
1 | module Cell
2 | module Inspect
3 | def inspect
4 | if inspect_blacklist.any?
5 | build_inspect_s
6 | else
7 | super
8 | end
9 | end
10 |
11 | private
12 |
13 | def build_inspect_s
14 | ivars = Hash[self.instance_variables.map { |name| [name[1..-1], self.instance_variable_get(name)] }]
15 |
16 | ivars_s = ivars.map do |name, value|
17 | if inspect_blacklist.include?(name)
18 | "@#{name}=#<#{value.class.name}:#{value.object_id}>"
19 | else
20 | "<@#{name}=#{value.inspect}>"
21 | end
22 | end.join(', ')
23 |
24 | "#<#{self.class.name}:#{self.object_id} #{ivars_s}>"
25 | end
26 |
27 | def inspect_blacklist
28 | []
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/cell/templates.rb:
--------------------------------------------------------------------------------
1 | module Cell
2 | # Gets cached in production.
3 | class Templates
4 | # prefixes could be instance variable as they will never change.
5 | def [](prefixes, view, options)
6 | find_template(prefixes, view, options)
7 | end
8 |
9 | private
10 | def cache
11 | @cache ||= Tilt::Cache.new
12 | end
13 |
14 | def find_template(prefixes, view, options) # options is not considered in cache key.
15 | cache.fetch(prefixes, view) do
16 | template_prefix = prefixes.find { |prefix| File.exist?("#{prefix}/#{view}") }
17 | return if template_prefix.nil? # We can safely return early. Tilt::Cache does not cache nils.
18 | template_class = options.delete(:template_class)
19 | template_class.new("#{template_prefix}/#{view}", options) # Tilt.new()
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/cell/prefixes.rb:
--------------------------------------------------------------------------------
1 | module Cell::Prefixes
2 | def self.included(includer)
3 | includer.extend(ClassMethods)
4 | end
5 |
6 | def _prefixes
7 | self.class.prefixes
8 | end
9 |
10 | # You're free to override those methods in case you want to alter our view inheritance.
11 | module ClassMethods
12 | def prefixes
13 | @prefixes ||= _prefixes
14 | end
15 |
16 | private
17 | def _prefixes
18 | return [] if abstract?
19 | _local_prefixes + superclass.prefixes
20 | end
21 |
22 | def _local_prefixes
23 | view_paths.collect { |path| "#{path}/#{controller_path}" }
24 | end
25 |
26 | # Instructs Cells to inherit views from a parent cell without having to inherit class code.
27 | def inherit_views(parent)
28 | define_method :_prefixes do
29 | super() + parent.prefixes
30 | end
31 | end
32 | end
33 | end
--------------------------------------------------------------------------------
/lib/cell/escaped.rb:
--------------------------------------------------------------------------------
1 | module Cell::ViewModel::Escaped
2 | def self.included(includer)
3 | includer.extend Property
4 | end
5 |
6 | module Property
7 | def property(*names)
8 | super.tap do # super defines #title
9 | mod = Module.new do
10 | names.each do |name|
11 | define_method(name) do |options={}|
12 | value = super() # call the original #title.
13 | return value unless value.is_a?(String)
14 | return value if options[:escape] == false
15 | escape!(value)
16 | end
17 | end
18 | end
19 | include mod
20 | end
21 | end
22 | end # Property
23 |
24 | # Can be used as a helper in the cell, too.
25 | # Feel free to override and use a different escaping implementation.
26 | require "erb"
27 | def escape!(string)
28 | ::ERB::Util.html_escape(string)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 | Bundler::GemHelper.install_tasks
3 |
4 | require 'rake/testtask'
5 |
6 | desc 'Default: run unit tests.'
7 | task :default => :test
8 |
9 | Rake::TestTask.new(:test) do |test|
10 | test.libs << 'test'
11 | test.pattern = 'test/*_test.rb'
12 | test.verbose = true
13 | # Ruby built-in warnings contain way too much noise to be useful. Consider turning them on again when the following issues are accepted in ruby:
14 | # * https://bugs.ruby-lang.org/issues/10967 (remove warning: private attribute?)
15 | # * https://bugs.ruby-lang.org/issues/12299 (customized warning handling)
16 | test.warning = false
17 | end
18 |
19 | # Rake::TestTask.new(:rails) do |test|
20 | # test.libs << 'test/rails'
21 | # test.test_files = FileList['test/rails4.2/*_test.rb']
22 | # test.verbose = true
23 | # end
24 |
25 | # rails_task = Rake::Task["rails"]
26 | # test_task = Rake::Task["test"]
27 | # default_task.enhance { test_task.invoke }
28 | # default_task.enhance { rails_task.invoke }
29 |
--------------------------------------------------------------------------------
/lib/cell/option.rb:
--------------------------------------------------------------------------------
1 | require "trailblazer/option"
2 | require "uber/callable"
3 |
4 | module Cell
5 | # Extend `Trailblazer::Option` to make static values as callables too.
6 | class Option < ::Trailblazer::Option
7 | def self.build(value)
8 | callable = case value
9 | when Proc, Symbol, Uber::Callable
10 | value
11 | else
12 | ->(*) { value } # Make non-callable value to callable.
13 | end
14 |
15 | super(callable)
16 | end
17 | end
18 |
19 | class Options < Hash
20 | # Evaluates every element and returns a hash. Accepts arbitrary arguments.
21 | def call(*args, **options, &block)
22 | Hash[ collect { |k,v| [k,v.(*args, **options, &block) ] } ]
23 | end
24 | end
25 |
26 | def self.Option(value)
27 | ::Cell::Option.build(value)
28 | end
29 |
30 | def self.Options(options)
31 | Options.new.tap do |hsh|
32 | options.each { |k,v| hsh[k] = Option(v) }
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/templates_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 |
4 | class TemplatesTest < MiniTest::Spec
5 | Templates = Cell::Templates
6 |
7 | # existing.
8 | it { _(Templates.new[['test/fixtures/bassist'], 'play.erb', {template_class: Cell::Erb::Template}].file).must_equal 'test/fixtures/bassist/play.erb' }
9 |
10 | # not existing.
11 | it { assert_nil(Templates.new[['test/fixtures/bassist'], 'not-here.erb', {}]) }
12 |
13 |
14 | # different caches for different classes
15 |
16 | # same cache for subclasses
17 |
18 | end
19 |
20 |
21 | class TemplatesCachingTest < MiniTest::Spec
22 | class SongCell < Cell::ViewModel
23 | self.view_paths = ['test/fixtures']
24 | # include Cell::Erb
25 |
26 | def show
27 | render
28 | end
29 | end
30 |
31 | # templates are cached once and forever.
32 | it do
33 | cell = cell("templates_caching_test/song")
34 |
35 | _(cell.call(:show)).must_equal 'The Great Mind Eraser'
36 |
37 | SongCell.templates.instance_eval do
38 | def create; raise; end
39 | end
40 |
41 | # cached, NO new tilt template.
42 | _(cell.call(:show)).must_equal 'The Great Mind Eraser'
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/context_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ContextTest < MiniTest::Spec
4 | class ParentCell < Cell::ViewModel
5 | def user
6 | context[:user]
7 | end
8 |
9 | def controller
10 | context[:controller]
11 | end
12 | end
13 |
14 | let (:model) { Object.new }
15 | let (:user) { Object.new }
16 | let (:controller) { Object.new }
17 |
18 | let (:parent) { ParentCell.(model, admin: true, context: { user: user, controller: controller }) }
19 |
20 | it do
21 | _(parent.model).must_equal model
22 | _(parent.controller).must_equal controller
23 | _(parent.user).must_equal user
24 |
25 | # nested cell
26 | child = parent.cell("context_test/parent", "")
27 |
28 | _(child.model).must_equal ""
29 | _(child.controller).must_equal controller
30 | _(child.user).must_equal user
31 | end
32 |
33 | # child can add to context
34 | it do
35 | child = parent.cell(ParentCell, nil, context: { "is_child?" => true })
36 |
37 | assert_nil(parent.context["is_child?"])
38 |
39 | assert_nil(child.model)
40 | _(child.controller).must_equal controller
41 | _(child.user).must_equal user
42 | _(child.context["is_child?"]).must_equal true
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/partial_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "cell/partial"
3 |
4 | class PartialTest < MiniTest::Spec
5 | class WithPartial < Cell::ViewModel
6 | self.view_paths = ['test/fixtures'] # doesn't exist.
7 | include ::Cell::Erb
8 |
9 | include Partial
10 |
11 | def show
12 | render partial: "../fixtures/partials/show.html"
13 | end
14 |
15 | def show_with_format
16 | render partial: "../fixtures/partials/show", formats: [:xml]
17 | end
18 |
19 | def show_without_partial
20 | render :show
21 | end
22 | end
23 |
24 | class WithPartialAndManyViewPaths < WithPartial
25 | self.view_paths << ['app/views']
26 | end
27 |
28 | it { _(WithPartial.new(nil).show).must_equal "I Am Wrong And I Am Right" }
29 | it { _(WithPartial.new(nil).show_with_format).must_equal "I Am Wrong And I Am Right" }
30 | it { _(WithPartial.new(nil).show_without_partial).must_equal "Adenosine Breakdown" }
31 |
32 | it { _(WithPartialAndManyViewPaths.new(nil).show).must_equal "I Am Wrong And I Am Right" }
33 | it { _(WithPartialAndManyViewPaths.new(nil).show_with_format).must_equal "I Am Wrong And I Am Right" }
34 | it { _(WithPartialAndManyViewPaths.new(nil).show_without_partial).must_equal "Adenosine Breakdown" }
35 | end
36 |
--------------------------------------------------------------------------------
/cells.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path("lib", __dir__)
2 | $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
3 |
4 | require "cell/version"
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "cells"
8 | spec.version = Cell::VERSION
9 | spec.authors = ["Nick Sutterer"]
10 | spec.email = ["apotonick@gmail.com"]
11 | spec.homepage = "https://github.com/apotonick/cells"
12 | spec.summary = "View Models for Ruby and Rails."
13 | spec.description = "View Models for Ruby and Rails, replacing helpers and partials while giving you a clean view architecture with proper encapsulation."
14 | spec.license = "MIT"
15 |
16 | spec.files = `git ls-files`.split("\n")
17 | spec.test_files = `git ls-files -- {test}/*`.split("\n")
18 | spec.require_paths = ["lib"]
19 | spec.required_ruby_version = ">= 2.2.10"
20 |
21 | spec.add_dependency "declarative-builder", "~> 0.2.0"
22 | spec.add_dependency "trailblazer-option", "~> 0.1.0"
23 | spec.add_dependency "tilt", ">= 1.4", "< 3"
24 | spec.add_dependency "uber", "< 0.2.0"
25 |
26 | spec.add_development_dependency "capybara"
27 | spec.add_development_dependency "cells-erb", ">= 0.1.0"
28 | spec.add_development_dependency "minitest"
29 | spec.add_development_dependency "rake"
30 | end
31 |
--------------------------------------------------------------------------------
/test/inspect_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class InspectTest < Minitest::Spec
4 | class FakeModel
5 | def initialize(title)
6 | @title = title
7 | end
8 | end
9 |
10 | def build_model
11 | InspectTest::FakeModel.new('Title')
12 | end
13 | # #inspect
14 | it do
15 | cell = Cell::ViewModel.(model_obj = build_model, options = { genre: "Djent" })
16 |
17 | inspection_s = cell.inspect
18 |
19 | _(inspection_s).must_match '#"
41 | _(inspection_s).must_match "@options=#{options.inspect}"
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/property_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class PropertyTest < MiniTest::Spec
4 | class SongCell < Cell::ViewModel
5 | property :title
6 |
7 | def title
8 | super + ""
9 | end
10 | end
11 |
12 | let (:song) { Struct.new(:title).new("She Sells And Sand Sandwiches") }
13 | # ::property creates automatic accessor.
14 | it { _(SongCell.(song).title).must_equal "She Sells And Sand Sandwiches" }
15 | end
16 |
17 |
18 | class EscapedPropertyTest < MiniTest::Spec
19 | class SongCell < Cell::ViewModel
20 | include Escaped
21 | property :title
22 | property :artist
23 | property :copyright, :lyrics
24 |
25 | def title(*)
26 | "#{super}" # super + "" still escapes, but this is Rails.
27 | end
28 |
29 | def raw_title
30 | title(escape: false)
31 | end
32 | end
33 |
34 | let (:song) do
35 | Struct
36 | .new(:title, :artist, :copyright, :lyrics)
37 | .new("She Sells And Sand Sandwiches", Object, "Copy", "Words")
38 | end
39 |
40 | # ::property escapes, everywhere.
41 | it { _(SongCell.(song).title).must_equal "<b>She Sells And Sand Sandwiches" }
42 | it { _(SongCell.(song).copyright).must_equal "<a>Copy</a>" }
43 | it { _(SongCell.(song).lyrics).must_equal "<i>Words</i>" }
44 | # no escaping for non-strings.
45 | it { _(SongCell.(song).artist).must_equal Object }
46 | # no escaping when escape: false
47 | it { _(SongCell.(song).raw_title).must_equal "She Sells And Sand Sandwiches" }
48 | end
49 |
--------------------------------------------------------------------------------
/test/builder_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class BuilderTest < MiniTest::Spec
4 | Song = Struct.new(:title)
5 | Hit = Struct.new(:title)
6 |
7 | class SongCell < Cell::ViewModel
8 | include Cell::Builder
9 |
10 | builds do |model, options|
11 | if model.is_a? Hit
12 | HitCell
13 | elsif options[:evergreen]
14 | EvergreenCell
15 | end
16 | end
17 |
18 | def options
19 | @options
20 | end
21 |
22 | def show
23 | "* #{title}"
24 | end
25 |
26 | property :title
27 | end
28 |
29 | class HitCell < SongCell
30 | def show
31 | "* **#{title}**"
32 | end
33 | end
34 |
35 | class EvergreenCell < SongCell
36 | end
37 |
38 | # the original class is used when no builder matches.
39 | it { _(SongCell.(Song.new("Nation States"), {})).must_be_instance_of SongCell }
40 |
41 | it do
42 | cell = SongCell.(Hit.new("New York"), {})
43 | _(cell).must_be_instance_of HitCell
44 | _(cell.options).must_equal({})
45 | end
46 |
47 | it do
48 | cell = SongCell.(Song.new("San Francisco"), evergreen: true)
49 | _(cell).must_be_instance_of EvergreenCell
50 | _(cell.options).must_equal({evergreen:true})
51 | end
52 |
53 | # without arguments.
54 | it { _(SongCell.(Hit.new("Frenzy"))).must_be_instance_of HitCell }
55 |
56 | # with collection.
57 | it { _(SongCell.(collection: [Song.new("Nation States"), Hit.new("New York")]).()).must_equal "* Nation States* **New York**" }
58 |
59 | # with Concept
60 | class Track < Cell::Concept
61 | end
62 | it { _(Track.()).must_be_instance_of Track }
63 | end
64 |
--------------------------------------------------------------------------------
/test/testing_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TestCaseTest < MiniTest::Spec
4 | class SongCell < Cell::ViewModel
5 | def show
6 | "Give It All!"
7 | end
8 | end
9 |
10 | class Song
11 | class Cell < Cell::Concept
12 | end
13 | end
14 |
15 | let (:song) { Object.new }
16 |
17 | # #cell returns the instance
18 | describe "#cell" do
19 | subject { cell("test_case_test/song", song) }
20 |
21 | it { _(subject).must_be_instance_of SongCell }
22 | it { _(subject.model).must_equal song }
23 |
24 | it { _(cell("test_case_test/song", collection: [song, song]).()).must_equal "Give It All!Give It All!" }
25 | end
26 |
27 |
28 | describe "#concept" do
29 | subject { concept("test_case_test/song/cell", song) }
30 |
31 | it { _(subject).must_be_instance_of Song::Cell }
32 | it { _(subject.model).must_equal song }
33 | end
34 | end
35 |
36 | # capybara support
37 | require "capybara"
38 |
39 | class CapybaraTest < MiniTest::Spec
40 | class CapybaraCell < Cell::ViewModel
41 | def show
42 | "Grunt"
43 | end
44 | end
45 |
46 | describe "capybara support" do
47 | subject { cell("capybara_test/capybara", nil) }
48 |
49 | before { Cell::Testing.capybara = true } # yes, a global switch!
50 | after { Cell::Testing.capybara = false }
51 |
52 | it { _(subject.(:show).has_selector?('b')).must_equal true }
53 |
54 | it { _(cell("capybara_test/capybara", collection: [1, 2]).().has_selector?('b')).must_equal true }
55 |
56 | # FIXME: this kinda sucks, what if you want the string in a Capybara environment?
57 | it { _(subject.(:show).to_s).must_match "Grunt" }
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/cell/layout.rb:
--------------------------------------------------------------------------------
1 | module Cell
2 | class ViewModel
3 | # Set the layout per cell class. This is used in #render calls. Gets inherited to subclasses.
4 | module Layout
5 | def self.included(base)
6 | base.extend ClassMethods
7 | base.inheritable_attr :layout_name
8 | end
9 |
10 | module ClassMethods
11 | def layout(name)
12 | self.layout_name = name
13 | end
14 | end
15 |
16 | private
17 | def process_options!(options)
18 | options[:layout] ||= self.class.layout_name
19 | super
20 | end
21 |
22 | def render_to_string(options, &block)
23 | with_layout(options, super)
24 | end
25 |
26 | def with_layout(options, content)
27 | return content unless layout = options[:layout]
28 |
29 | render_layout(layout, options, content)
30 | end
31 |
32 | def render_layout(name, options, content)
33 | template = find_template(options.merge view: name) # we could also allow a different layout engine, etc.
34 | render_template(template, options) { content }
35 | end
36 |
37 | # Allows using a separate layout cell which will wrap the actual content.
38 | # Use like cell(..., layout: Cell::Layout)
39 | #
40 | # Note that still allows the `render layout: :application` option.
41 | module External
42 | def call(*)
43 | content = super
44 | Render.(content, model, @options[:layout], @options)
45 | end
46 |
47 | Render = ->(content, model, layout, options) do # WARNING: THIS IS NOT FINAL API.
48 | return content unless layout = layout # TODO: test when invoking cell without :layout.
49 |
50 | # DISCUSS: should we allow instances, too? we could cache the layout cell.
51 | layout.new(model, context: options[:context]).(&lambda { content })
52 | end
53 | end # External
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/cell/collection.rb:
--------------------------------------------------------------------------------
1 | module Cell
2 | class Collection
3 | def initialize(ary, options, cell_class)
4 | options.delete(:collection)
5 | set_deprecated_options(options) # TODO: remove in 5.0.
6 |
7 | @ary = ary
8 | @options = options # these options are "final" and will be identical for all collection cells.
9 | @cell_class = cell_class
10 | end
11 |
12 | def set_deprecated_options(options) # TODO: remove in 5.0.
13 | self.method = options.delete(:method) if options.include?(:method)
14 | self.collection_join = options.delete(:collection_join) if options.include?(:collection_join)
15 | end
16 |
17 | module Call
18 | def call(state=:show)
19 | join(collection_join) { |cell, i| cell.(method || state) }
20 | end
21 |
22 | end
23 | include Call
24 |
25 | def to_s
26 | call
27 | end
28 |
29 | # Iterate collection and build a cell for each item.
30 | # The passed block receives that cell and the index.
31 | # Its return value is captured and joined.
32 | def join(separator="", &block)
33 | @ary.each_with_index.collect do |model, i|
34 | cell = @cell_class.build(model, @options)
35 | block_given? ? yield(cell, i) : cell
36 | end.join(separator)
37 | end
38 |
39 | module Layout
40 | def call(*) # WARNING: THIS IS NOT FINAL API.
41 | layout = @options.delete(:layout) # we could also override #initialize and that there?
42 |
43 | content = super # DISCUSS: that could come in via the pipeline argument.
44 | ViewModel::Layout::External::Render.(content, @ary, layout, @options)
45 | end
46 | end
47 | include Layout
48 |
49 | # TODO: remove in 5.0.
50 | private
51 | attr_accessor :collection_join, :method
52 |
53 | extend Gem::Deprecate
54 | deprecate :method=, "`call(method)` as documented here: http://trailblazer.to/gems/cells/api.html#collection", 2016, 7
55 | deprecate :collection_join=, "`join(\"
\")` as documented here: http://trailblazer.to/gems/cells/api.html#collection", 2016, 7
56 | end
57 | end
58 |
59 | # Collection#call
60 | # |> Header#call
61 | # |> Layout#call
62 |
--------------------------------------------------------------------------------
/lib/cell/testing.rb:
--------------------------------------------------------------------------------
1 | require "uber/inheritable_attr"
2 |
3 | module Cell
4 | # Builder methods and Capybara support.
5 | # This gets included into Test::Unit, MiniTest::Spec, etc.
6 | module Testing
7 | def cell(name, *args)
8 | cell_for(ViewModel, name, *args)
9 | end
10 |
11 | def concept(name, *args)
12 | cell_for(Concept, name, *args)
13 | end
14 |
15 | private
16 | def cell_for(baseclass, name, model=nil, options={})
17 | options[:context] ||= {}
18 | options[:context][:controller] = controller
19 |
20 | cell = baseclass.cell(name, model, options)
21 |
22 | cell.extend(Capybara) if Cell::Testing.capybara? # leaving this here as most people use Capybara.
23 | # apparently it's ok to only override ViewModel#call and capybararize the result.
24 | # when joining in a Collection, the joint will still be capybararized.
25 | cell
26 | end
27 |
28 |
29 | # Set this to true if you have Capybara loaded. Happens automatically in Cell::TestCase.
30 | def self.capybara=(value)
31 | @capybara = value
32 | end
33 |
34 | def self.capybara?
35 | @capybara if defined?(@capybara)
36 | end
37 |
38 | # Extends ViewModel#call by injecting Capybara support.
39 | module Capybara
40 | module ToS
41 | def to_s
42 | native.to_s
43 | end
44 | end
45 |
46 | def call(*)
47 | ::Capybara.string(super).extend(ToS)
48 | end
49 | end
50 |
51 | module ControllerFor
52 | # This method is provided by the cells-rails gem.
53 | def controller_for(controller_class)
54 | # raise "[Cells] Please install (or update?) the cells-rails gem."
55 | end
56 | end
57 | include ControllerFor
58 |
59 | def controller # FIXME: this won't allow us using let(:controller) in MiniTest.
60 | controller_for(self.class.controller_class)
61 | end
62 |
63 | def self.included(base)
64 | base.class_eval do
65 | extend Uber::InheritableAttr
66 | inheritable_attr :controller_class
67 |
68 | def self.controller(name) # DSL method for the test.
69 | self.controller_class = name
70 | end
71 | end
72 | end
73 | end # Testing
74 | end
75 |
--------------------------------------------------------------------------------
/test/layout_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class SongWithLayoutCell < Cell::ViewModel
4 | self.view_paths = ['test/fixtures']
5 | # include Cell::Erb
6 |
7 | def show
8 | render layout: :merry
9 | end
10 |
11 | def unknown
12 | render layout: :no_idea_what_u_mean
13 | end
14 |
15 | def what
16 | "Xmas"
17 | end
18 |
19 | def string
20 | "Right"
21 | end
22 |
23 | private
24 | def title
25 | "Papertiger"
26 | end
27 | end
28 |
29 | class SongWithLayoutOnClassCell < SongWithLayoutCell
30 | # inherit_views SongWithLayoutCell
31 | layout :merry
32 |
33 | def show
34 | render
35 | end
36 |
37 | def show_with_layout
38 | render layout: :happy
39 | end
40 | end
41 |
42 | class LayoutTest < MiniTest::Spec
43 | # render show.haml calling method.
44 | # same context as content view as layout call method.
45 | it { _(SongWithLayoutCell.new(nil).show).must_equal "Merry Xmas, Papertiger\n" }
46 |
47 | # raises exception when layout not found!
48 |
49 | it { assert_raises(Cell::TemplateMissingError) { SongWithLayoutCell.new(nil).unknown } }
50 | # assert message of exception.
51 | it { }
52 |
53 | # with ::layout.
54 | it { _(SongWithLayoutOnClassCell.new(nil).show).must_equal "Merry Xmas, Papertiger\n" }
55 |
56 | # with ::layout and :layout, :layout wins.
57 | it { _(SongWithLayoutOnClassCell.new(nil).show_with_layout).must_equal "Happy Friday!" }
58 | end
59 |
60 | module Comment
61 | class ShowCell < Cell::ViewModel
62 | self.view_paths = ['test/fixtures']
63 | include Layout::External
64 |
65 | def show
66 | render + render
67 | end
68 | end
69 |
70 | class LayoutCell < Cell::ViewModel
71 | self.view_paths = ['test/fixtures']
72 | end
73 | end
74 |
75 | class ExternalLayoutTest < Minitest::Spec
76 | it do
77 | _(Comment::ShowCell.new(nil, layout: Comment::LayoutCell, context: { beer: true }).
78 | ()).must_equal "$layout.erb{$show.erb, {:beer=>true}\n$show.erb, {:beer=>true}\n, {:beer=>true}}
79 | "
80 | end
81 |
82 | # collection :layout
83 | it do
84 | _(Cell::ViewModel.cell("comment/show", collection: [Object, Module], layout: Comment::LayoutCell).()).
85 | must_equal "$layout.erb{$show.erb, nil\n$show.erb, nil\n$show.erb, nil\n$show.erb, nil\n, nil}
86 | "
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/lib/cell/caching.rb:
--------------------------------------------------------------------------------
1 | require "cell/option"
2 |
3 | module Cell
4 | module Caching
5 | def self.included(includer)
6 | includer.class_eval do
7 | extend ClassMethods
8 | extend Uber::InheritableAttr
9 | inheritable_attr :version_procs
10 | inheritable_attr :conditional_procs
11 | inheritable_attr :cache_options
12 |
13 | self.version_procs = {}
14 | self.conditional_procs = {}
15 | self.cache_options = {}
16 | end
17 | end
18 |
19 | module ClassMethods
20 | def cache(state, *args, **kws, &block)
21 | conditional_procs[state] = Cell::Option(kws.delete(:if) || true)
22 | version_procs[state] = Cell::Option(args.first || block)
23 | cache_options[state] = Cell::Options(kws)
24 | end
25 |
26 | # Computes the complete, namespaced cache key for +state+.
27 | def state_cache_key(state, key_parts={})
28 | expand_cache_key([controller_path, state, key_parts])
29 | end
30 |
31 | def expire_cache_key_for(key, cache_store, *args)
32 | cache_store.delete(key, *args)
33 | end
34 |
35 | private
36 |
37 | def expand_cache_key(key)
38 | key.join("/")
39 | end
40 | end
41 |
42 | def render_state(state, *args, **kws)
43 | state = state.to_sym
44 |
45 | # Before Ruby 3.0, this wasn't necessary, but since cache filters don't receive kwargs as per the "old" (existing cells version) implementation, we can make it one array.
46 | cache_filter_args = args + [kws]
47 |
48 | return super(state, *args, **kws) unless cache?(state, *cache_filter_args)
49 |
50 |
51 | key = self.class.state_cache_key(state, self.class.version_procs[state].(*cache_filter_args, exec_context: self))
52 | options = self.class.cache_options[state].(*cache_filter_args, exec_context: self)
53 |
54 | fetch_from_cache_for(key, options) { super(state, *args, **kws) }
55 | end
56 |
57 | def cache_store # we want to use DI to set a cache store in cell/rails.
58 | raise "No cache store has been set."
59 | end
60 |
61 | def cache?(state, *args)
62 | perform_caching? and state_cached?(state) and self.class.conditional_procs[state].(*args, exec_context: self)
63 | end
64 |
65 | private
66 |
67 | def perform_caching?
68 | true
69 | end
70 |
71 | def fetch_from_cache_for(key, options, &block)
72 | cache_store.fetch(key, options, &block)
73 | end
74 |
75 | def state_cached?(state)
76 | self.class.version_procs.has_key?(state)
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/test/render_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class SongCell < Cell::ViewModel
4 | self.view_paths = ['test/fixtures']
5 | # include ::Cell::Erb
6 |
7 | def show
8 | render
9 | end
10 |
11 | def ivar
12 | @title = "Carnage"
13 | render
14 | end
15 |
16 | def unknown
17 | render
18 | end
19 |
20 | def string
21 | "Right"
22 | end
23 |
24 | # TODO: just pass hash.
25 | def with_locals
26 | render locals: {length: 280, title: "Shot Across The Bow"}
27 | end
28 |
29 | def with_erb
30 | render template_engine: :erb
31 | end
32 |
33 | def with_view_name
34 | @title = "Man Of Steel"
35 | render :ivar
36 | end
37 |
38 | def receiving_options(layout=:default)
39 | "#{layout}"
40 | end
41 |
42 | def with_html
43 | render
44 | end
45 |
46 | def send
47 | "send"
48 | end
49 |
50 | def with_block
51 | render { "Clean Sheets" + render(:with_html) }
52 | end
53 |
54 | private
55 | def title
56 | "Papertiger"
57 | end
58 | end
59 |
60 | class RenderTest < MiniTest::Spec
61 | # render show.haml calling method, implicit render.
62 | it { _(SongCell.new(nil).show).must_equal "Papertiger\n" }
63 |
64 | # render ivar.haml using instance variable.
65 | it { _(SongCell.new(nil).ivar).must_equal "Carnage\n" }
66 |
67 | # render string.
68 | it { _(SongCell.new(nil).string).must_equal "Right" }
69 |
70 | # #call renders :show
71 | it { _(SongCell.new(nil).call).must_equal "Papertiger\n" }
72 |
73 | # call(:form) renders :form
74 | it { _(SongCell.new(nil).call(:with_view_name)).must_equal "Man Of Steel\n" }
75 |
76 | # works with state called `send`
77 | it { _(SongCell.new(nil).call(:send)).must_equal "send" }
78 |
79 | # throws an exception when not found.
80 | it do
81 | exception = assert_raises(Cell::TemplateMissingError) { SongCell.new(nil).unknown }
82 | _(exception.message).must_equal "Template missing: view: `unknown.erb` prefixes: [\"test/fixtures/song\"]"
83 | end
84 |
85 | # allows locals
86 | it { _(SongCell.new(nil).with_locals).must_equal "Shot Across The Bow\n280\n" }
87 |
88 | # render :form is a shortcut.
89 | it { _(SongCell.new(nil).with_view_name).must_equal "Man Of Steel\n" }
90 |
91 | # :template_engine renders ERB.
92 | # it { SongCell.new(nil).with_erb.must_equal "ERB:\n\n Papertiger\n" }
93 |
94 | # view: "show.html"
95 |
96 | # allows passing in options DISCUSS: how to handle that in cache block/builder?
97 | it { _(SongCell.new(nil).receiving_options).must_equal "default" }
98 | it { _(SongCell.new(nil).receiving_options(:fancy)).must_equal "fancy" }
99 | it { _(SongCell.new(nil).call(:receiving_options, :fancy)).must_equal "fancy" }
100 |
101 | # doesn't escape HTML.
102 | it { _(SongCell.new(nil).call(:with_html)).must_equal "Yew!
" }
103 |
104 | # render {} with block
105 | it { _(SongCell.new(nil).with_block).must_equal "Yo! Clean SheetsYew!
\n" }
106 | end
107 |
108 | # test inheritance
109 |
110 | # test view: :bla and :bla
111 | # with layout and locals.
112 | # with layout and :text
113 |
114 | # render with format (e.g. when using ERB for one view)
115 | # should we allow changing the format "per run", so a cell can do .js and .haml? or should that be configurable on class level?
116 |
--------------------------------------------------------------------------------
/test/concept_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | Cell::Concept.class_eval do
4 | self.view_paths = ['test/fixtures/concepts']
5 | end
6 |
7 | # Trailblazer style:
8 | module Record
9 | class Cell < ::Cell::Concept # cell("record")
10 | include ::Cell::Erb
11 |
12 | def show
13 | render # Party On, #{model}
14 | end
15 |
16 | # cell(:song, concept: :record)
17 | class Song < self # cell("record/cell/song")
18 | def show
19 | render view: :song#, layout: "layout"
20 | # TODO: test layout: .. in ViewModel
21 | end
22 | end
23 |
24 | class Hit < ::Cell::Concept
25 | inherit_views Record::Cell
26 | end
27 |
28 |
29 | def description
30 | "A Tribute To Rancid, with #{@options[:tracks]} songs! [#{context}]"
31 | end
32 | end
33 | end
34 |
35 | module Record
36 | module Cells
37 | class Cell < ::Cell::Concept
38 | class Song < ::Cell::Concept
39 | end
40 | end
41 | end
42 | end
43 |
44 | # app/cells/comment/views
45 | # app/cells/comment/form/views
46 | # app/cells/comment/views/form inherit_views Comment::Cell, render form/show
47 |
48 |
49 | class ConceptTest < MiniTest::Spec
50 | describe "::controller_path" do
51 | it { _(Record::Cell.new.class.controller_path).must_equal "record" }
52 | it { _(Record::Cell::Song.new.class.controller_path).must_equal "record/song" }
53 | it { _(Record::Cells::Cell.new.class.controller_path).must_equal "record/cells" }
54 | it { _(Record::Cells::Cell::Song.new.class.controller_path).must_equal "record/cells/song" }
55 | end
56 |
57 |
58 | describe "#_prefixes" do
59 | it { _(Record::Cell.new._prefixes).must_equal ["test/fixtures/concepts/record/views"] }
60 | it { _(Record::Cell::Song.new._prefixes).must_equal ["test/fixtures/concepts/record/song/views", "test/fixtures/concepts/record/views"] }
61 | it { _(Record::Cell::Hit.new._prefixes).must_equal ["test/fixtures/concepts/record/hit/views", "test/fixtures/concepts/record/views"] } # with inherit_views.
62 | end
63 |
64 | it { _(Record::Cell.new("Wayne").call(:show)).must_equal "Party on, Wayne!" }
65 |
66 |
67 | describe "::cell" do
68 | it { _(Cell::Concept.cell("record/cell")).must_be_instance_of( Record::Cell) }
69 | it { _(Cell::Concept.cell("record/cell/song")).must_be_instance_of Record::Cell::Song }
70 | # cell("song", concept: "record/compilation") # record/compilation/cell/song
71 | end
72 |
73 | describe "#render" do
74 | it { _(Cell::Concept.cell("record/cell/song").show).must_equal "Lalala" }
75 | end
76 |
77 | describe "#cell (in state)" do
78 | # test with controller, but remove tests when we don't need it anymore.
79 | it { _(Cell::Concept.cell("record/cell", nil, context: { controller: Object }).cell("record/cell", nil)).must_be_instance_of Record::Cell }
80 | it { _(Cell::Concept.cell("record/cell", nil, context: { controller: Object }).concept("record/cell", nil, tracks: 24).(:description)).must_equal "A Tribute To Rancid, with 24 songs! [{:controller=>Object}]" }
81 | # concept(.., collection: ..)
82 | it do
83 | _(Cell::Concept.cell("record/cell", nil, context: { controller: Object }).
84 | concept("record/cell", collection: [1,2], tracks: 24).(:description)).must_equal "A Tribute To Rancid, with 24 songs! [{:controller=>Object}]A Tribute To Rancid, with 24 songs! [{:controller=>Object}]"
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/test/cache_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class CacheTest < Minitest::Spec
4 | STORE = Class.new(Hash) do
5 | def fetch(key, options, &block)
6 | self[key] || self[key] = yield
7 | end
8 | end.new
9 |
10 | module Cache
11 | def show(*)
12 | "#{@model}"
13 | end
14 |
15 | def cache_store
16 | STORE
17 | end
18 |
19 | def has_changed?(*)
20 | @model < 3
21 | end
22 | end
23 |
24 | Component = ->(*args, **kwargs, &block) {
25 | Class.new(Cell::ViewModel) do
26 | cache :show, *args, **kwargs, &block
27 | include Cache
28 | end
29 | }
30 |
31 | it "without any options" do
32 | WithoutOptions = Component.()
33 |
34 | _(WithoutOptions.new(1).()).must_equal("1")
35 | _(WithoutOptions.new(2).()).must_equal("1")
36 | end
37 |
38 | it "with specified version" do
39 | version = ->(options) { options[:version] }
40 |
41 | # Cache invalidation using version as a proc
42 | WithVersionArg = Component.(version)
43 |
44 | _(WithVersionArg.new(1).(:show, version: 1)).must_equal("1")
45 | _(WithVersionArg.new(2).(:show, version: 1)).must_equal("1")
46 |
47 | _(WithVersionArg.new(3).(:show, version: 2)).must_equal("3")
48 |
49 | # Cache invalidation using version as a block
50 | WithVersionBlock = Component.(&version)
51 |
52 | _(WithVersionBlock.new(1).(:show, version: 1)).must_equal("1")
53 | _(WithVersionBlock.new(2).(:show, version: 1)).must_equal("1")
54 |
55 | _(WithVersionBlock.new(3).(:show, version: 2)).must_equal("3")
56 | end
57 |
58 | it "with conditional" do
59 | WithConditional = Component.(if: :has_changed?)
60 |
61 | _(WithConditional.new(1).()).must_equal("1")
62 | _(WithConditional.new(2).()).must_equal("1")
63 |
64 | _(WithConditional.new(3).()).must_equal("3")
65 | end
66 |
67 | it "forwards remaining options to cache store" do
68 | WithOptions = Class.new(Cell::ViewModel) do
69 | cache :show, if: :has_changed?, expires_in: 10, tags: ->(*args) { Hash(args.first)[:tags] }
70 | ## We can use kwargs in the cache key filter
71 | # cache :new, expires_in: 10, tags: ->(*, my_tags:, **) { my_tags } # FIXME: allow this in Cells 5.
72 | include Cache
73 |
74 | CACHE_WITH_OPTIONS_STORE = Class.new(Hash) do
75 | def fetch(key, options)
76 | value = self[key] || self[key] = yield
77 | [value, options]
78 | end
79 | end.new
80 |
81 | def cache_store
82 | CACHE_WITH_OPTIONS_STORE
83 | end
84 | end
85 |
86 | _(WithOptions.new(1).()).must_equal(%{["1", {:expires_in=>10, :tags=>nil}]})
87 | _(WithOptions.new(2).()).must_equal(%{["1", {:expires_in=>10, :tags=>nil}]})
88 | _(WithOptions.new(2).(:show, tags: [:a, :b])).must_equal(%{["1", {:expires_in=>10, :tags=>[:a, :b]}]})
89 |
90 | # FIXME: allow this in Cells 5.
91 | # _(WithOptions.new(2).(:new, my_tags: [:a, :b])).must_equal(%{["1", {:expires_in=>10, :tags=>[:a, :b]}]})
92 | end
93 |
94 | it "forwards all arguments to renderer after cache hit" do
95 | SongCell = Class.new(Cell::ViewModel) do
96 | cache :show
97 |
98 | def show(type, title:, part:, **)
99 | "#{type} #{title} #{part}"
100 | end
101 |
102 | def cache_store
103 | STORE
104 | end
105 | end
106 |
107 | # cache miss for the first render
108 | _(SongCell.new.(:show, "Album", title: "IT", part: "1")).must_equal("Album IT 1")
109 |
110 | # cache hit for the second render
111 | _(SongCell.new.(:show, "Album", title: "IT", part: "1")).must_equal("Album IT 1")
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/test/public_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class PublicTest < MiniTest::Spec
4 | class SongCell < Cell::ViewModel
5 | def initialize(*args)
6 | @initialize_args = *args
7 | end
8 | attr_reader :initialize_args
9 |
10 | def show
11 | initialize_args.inspect
12 | end
13 |
14 | def detail
15 | "* #{initialize_args}"
16 | end
17 | end
18 |
19 | class Songs < Cell::Concept
20 | end
21 |
22 | # ViewModel.cell returns the cell instance.
23 | it { _(Cell::ViewModel.cell("public_test/song")).must_be_instance_of SongCell }
24 | it { _(Cell::ViewModel.cell(PublicTest::SongCell)).must_be_instance_of SongCell }
25 |
26 | # Concept.cell simply camelizes the string before constantizing.
27 | it { _(Cell::Concept.cell("public_test/songs")).must_be_instance_of Songs }
28 |
29 | it { _(Cell::Concept.cell(PublicTest::Songs)).must_be_instance_of Songs }
30 |
31 | # ViewModel.cell passes options to cell.
32 | it { _(Cell::ViewModel.cell("public_test/song", Object, genre: "Metal").initialize_args).must_equal [Object, {genre:"Metal"}] }
33 |
34 | # ViewModel.cell(collection: []) renders cells.
35 | it { _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module]).to_s).must_equal '[Object, {}][Module, {}]' }
36 |
37 | # DISCUSS: should cell.() be the default?
38 | # ViewModel.cell(collection: []) renders cells with custom join.
39 | it do
40 | Gem::Deprecate::skip_during do
41 | _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module]).join('
') do |cell|
42 | cell.()
43 | end).must_equal '[Object, {}]
[Module, {}]'
44 | end
45 | end
46 |
47 | # ViewModel.cell(collection: []) passes generic options to cell.
48 | it { _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module], genre: 'Metal', context: { ready: true }).to_s).must_equal "[Object, {:genre=>\"Metal\", :context=>{:ready=>true}}][Module, {:genre=>\"Metal\", :context=>{:ready=>true}}]" }
49 |
50 | # ViewModel.cell(collection: [], method: :detail) invokes #detail instead of #show.
51 | # TODO: remove in 5.0.
52 | it do
53 | Gem::Deprecate::skip_during do
54 | _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module], method: :detail).to_s).must_equal '* [Object, {}]* [Module, {}]'
55 | end
56 | end
57 |
58 | # ViewModel.cell(collection: []).() invokes #show.
59 | it { _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module]).()).must_equal '[Object, {}][Module, {}]' }
60 |
61 | # ViewModel.cell(collection: []).(:detail) invokes #detail instead of #show.
62 | it { _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module]).(:detail)).must_equal '* [Object, {}]* [Module, {}]' }
63 |
64 | # #cell(collection: [], genre: "Fusion").() doesn't change options hash.
65 | it do
66 | Cell::ViewModel.cell("public_test/song", options = { genre: "Fusion", collection: [Object] }).()
67 | _(options.to_s).must_equal "{:genre=>\"Fusion\", :collection=>[Object]}"
68 | end
69 |
70 | # it do
71 | # content = ""
72 | # Cell::ViewModel.cell("public_test/song", collection: [Object, Module]).each_with_index do |cell, i|
73 | # content += (i == 1 ? cell.(:detail) : cell.())
74 | # end
75 |
76 | # content.must_equal '[Object, {}]* [Module, {}]'
77 | # end
78 |
79 | # cell(collection: []).join captures return value and joins it for you.
80 | it do
81 | _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module]).join do |cell, i|
82 | i == 1 ? cell.(:detail) : cell.()
83 | end).must_equal '[Object, {}]* [Module, {}]'
84 | end
85 |
86 | # cell(collection: []).join("<") captures return value and joins it for you with join.
87 | it do
88 | _(Cell::ViewModel.cell("public_test/song", collection: [Object, Module]).join(">") do |cell, i|
89 | i == 1 ? cell.(:detail) : cell.()
90 | end).must_equal '[Object, {}]>* [Module, {}]'
91 | end
92 |
93 | # 'join' can be used without a block:
94 | it do
95 | _(Cell::ViewModel.cell(
96 | "public_test/song", collection: [Object, Module]
97 | ).join('---')).must_equal('[Object, {}]---[Module, {}]')
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/test/prefixes_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class BassistCell::FenderCell < Cell::ViewModel
4 | end
5 |
6 | class BassistCell::IbanezCell < BassistCell
7 | end
8 |
9 | class WannabeCell < BassistCell::IbanezCell
10 | end
11 |
12 | # engine: shopify
13 | # shopify/cart/cell
14 |
15 | class EngineCell < Cell::ViewModel
16 | self.view_paths << "/var/engine/app/cells"
17 | end
18 | class InheritingFromEngineCell < EngineCell
19 | end
20 |
21 | class PrefixesTest < MiniTest::Spec
22 | class SingerCell < Cell::ViewModel
23 | end
24 |
25 | class BackgroundVocalsCell < SingerCell
26 | end
27 |
28 | class ChorusCell < BackgroundVocalsCell
29 | end
30 |
31 | class GuitaristCell < SingerCell
32 | def self._local_prefixes
33 | ["stringer"]
34 | end
35 | end
36 |
37 | class BassistCell < SingerCell
38 | def self._local_prefixes
39 | super + ["basser"]
40 | end
41 | end
42 |
43 |
44 | describe "::controller_path" do
45 | it { _(::BassistCell.new(@controller).class.controller_path).must_equal "bassist" }
46 | it { _(SingerCell.new(@controller).class.controller_path).must_equal "prefixes_test/singer" }
47 | end
48 |
49 | describe "#_prefixes" do
50 | it { _(::BassistCell.new(@controller)._prefixes).must_equal ["test/fixtures/bassist"] }
51 | it { _(::BassistCell::FenderCell.new(@controller)._prefixes).must_equal ["app/cells/bassist_cell/fender"] }
52 | it { _(::BassistCell::IbanezCell.new(@controller)._prefixes).must_equal ["test/fixtures/bassist_cell/ibanez", "test/fixtures/bassist"] }
53 |
54 | it { _(SingerCell.new(@controller)._prefixes).must_equal ["app/cells/prefixes_test/singer"] }
55 | it { _(BackgroundVocalsCell.new(@controller)._prefixes).must_equal ["app/cells/prefixes_test/background_vocals", "app/cells/prefixes_test/singer"] }
56 | it { _(ChorusCell.new(@controller)._prefixes).must_equal ["app/cells/prefixes_test/chorus", "app/cells/prefixes_test/background_vocals", "app/cells/prefixes_test/singer"] }
57 |
58 | it { _(GuitaristCell.new(@controller)._prefixes).must_equal ["stringer", "app/cells/prefixes_test/singer"] }
59 | it { _(BassistCell.new(@controller)._prefixes).must_equal ["app/cells/prefixes_test/bassist", "basser", "app/cells/prefixes_test/singer"] }
60 | # it { DrummerCell.new(@controller)._prefixes.must_equal ["drummer", "stringer", "prefixes_test/singer"] }
61 |
62 | # multiple view_paths.
63 | it { _(EngineCell.prefixes).must_equal ["app/cells/engine", "/var/engine/app/cells/engine"] }
64 | it do
65 | _(InheritingFromEngineCell.prefixes).must_equal [
66 | "app/cells/inheriting_from_engine", "/var/engine/app/cells/inheriting_from_engine",
67 | "app/cells/engine", "/var/engine/app/cells/engine"]
68 | end
69 |
70 | # ::_prefixes is cached.
71 | it do
72 | _(WannabeCell.prefixes).must_equal ["test/fixtures/wannabe", "test/fixtures/bassist_cell/ibanez", "test/fixtures/bassist"]
73 | WannabeCell.instance_eval { def _local_prefixes; ["more"] end }
74 | # _prefixes is cached.
75 | _(WannabeCell.prefixes).must_equal ["test/fixtures/wannabe", "test/fixtures/bassist_cell/ibanez", "test/fixtures/bassist"]
76 | # superclasses don't get disturbed.
77 | _(::BassistCell.prefixes).must_equal ["test/fixtures/bassist"]
78 | end
79 | end
80 |
81 | # it { Record::Cell.new(@controller).render_state(:show).must_equal "Rock on!" }
82 | end
83 |
84 | class InheritViewsTest < MiniTest::Spec
85 | class SlapperCell < Cell::ViewModel
86 | self.view_paths = ['test/fixtures'] # todo: REMOVE!
87 | include Cell::Erb
88 |
89 | inherit_views ::BassistCell
90 |
91 | def play
92 | render
93 | end
94 | end
95 |
96 | class FunkerCell < SlapperCell
97 | end
98 |
99 | it { _(SlapperCell.new(nil)._prefixes).must_equal ["test/fixtures/inherit_views_test/slapper", "test/fixtures/bassist"] }
100 | it { _(FunkerCell.new(nil)._prefixes).must_equal ["test/fixtures/inherit_views_test/funker", "test/fixtures/inherit_views_test/slapper", "test/fixtures/bassist"] }
101 |
102 | # test if normal cells inherit views.
103 | it { _(cell('inherit_views_test/slapper').play).must_equal 'Doo' }
104 | it { _(cell('inherit_views_test/funker').play).must_equal 'Doo' }
105 |
106 |
107 | # TapperCell
108 | class TapperCell < Cell::ViewModel
109 | self.view_paths = ['test/fixtures']
110 | # include Cell::Erb
111 |
112 | def play
113 | render
114 | end
115 |
116 | def tap
117 | render
118 | end
119 | end
120 |
121 | class PopperCell < TapperCell
122 | end
123 |
124 | # Tapper renders its play
125 | it { _(cell('inherit_views_test/tapper').call(:play)).must_equal 'Dooom!' }
126 | # Tapper renders its tap
127 | it { _(cell('inherit_views_test/tapper').call(:tap)).must_equal 'Tap tap tap!' }
128 |
129 | # Popper renders Tapper's play
130 | it { _(cell('inherit_views_test/popper').call(:play)).must_equal 'Dooom!' }
131 | # Popper renders its tap
132 | it { _(cell('inherit_views_test/popper').call(:tap)).must_equal "TTttttap I'm not good enough!" }
133 | end
134 |
--------------------------------------------------------------------------------
/lib/cell/view_model.rb:
--------------------------------------------------------------------------------
1 | require "uber/delegates"
2 |
3 | module Cell
4 | class ViewModel
5 | extend Abstract
6 | abstract!
7 |
8 | extend Uber::InheritableAttr
9 | extend Uber::Delegates
10 |
11 | inheritable_attr :view_paths
12 | self.view_paths = ["app/cells"]
13 |
14 | class << self
15 | def templates
16 | @templates ||= Templates.new # note: this is shared in subclasses. do we really want this?
17 | end
18 | end
19 |
20 | include Prefixes
21 | extend Util
22 |
23 | def self.controller_path
24 | @controller_path ||= util.underscore(name.sub(/Cell$/, ''))
25 | end
26 |
27 | attr_reader :model
28 |
29 | module Helpers
30 | # Constantizes name if needed, call builders and returns instance.
31 | def cell(name, *args, **kws, &block) # classic Rails fuzzy API.
32 | constant = name.is_a?(Class) ? name : class_from_cell_name(name)
33 | constant.(*args, **kws, &block)
34 | end
35 | end
36 | extend Helpers
37 |
38 | class << self
39 | def property(*names)
40 | delegates :model, *names # Uber::Delegates.
41 | end
42 |
43 | # Public entry point. Use this to instantiate cells with builders.
44 | #
45 | # SongCell.(@song)
46 | # SongCell.(collection: Song.all)
47 | def call(model=nil, options={}, &block)
48 | if model.is_a?(Hash) and array = model[:collection]
49 | return Collection.new(array, model.merge(options), self)
50 | end
51 |
52 | build(model, options)
53 | end
54 |
55 | alias build new # semi-public for Cell::Builder
56 |
57 | private
58 | def class_from_cell_name(name)
59 | util.constant_for("#{name}_cell")
60 | end
61 | end
62 |
63 | # Build nested cell in instance.
64 | def cell(name, model=nil, options={})
65 | context = Context[options[:context], self.context]
66 |
67 | self.class.cell(name, model, options.merge(context: context))
68 | end
69 |
70 | def initialize(model=nil, options={})
71 | setup!(model, options)
72 | end
73 |
74 | def context
75 | @options[:context]
76 | end
77 |
78 | # DISCUSS: we could use the same mechanism as TRB::Skills here for speed at runtime?
79 | class Context# < Hash
80 | # Only dup&merge when :context was passed in parent.cell(context: ..)
81 | # Otherwise we can simply pass on the old context.
82 | def self.[](options, context)
83 | return context unless options
84 | context.dup.merge(options) # DISCUSS: should we create a real Context object here, to make it overridable?
85 | end
86 | end
87 |
88 | module Rendering
89 | # Invokes the passed method (defaults to :show) while respecting caching.
90 | # In Rails, the return value gets marked html_safe.
91 | def call(state=:show, *args, **kws, &block)
92 | content = render_state(state, *args, **kws, &block)
93 | content.to_s
94 | end
95 |
96 | # Since 4.1, you get the #show method for free.
97 | def show(&block)
98 | render(&block)
99 | end
100 |
101 | # render :show
102 | def render(options={}, &block)
103 | options = normalize_options(options)
104 | render_to_string(options, &block)
105 | end
106 |
107 | private
108 | def render_to_string(options, &block)
109 | template = find_template(options)
110 | render_template(template, options, &block)
111 | end
112 |
113 | def render_state(*args, **kws, &block)
114 | __send__(*args, **kws, &block) # Ruby 2.7+
115 | end
116 |
117 | def render_template(template, options, &block)
118 | template.render(self, options[:locals], &block) # DISCUSS: hand locals to layout?
119 | end
120 |
121 | module RubyPre2_7_RenderState
122 | def render_state(*args, **kws, &block)
123 | args = args + [kws] if kws.any?
124 | __send__(*args, &block)
125 | end
126 | end
127 | end
128 |
129 | include Rendering
130 | include Rendering::RubyPre2_7_RenderState if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0')
131 | include Inspect
132 |
133 | def to_s
134 | call
135 | end
136 | include Caching
137 |
138 | private
139 | attr_reader :options
140 |
141 | def setup!(model, options)
142 | @model = model
143 | @options = options
144 | # or: create_twin(model, options)
145 | end
146 |
147 | module TemplateFor
148 | def find_template(options)
149 | template_options = template_options_for(options) # imported by Erb, Haml, etc.
150 | # required options: :template_class, :suffix. everything else is passed to the template implementation.
151 |
152 | view = options[:view]
153 | prefixes = options[:prefixes]
154 | suffix = template_options.delete(:suffix)
155 | view = "#{view}.#{suffix}"
156 |
157 | template_for(prefixes, view, template_options) or raise TemplateMissingError.new(prefixes, view)
158 | end
159 |
160 | def template_for(prefixes, view, options)
161 | # we could also pass _prefixes when creating class.templates, because prefixes are never gonna change per instance. not too sure if i'm just assuming this or if people need that.
162 | # Note: options here is the template-relevant options, only.
163 | self.class.templates[prefixes, view, options]
164 | end
165 | end
166 | include TemplateFor
167 |
168 | def normalize_options(options)
169 | options = if options.is_a?(Hash)
170 | options[:view] ||= state_for_implicit_render(options)
171 | options
172 | else
173 | {view: options.to_s}
174 | end
175 |
176 | options[:prefixes] ||= _prefixes
177 |
178 | process_options!(options)
179 | options
180 | end
181 |
182 | # Overwrite #process_options in included feature modules, but don't forget to call +super+.
183 | module ProcessOptions
184 | def process_options!(options)
185 | end
186 | end
187 | include ProcessOptions
188 |
189 | # Computes the view name from the call stack in which `render` was invoked.
190 | def state_for_implicit_render(options)
191 | caller(3, 1)[0].match(/`(\w+)/)[1]
192 | end
193 |
194 | include Layout
195 | end
196 | end
197 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cells
2 |
3 | *View Components for Ruby and Rails.*
4 |
5 | [](https://trailblazer.zulipchat.com/login/)
6 | [](https://trailblazer.to/2.0/newsletter.html)
7 | [](https://travis-ci.org/trailblazer/cells)
9 | [](http://badge.fury.io/rb/cells)
10 |
11 | ## Overview
12 |
13 | Cells allow you to encapsulate parts of your UI into components into _view models_. View models, or cells, are simple ruby classes that can render templates.
14 |
15 | Nevertheless, a cell gives you more than just a template renderer. They allow proper OOP, polymorphic builders, [nesting](#nested-cells), view inheritance, using Rails helpers, [asset packaging](https://trailblazer.to/2.1/docs/cells.html#cells-rails-asset-pipeline) to bundle JS, CSS or images, simple distribution via gems or Rails engines, encapsulated testing, [caching](#caching), and [integrate with Trailblazer](https://github.com/trailblazer/trailblazer-cells).
16 |
17 | ## Full Documentation
18 |
19 | Cells is part of the Trailblazer framework. [Full documentation](https://trailblazer.to/2.1/docs/cells.html) is available on the project site.
20 |
21 | Cells is completely decoupled from Rails. However, Rails-specific functionality is to be found [here](https://trailblazer.to/2.1/docs/cells.html#cells-rails).
22 |
23 | ## Rendering Cells
24 |
25 | You can render cells anywhere and as many as you want, in views, controllers, composites, mailers, etc.
26 |
27 | Rendering a cell in Rails ironically happens via a helper.
28 |
29 | ```ruby
30 | <%= cell(:comment, @comment) %>
31 | ```
32 |
33 | This boils down to the following invocation, that can be used to render cells in *any other Ruby* environment.
34 |
35 | ```ruby
36 | CommentCell.(@comment).()
37 | ```
38 |
39 | You can also pass the cell class in explicitly:
40 |
41 | ```ruby
42 | <%= cell(CommentCell, @comment) %>
43 | ```
44 |
45 | In Rails you have the same helper API for views and controllers.
46 |
47 | ```ruby
48 | class DashboardController < ApplicationController
49 | def dashboard
50 | @comments = cell(:comment, collection: Comment.recent)
51 | @traffic = cell(:report, TrafficReport.find(1)).()
52 | end
53 | ```
54 |
55 | Usually, you'd pass in one or more objects you want the cell to present. That can be an ActiveRecord model, a ROM instance or any kind of PORO you fancy.
56 |
57 | ## Cell Class
58 |
59 | A cell is a light-weight class with one or multiple methods that render views.
60 |
61 | ```ruby
62 | class CommentCell < Cell::ViewModel
63 | property :body
64 | property :author
65 |
66 | def show
67 | render
68 | end
69 |
70 | private
71 | def author_link
72 | link_to "#{author.email}", author
73 | end
74 | end
75 | ```
76 |
77 | Here, `show` is the only public method. By calling `render` it will invoke rendering for the `show` view.
78 |
79 |
80 | ## Logicless Views
81 |
82 | Views come packaged with the cell and can be ERB, Haml, or Slim.
83 |
84 | ```erb
85 | New Comment
86 | <%= body %>
87 |
88 | By <%= author_link %>
89 | ```
90 |
91 | The concept of "helpers" that get strangely copied from modules to the view does not exist in Cells anymore.
92 |
93 | Methods called in the view are directly called _on the cell instance_. You're free to use loops and deciders in views, even instance variables are allowed, but Cells tries to push you gently towards method invocations to access data in the view.
94 |
95 | ## File Structure
96 |
97 | In Rails, cells are placed in `app/cells` or `app/concepts/`. Every cell has their own directory where it keeps views, assets and code.
98 |
99 | ```
100 | app
101 | ├── cells
102 | │ ├── comment_cell.rb
103 | │ ├── comment
104 | │ │ ├── show.haml
105 | │ │ ├── list.haml
106 | ```
107 |
108 | The discussed `show` view would reside in `app/cells/comment/show.haml`. However, you can set [any set of view paths](#view-paths) you want.
109 |
110 |
111 | ## Invocation Styles
112 |
113 | In order to make a cell render, you have to call the rendering methods. While you could call the method directly, the preferred way is the _call style_.
114 |
115 | ```ruby
116 | cell(:comment, @song).() # calls CommentCell#show.
117 | cell(:comment, @song).(:index) # calls CommentCell#index.
118 | ```
119 |
120 | The call style respects caching.
121 |
122 | Keep in mind that `cell(..)` really gives you the cell object. In case you want to reuse the cell, need setup logic, etc. that's completely up to you.
123 |
124 | ## Parameters
125 |
126 | You can pass in as many parameters as you need. Per convention, this is a hash.
127 |
128 | ```ruby
129 | cell(:comment, @song, volume: 99, genre: "Jazz Fusion")
130 | ```
131 |
132 | Options can be accessed via the `@options` instance variable.
133 |
134 | Naturally, you may also pass arbitrary options into the call itself. Those will be simple method arguments.
135 |
136 | ```ruby
137 | cell(:comment, @song).(:show, volume: 99)
138 | ```
139 |
140 | Then, the `show` method signature changes to `def show(options)`.
141 |
142 |
143 | ## Testing
144 |
145 | A huge benefit from "all this encapsulation" is that you can easily write tests for your components. The API does not change and everything is exactly as it would be in production.
146 |
147 | ```ruby
148 | html = CommentCell.(@comment).()
149 | Capybara.string(html).must_have_css "h3"
150 | ```
151 |
152 | It is completely up to you how you test, whether it's RSpec, MiniTest or whatever. All the cell does is return HTML.
153 |
154 | [In Rails, there's support](https://trailblazer.to/2.1/docs/cells.html#cells-testing) for TestUnit, MiniTest and RSpec available, along with Capybara integration.
155 |
156 | ## Properties
157 |
158 | The cell's model is available via the `model` reader. You can have automatic readers to the model's fields by using `::property`.
159 |
160 | ```ruby
161 | class CommentCell < Cell::ViewModel
162 | property :author # delegates to model.author
163 |
164 | def author_link
165 | link_to author.name, author
166 | end
167 | end
168 | ```
169 |
170 | ## HTML Escaping
171 |
172 | Cells per default does no HTML escaping, anywhere. Include `Escaped` to make property readers return escaped strings.
173 |
174 | ```ruby
175 | class CommentCell < Cell::ViewModel
176 | include Escaped
177 |
178 | property :title
179 | end
180 |
181 | song.title #=> ""
182 | Comment::Cell.(song).title #=> <script>Dangerous</script>
183 | ```
184 |
185 | Properties and escaping are [documented here](https://trailblazer.to/2.1/docs/cells.html#cells-api-html-escaping).
186 |
187 | ## Installation
188 |
189 | Cells runs with any framework.
190 |
191 | ```ruby
192 | gem "cells"
193 | ```
194 |
195 | For Rails, please use the [cells-rails](https://github.com/trailblazer/cells-rails) gem. It supports Rails >= 4.0.
196 |
197 | ```ruby
198 | gem "cells-rails"
199 | ```
200 |
201 | Lower versions of Rails will still run with Cells, but you will get in trouble with the helpers. (Note: we use Cells in production with Rails 3.2 and Haml and it works great.)
202 |
203 | Various template engines are supported but need to be added to your Gemfile.
204 |
205 | * [cells-erb](https://github.com/trailblazer/cells-erb)
206 | * [cells-hamlit](https://github.com/trailblazer/cells-hamlit) We strongly recommend using [Hamlit](https://github.com/k0kubun/hamlit) as a Haml replacement.
207 | * [cells-haml](https://github.com/trailblazer/cells-haml) Make sure to bundle Haml 4.1: `gem "haml", github: "haml/haml", ref: "7c7c169"`. Use `cells-hamlit` instead.
208 | * [cells-slim](https://github.com/trailblazer/cells-slim)
209 |
210 | ```ruby
211 | gem "cells-erb"
212 | ```
213 |
214 | In Rails, this is all you need to do. In other environments, you need to include the respective module into your cells.
215 |
216 | ```ruby
217 | class CommentCell < Cell::ViewModel
218 | include ::Cell::Erb # or Cell::Hamlit, or Cell::Haml, or Cell::Slim
219 | end
220 | ```
221 |
222 | ## Namespaces
223 |
224 | Cells can be namespaced as well.
225 |
226 | ```ruby
227 | module Admin
228 | class CommentCell < Cell::ViewModel
229 | ```
230 |
231 | Invocation in Rails would happen as follows.
232 |
233 | ```ruby
234 | cell("admin/comment", @comment).()
235 | ```
236 |
237 | Views will be searched in `app/cells/admin/comment` per default.
238 |
239 |
240 | ## Rails Helper API
241 |
242 | Even in a non-Rails environment, Cells provides the Rails view API and allows using all Rails helpers.
243 |
244 | You have to include all helper modules into your cell class. You can then use `link_to`, `simple_form_for` or whatever you feel like.
245 |
246 | ```ruby
247 | class CommentCell < Cell::ViewModel
248 | include ActionView::Helpers::UrlHelper
249 | include ActionView::Helpers::CaptureHelper
250 |
251 | def author_link
252 | content_tag :div, link_to(author.name, author)
253 | end
254 | ```
255 |
256 | As always, you can use helpers in cells and in views.
257 |
258 | You might run into problems with wrong escaping or missing URL helpers. This is not Cells' fault but Rails suboptimal way of implementing and interfacing their helpers. Please open the actionview gem helper code and try figuring out the problem yourself before bombarding us with issues because helper `xyz` doesn't work.
259 |
260 |
261 | ## View Paths
262 |
263 | In Rails, the view path is automatically set to `app/cells/` or `app/concepts/`. You can append or set view paths by using `::view_paths`. Of course, this works in any Ruby environment.
264 |
265 | ```ruby
266 | class CommentCell < Cell::ViewModel
267 | self.view_paths = "lib/views"
268 | end
269 | ```
270 |
271 | ## Asset Packaging
272 |
273 | Cells can easily ship with their own JavaScript, CSS and more and be part of Rails' asset pipeline. Bundling assets into a cell allows you to implement super encapsulated widgets that are stand-alone. Asset pipeline is [documented here](https://trailblazer.to/2.1/docs/cells.html#cells-rails-asset-pipeline).
274 |
275 | ## Render API
276 |
277 | Unlike Rails, the `#render` method only provides a handful of options you gotta learn.
278 |
279 | ```ruby
280 | def show
281 | render
282 | end
283 | ```
284 |
285 | Without options, this will render the state name, e.g. `show.erb`.
286 |
287 | You can provide a view name manually. The following calls are identical.
288 |
289 | ```ruby
290 | render :index
291 | render view: :index
292 | ```
293 |
294 | If you need locals, pass them to `#render`.
295 |
296 | ```ruby
297 | render locals: {style: "border: solid;"}
298 | ```
299 |
300 | ## Layouts
301 |
302 | Every view can be wrapped by a layout. Either pass it when rendering.
303 |
304 | ```ruby
305 | render layout: :default
306 | ```
307 |
308 | Or configure it on the class-level.
309 |
310 | ```ruby
311 | class CommentCell < Cell::ViewModel
312 | layout :default
313 | ```
314 |
315 | The layout is treated as a view and will be searched in the same directories.
316 |
317 |
318 | ## Nested Cells
319 |
320 | Cells love to render. You can render as many views as you need in a cell state or view.
321 |
322 | ```ruby
323 | <%= render :index %>
324 | ```
325 |
326 | The `#render` method really just returns the rendered template string, allowing you all kind of modification.
327 |
328 | ```ruby
329 | def show
330 | render + render(:additional)
331 | end
332 | ```
333 |
334 | You can even render other cells _within_ a cell using the exact same API.
335 |
336 | ```ruby
337 | def about
338 | cell(:profile, model.author).()
339 | end
340 | ```
341 |
342 | This works both in cell views and on the instance, in states.
343 |
344 |
345 | ## View Inheritance
346 |
347 | You can not only inherit code across cell classes, but also views. This is extremely helpful if you want to override parts of your UI, only. It's [documented here](https://trailblazer.to/2.1/docs/cells.html#cells-api-view-inheritance).
348 |
349 | ## Collections
350 |
351 | In order to render collections, Cells comes with a shortcut.
352 |
353 | ```ruby
354 | comments = Comment.all #=> three comments.
355 | cell(:comment, collection: comments).()
356 | ```
357 |
358 | This will invoke `cell(:comment, comment).()` three times and concatenate the rendered output automatically.
359 |
360 | Learn more [about collections here](https://trailblazer.to/2.1/docs/cells.html#cells-api-collection).
361 |
362 |
363 | ## Builder
364 |
365 | Builders allow instantiating different cell classes for different models and options. They introduce polymorphism into cells.
366 |
367 | ```ruby
368 | class CommentCell < Cell::ViewModel
369 | include ::Cell::Builder
370 |
371 | builds do |model, options|
372 | case model
373 | when Post; PostCell
374 | when Comment; CommentCell
375 | end
376 | end
377 | ```
378 |
379 | The `#cell` helper takes care of instantiating the right cell class for you.
380 |
381 | ```ruby
382 | cell(:comment, Post.find(1)) #=> creates a PostCell.
383 | ```
384 |
385 | Learn more [about builders here](https://trailblazer.to/2.1/docs/cells.html#cells-api-builder).
386 |
387 | ## Caching
388 |
389 | For every cell class you can define caching per state. Without any configuration the cell will run and render the state once. In following invocations, the cached fragment is returned.
390 |
391 | ```ruby
392 | class CommentCell < Cell::ViewModel
393 | cache :show
394 | # ..
395 | end
396 | ```
397 |
398 | The `::cache` method will forward options to the caching engine.
399 |
400 | ```ruby
401 | cache :show, expires_in: 10.minutes
402 | ```
403 |
404 | You can also compute your own cache key, use dynamic keys, cache tags, and conditionals using `:if`. Caching is documented [here](https://trailblazer.to/2.1/docs/cells.html#cells-api-caching) and in chapter 8 of the [Trailblazer book](http://leanpub.com/trailblazer).
405 |
406 |
407 | ## The Book
408 |
409 | Cells is part of the [Trailblazer project](https://github.com/apotonick/trailblazer). Please [buy my book](https://leanpub.com/trailblazer) to support the development and to learn all the cool stuff about Cells. The book discusses many use cases of Cells.
410 |
411 |
412 | 
413 |
414 |
415 | * Basic view models, replacing helpers, and how to structure your view into cell components (chapter 2 and 4).
416 | * Advanced Cells API (chapter 4 and 6).
417 | * Testing Cells (chapter 4 and 6).
418 | * Cells Pagination with AJAX (chapter 6).
419 | * View Caching and Expiring (chapter 8).
420 |
421 | The book picks up where the README leaves off. Go grab a copy and support us - it talks about object- and view design and covers all aspects of the API.
422 |
423 | ## This is not Cells 3.x!
424 |
425 | Temporary note: This is the README and API for Cells 4. Many things have improved. If you want to upgrade, [follow this guide](https://github.com/apotonick/cells/wiki/From-Cells-3-to-Cells-4---Upgrading-Guide). When in trouble, join the [Zulip channel](https://trailblazer.zulipchat.com/login/).
426 |
427 | ## LICENSE
428 |
429 | Copyright (c) 2007-2020, Nick Sutterer
430 |
431 | Copyright (c) 2007-2008, Solide ICT by Peter Bex and Bob Leers
432 |
433 | Released under the MIT License.
434 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | ## 4.5.0
2 |
3 | * Drop support for rubies prior 2.2.10
4 |
5 | ## 4.1.8
6 |
7 | * Fix ruby warning with undefined instance variable in codebase
8 |
9 | ## 4.1.7
10 |
11 | * `Collection#join` can now be called without a block.
12 |
13 | ## 4.1.6
14 |
15 | * Use `Declarative::Option` and `Declarative::Builder` instead of `uber`'s. This allows removing the `uber` version restriction.
16 |
17 | ## 4.1.5
18 |
19 | * Fix a bug where nested calls of `cell(name, context: {...})` would ignore the new context elements, resulting in the old context being passed on. By adding `Context::[]` the new elements are now properly merged into a **new context hash**. This means that adding elements to the child context won't leak up into the parent context anymore.
20 |
21 | ## 4.1.4
22 |
23 | * Upgrading to Uber 0.1 which handles builders a bit differently.
24 |
25 | ## 4.1.3
26 |
27 | * Load `Uber::InheritableAttr` in `Testing` to fix a bug in `cells-rspec`.
28 |
29 | ## 4.1.2
30 |
31 | * Testing with Rails 5 now works, by removing code the last piece of Rails-code (I know, it sounds bizarre).
32 |
33 | ## 4.1.1
34 |
35 | * Fix rendering of `Collection` where in some environments (Rails), the overridden `#call` method wouldn't work and strings would be escaped.
36 |
37 | ## 4.1.0
38 |
39 | ### API Fix/Changes
40 |
41 | * All Rails code removed. Make sure to use [Cells-rails](https://github.com/trailblazer/cells-rails) if you want the old Rails behavior.
42 | * The `builds` feature is now optional, you have to include `Builder` in your cell.
43 | ```ruby
44 | class CommentCell < Cell::ViewModel
45 | include Cell::Builder
46 |
47 | builds do |..|
48 | ```
49 |
50 | * A basic, rendering `#show` method is now provided automatically.
51 | * `ViewModel#render` now accepts a block that can be `yield`ed in the view.
52 | * Passing a block to `ViewModel#call` changed. Use `tap` if you want the "old" behavior (which was never official or documented).
53 | ```ruby
54 | Comment::Cell.new(comment).().tap { |cell| }
55 | ```
56 | The new behavior is to pass that block to your state method. You can pass it on to `render`, and then `yield` it in the template.
57 |
58 | ```ruby
59 | def show(&block)
60 | render &block # yield in show.haml
61 | end
62 | ```
63 |
64 | Note that this happens automatically in the default `ViewModel#show` method.
65 | * `Concept#cell` now will resolve a concept cell (`Song::Cell`), and not the old-style suffix cell (`SongCell`). The same applies to `Concept#concept`.
66 |
67 | ```ruby
68 | concept("song/cell", song).cell("song/cell/composer") #=> resolves to Song::Cell::Composer
69 | ```
70 | This decision has been made in regards of the upcoming Cells 5. It simplifies code dramatically, and we consider it unnatural to mix concept and suffix cells in applications.
71 | * In case you were using `@parent_controller`, this doesn't exist anymore (and was never documented, either). Use `context[:controller]`.
72 | * `::self_contained!` is no longer included into `ViewModel`. Please try using `Trailblazer::Cell` instead. If you still need it, here's how.
73 |
74 | ```ruby
75 | class SongCell < Cell::ViewModel
76 | extend SelfContained
77 | self_contained!
78 | ```
79 | * `Cell::Concept` is deprecated and you should be using the excellent [`Trailblazer::Cell`](https://github.com/trailblazer/trailblazer-cells) class instead, because that's what a concept cell tries to be in an awkward way. The latter is usable without Trailblazer.
80 |
81 | We are hereby dropping support for `Cell::Concept` (it still works).
82 |
83 | * Deprecating `:collection_join` and `:method` for collections.
84 |
85 | ### Awesomeness
86 |
87 | * Introduced the concept of a context object that is being passed to all nested cells. This object is supposed to contain dependencies such as `current_user`, in Rails it contains the "parent_controller" under the `context[:controller]` key.
88 |
89 | Simple provide it as an option when rendering the cell.
90 |
91 | ```ruby
92 | cell(:song, song, context: { current_user: current_user })
93 | ```
94 |
95 | The `#context` method allows to access this very hash.
96 |
97 | ```ruby
98 | def role
99 | context[:current_user].admin? "Admin" : "A nobody"
100 | end
101 | ```
102 | * The `cell` helper now allows to pass in a constant, too.
103 |
104 | ```ruby
105 | cell(Song::Cell, song)
106 | ```
107 | * New API for `:collection`. If used in views, this happens automatically, but here's how it works now.
108 |
109 | ```ruby
110 | cell(:comment, collection: Comment.all).() # will invoke show.
111 | cell(:comment, collection: Comment.all).(:item) # will invoke item.
112 | cell(:comment, collection: Comment.all).join { |cell, i| cell.show(index: i) }
113 | ```
114 | Basically, a `Collection` instance is returned that optionally allows to invoke each cell manually.
115 | * Layout cells can now be injected to wrap the original content.
116 | ```ruby
117 | cell(:comment, Comment.find(1), layout: LayoutCell)
118 | ```
119 |
120 | The LayoutCell will be instantiated and the `show` state invoked. The content cell's content is passed as a block, allowing the layout's view to `yield`.
121 |
122 | This works with `:collection`, too.
123 |
124 | ## 4.0.5
125 |
126 | * Fix `Testing` so you can use Capybara matchers on `cell(:song, collection: [..])`.
127 |
128 | ## 4.0.4
129 |
130 | * `Escaped::property` now properly escapes all passed properties. Thanks @xzo and @jlogsdon!
131 |
132 | ## 4.0.3
133 |
134 | * `Cell::Partial` now does _append_ the global partial path to its `view_paths` instead of using `unshift` and thereby removing possible custom paths.
135 | * Adding `Cell::Translation` which allows using the `#t` helper. Thanks to @johnlane.
136 | * Performance improvement: when inflecting the view name (90% likely to be done) the `caller` is now limited to the data we need, saving memory. Thanks @timoschilling for implementing this.
137 | * In the `concept` helper, we no longer use `classify`, which means you can say `concept("comment/data")` and it will instantiate `Comment::Data` and not `Comment::Datum`. Thanks @firedev!
138 |
139 | ## 4.0.2
140 |
141 | * In Rails, include `ActionView::Helpers::FormHelper` into `ViewModel` so we already have (and pollute our cell with) `UrlHelper` and `FormTagHelper`. Helpers, so much fun.
142 | * Concept cells will now infer their name properly even if the string `Cell` appears twice.
143 |
144 | ## 4.0.1
145 |
146 | * Support forgery protection in `form_tag`.
147 |
148 | ## 4.0.0
149 |
150 | * **Rails Support:** Rails 4.0+ is fully supported, in older versions some form helpers do not work. Let us know how you fixed this.
151 | * **State args:** View models don't use state args. Options are passed into the constructor and saved there. That means that caching callbacks no longer receive arguments as everything is available via the instance itself.
152 | * `ViewModel.new(song: song)` won't automatically create a reader `#song`. You have to configure the cell to use a Struct twin {TODO: document}
153 | * **HTML Escaping:** Escaping only happens for defined `property`s when `Escaped` is included.
154 | * **Template Engines:** There's now _one_ template engine (e.g. ERB or HAML) per cell class. It can be set by including the respective module (e.g. `Cell::Erb`) into the cell class. This happens automatically in Rails.
155 | * **File Naming**. The default filename just uses the engine suffix, e.g. `show.haml`. If you have two different engine formats (e.g. `show.haml` and `show.erb`), use the `format:` option: `render format: :erb`.
156 | If you need to render a specific mime type, provide the filename: `render view: "show.html"`.
157 | * Builder blocks are no longer executed in controller context but in the context they were defined. This is to remove any dependencies to the controller. If you need e.g. `params`, pass them into the `#cell(..)` call.
158 | * Builders are now defined using `::builds`, not `::build`.
159 |
160 | ### Removed
161 |
162 | * `Cell::Rails` and `Cell::Base` got removed. Every cell is `ViewModel` or `Concept` now.
163 | * All methods from `AbstractController` are gone. This might give you trouble in case you were using `helper_method`. You don't need this anymore - every method included in the cell class is a "helper" in the view (it's one and the same method call).
164 |
165 |
166 | ## 4.0.0.rc2
167 |
168 | * Include `#protect_from_forgery?` into Rails cells. It returns false currently.
169 | * Fix `Concept#cell` which now instantiates a cell, not a concept cell.
170 |
171 | ## 4.0.0.rc1
172 |
173 | * Move delegations of `#url_options` etc. to the railtie, which makes it work.
174 |
175 | ## 4.0.0.beta6
176 |
177 | * Removed `ViewModel::template_engine`. This is now done explicitly by including `Cell::Erb`, etc. and happens automatically in a Rails environment.
178 |
179 | ## 4.0.0.beta5
180 |
181 | * Assets bundled in engine cells now work.
182 | * Directory change: Assets like `.css`, `.coffee` and `.js`, no longer have their own `assets/` directory but live inside the views directory of a cell. It turned out that two directories `views/` and `assets/` was too noisy for most users. If you think you have a valid point for re-introducing it, email me, it is not hard to implement.
183 | * When bundling your cell's assets into the asset pipeline, you have to specify the full name of your cell. The names will be constantized.
184 |
185 | ```ruby
186 | config.cells.with_assets = ["song/cell", "user_cell"] #=> Song::Cell, UserCell
187 | ```
188 | * `ViewModel` is now completely decoupled from Rails and doesn't inherit from AbstractController anymore.
189 | * API change: The controller dependency is now a second-class citizen being passed into the cell via options.
190 |
191 | ```ruby
192 | Cell.new(model, {controller: ..})
193 | ```
194 | * Removing `actionpack` from gemspec.
195 |
196 | ## 4.0.0.beta4
197 |
198 | * Fixed a bug when rendering more than once with ERB, the output buffer was being reused.
199 | * API change: ViewModel::_prefixes now returns the "fully qualified" pathes including the view paths, prepended to the prefixes. This allows multiple view paths and basically fixes cells in engines.
200 | * The only public way to retrieve prefixes for a cell is `ViewModel::prefixes`. The result is cached.
201 |
202 |
203 | ## 4.0.0.beta3
204 |
205 | * Introduce `Cell::Testing` for Rspec and MiniTest.
206 | * Add ViewModel::OutputBuffer to be used in Erbse and soon in Haml.
207 |
208 | ## 3.11.2
209 |
210 | * `ViewModel#call` now accepts a block and yields `self` (the cell instance) to it. This is handy to use with `content_for`.
211 | ```ruby
212 | = cell(:song, Song.last).call(:show) do |cell|
213 | content_for :footer, cell.footer
214 | ```
215 |
216 | ## 3.11.1
217 |
218 | * Override `ActionView::Helpers::UrlHelper#url_for` in Rails 4.x as it is troublesome. That removes the annoying
219 | `arguments passed to url_for can't be handled. Please require routes or provide your own implementation`
220 | exception when using simple_form, form_for, etc with a view model.
221 |
222 |
223 | ## 3.11.0
224 |
225 | * Deprecated `Cell::Rails::ViewModel`, please inherit: `class SongCell < Cell::ViewModel`.
226 | * `ViewModel#call` is now the prefered way to invoke the rendering flow. Without any argument, `call` will run `render_state(:show)`. Pass in any method name you want.
227 | * Added `Caching::Notifications`.
228 | * Added `cell(:song, collection: [song1, song2])` to render collections. This only works with ViewModel (and, of course, Concept, too).
229 | * Added `::inherit_views` to only inherit views whereas real class inheritance would inherit all the dark past of the class.
230 | * `::build_for` removed/privatized/changed. Use `Cell::Base::cell_for` instead.
231 | * `Base::_parent_prefixes` is no longer used, if you override that somewhere in your cells it will break. We have our own implementation for computing the controller's prefixes in `Cell::Base::Prefixes` (simpler).
232 | * `#expire_cell_state` doesn't take symbols anymore, only the real cell class name.
233 | * Remove `Cell::Base.setup_view_paths!` and `Cell::Base::DEFAULT_VIEW_PATHS` and the associated Railtie. I don't know why this code survived 3 major versions, if you wanna set you own view paths just use `Cell::Base.view_paths=`.
234 | * Add `Base::self_contained!`.
235 | * Add `Base::inherit_views`.
236 |
237 | ### Concept
238 | * `#concept` helper is mixed into all views as an alternative to `#cell` and `#render_cell`. Let us know if we should do that conditionally, only.
239 | * Concept cells look for layouts in their self-contained views directory.
240 | * Add generator for Concept cells: `rails g concept Comment`
241 |
242 |
243 | ## 3.10.1
244 | Allow packaging assets for Rails' asset pipeline into cells. This is still experimental but works great. I love it.
245 |
246 | ## 3.10.0
247 |
248 | * API CHANGE: Blocks passed to `::cache` and `::cache ... if: ` no longer receive the cell instance as the first argument. Instead, they're executed in cell instance context. Change your code like this:
249 | ```ruby
250 | cache :show do |cell, options|
251 | cell.version
252 | end
253 | # and
254 | cache :show, if: lambda {|cell, options| .. }
255 | ```
256 | should become
257 |
258 | ```ruby
259 | cache :show do |options|
260 | version
261 | end
262 | # and
263 | cache :show, if: lambda {|options| .. }
264 | ```
265 |
266 | Since the blocks are run in cell context, `self` will point to what was `cell` before.
267 |
268 |
269 | * `::cache` doesn't accept a `Proc` instance anymore, only blocks (was undocumented anyway).
270 | * Use [`uber` gem](https://github.com/apotonick/uber) for inheritable class attributes and dynamic options.
271 |
272 | ## 3.9.2
273 |
274 | * Autoload `Cell::Rails::ViewModel`.
275 | * Implement dynamic cache options by allowing lambdas that are executed at render-time - Thanks to @bibendi for this idea.
276 |
277 | ## 3.9.1
278 |
279 | * Runs with Rails 4.1 now.
280 | * Internal changes on `Layouts` to prepare 4.1 compat.
281 |
282 | ## 3.9.0
283 |
284 | * Cells in engines are now recognized under Rails 4.0.
285 | * Introducing @#cell@ and @#cell_for@ to instantiate cells in ActionController and ActionView.
286 | * Adding @Cell::Rails::ViewModel@ as a new "dialect" of working with cells.
287 | * Add @Cell::Base#process_args@ which is called in the initializer to handle arguments passed into the constructor.
288 | * Setting @controller in your @Cell::TestCase@ no longer get overridden by us.
289 |
290 | ## 3.8.8
291 |
292 | * Maintenance release.
293 |
294 | ## 3.8.7
295 |
296 | * Cells runs with Rails 4.
297 |
298 | ## 3.8.6
299 |
300 | * @cell/base@ can now be required without trouble.
301 | * Generated test files now respect namespaced cells.
302 |
303 | ## 3.8.5
304 |
305 | * Added @Cell::Rails::HelperAPI@ module to provide the entire Rails view "API" (quotes on purpose!) in cells running completely outside of Rails. This makes it possible to use gems like simple_form in any Ruby environment, especially interesting for people using Sinatra, webmachine, etc.
306 | * Moved @Caching.expire_cache_key@ to @Rails@. Use @Caching.expire_cache_key_for(key, cache_store, ..)@ if you want to expire caches outside of Rails.
307 |
308 | ## 3.8.4
309 |
310 | * Added @Cell::Rack@ for request-dependent Cells. This is also the new base class for @Cells::Rails@.
311 | * Removed deprecation warning from @TestCase#cell@ as it's signature is not deprecated.
312 | * Added the @base_cell_class@ config option to generator for specifying an alternative base class.
313 |
314 | ## 3.8.3
315 |
316 | * Added @Engines.existent_directories_for@ to prevent Rails 3.0 from crashing when it detects engines.
317 |
318 | ## 3.8.2
319 |
320 | * Engines should work in Rails 3.0 now, too.
321 |
322 | ## 3.8.1
323 |
324 | * Make it work with Rails 3.2 by removing deprecated stuff.
325 |
326 | ## 3.8.0
327 |
328 | * @Cell::Base@ got rid of the controller dependency. If you want the @ActionController@ instance around in your cell, use @Cell::Rails@ - this should be the default in a standard Rails setup. However, if you plan on using a Cell in a Rack middleware or don't need the controller, use @Cell::Base@.
329 | * New API (note that @controller@ isn't the first argument anymore):
330 | ** @Rails.create_cell_for(name, controller)@
331 | ** @Rails.render_cell_for(name, state, controller, *args)@
332 | * Moved builder methods to @Cell::Builder@ module.
333 | * @DEFAULT_VIEW_PATHS@ is now in @Cell::Base@.
334 | * Removed the monkey-patch that made state-args work in Rails <= 3.0.3. Upgrade to +3.0.4.
335 |
336 | ## 3.7.1
337 |
338 | * Works with Rails 3.2, too. Hopefully.
339 |
340 | ## 3.7.0
341 |
342 | h3. Changes
343 | * Cache settings using @Base.cache@ are now inherited.
344 | * Removed @opts.
345 | * Removed @#options@ in favor of state-args. If you still want the old behaviour, include the @Deprecations@ module in your cell.
346 | * The build process is now instantly delegated to Base.build_for on the concrete cell class.
347 |
348 | ## 3.6.8
349 |
350 | h3. Changes
351 | * Removed @opts.
352 | * Deprecated @#options@ in favour of state-args.
353 |
354 | ## 3.6.7
355 |
356 | h3. Changes
357 | * Added @view_assigns@ to TestCase.
358 |
359 | ## 3.6.6
360 |
361 | h3. Changes
362 | * Added the @:format@ option for @#render@ which should be used with caution. Sorry for that.
363 | * Removed the useless @layouts/@ view path from Cell::Base.
364 |
365 | ## 3.6.5
366 |
367 | h3. Bugfixes
368 | * `Cell::TestCase#invoke` now properly accepts state-args.
369 |
370 | h3. Changes
371 | * Added the `:if` option to `Base.cache` which allows adding a conditional proc or instance method to the cache definition. If it doesn't return true, caching for that state is skipped.
372 |
373 |
374 | ## 3.6.4
375 |
376 | h3. Bugfixes
377 | * Fixes @ArgumentError: wrong number of arguments (1 for 0)@ in @#render_cell@ for Ruby 1.8.
378 |
379 |
380 | ## 3.6.3
381 |
382 | h3. Bugfixes
383 | * [Rails 3.0] Helpers are now properly included (only once). Thanks to [paneq] for a fix.
384 | * `#url_options` in the Metal module is now delegated to `parent_controller` which propagates global URL setting like relative URLs to your cells.
385 |
386 | h3. Changes
387 | * `cells/test_case` is no longer required as it should be loaded automatically.
388 |
389 |
390 | ## 3.6.2
391 |
392 | h3. Bugfixes
393 | * Fixed cells.gemspec to allow Rails 3.x.
394 |
395 | ## 3.6.1
396 |
397 | h3. Changes
398 | * Added the @:format@ option allowing @#render@ to set different template types, e.g. @render :format => :json@.
399 |
400 |
401 | ## 3.6.0
402 |
403 | h3. Changes
404 | * Cells runs with Rails 3.0 and 3.1.
405 |
406 |
407 | ## 3.5.6
408 |
409 | h3. Changes
410 | * Added a generator for slim. Use it with `-e slim` when generating.
411 |
412 |
413 | ## 3.5.5
414 |
415 | h3. Bugfixes
416 | * The generator now places views of namespaced cells into the correct directory. E.g. `rails g Blog::Post display` puts views to `app/cells/blog/post/display.html.erb`.
417 |
418 | h3. Changes
419 | * Gem dependencies changed, we now require @actionpack@ and @railties@ >= 3.0.0 instead of @rails@.
420 |
421 |
422 | ## 3.5.4
423 |
424 | h3. Bugfixes
425 | * state-args work even if your state method receives optional arguments or default values, like @def show(user, age=18)@.
426 |
427 | h3. Changes
428 |
429 | * Cell::Base.view_paths is now setup in an initializer. If you do scary stuff with view_paths this might lead to scary problems.
430 | * Cells::DEFAULT_VIEW_PATHS is now Cell::Base::DEFAULT_VIEW_PATHS. Note that Cells will set its view_paths to DEFAULT_VIEW_PATHS at initialization time. If you want to alter the view_paths, use Base.append_view_path and friends in a separate initializer.
431 |
432 |
433 | ## 3.5.2
434 |
435 | h3. Bugfixes
436 | * Controller#render_cell now accepts multiple args as options.
437 |
438 | h3. Changes
439 | * Caching versioners now can accept state-args or options from the #render_cell call. This way, you don't have to access #options at all anymore.
440 |
441 |
442 | ## 3.5.1
443 |
444 | * No longer pass an explicit Proc but a versioner block to @Cell.Base.cache@. Example: @cache :show do "v1" end@
445 | * Caching.cache_key_for now uses @ActiveSupport::Cache.expand_cache_key@. Consequently, a key which used to be like @"cells/director/count/a=1/b=2"@ now is @cells/director/count/a=1&b=2@ and so on. Be warned that this might break your home-made cache expiry.
446 | * Controller#expire_cell_state now expects the cell class as first arg. Example: @expire_cell_state(DirectorCell, :count)@
447 |
448 | h3. Bugfixes
449 | * Passing options to @render :state@ in views finally works: @render({:state => :list_item}, item, i)@
450 |
451 |
452 | ## 3.5.0
453 |
454 | h3. Changes
455 | * Deprecated @opts, use #options now.
456 | * Added state-args. State methods can now receive the options as method arguments. This should be the prefered way of parameter exchange with the outer world.
457 | * #params, #request, and #config is now delegated to @parent_controller.
458 | * The generator now is invoked as @rails g cell ...@
459 | * The `--haml` option is no longer available.
460 | * The `-t` option now is compatible with the rest of rails generators, now it is used as alias for `--test-framework`. Use the `-e` option as an alias of `--template-engine`
461 | Thanks to Jorge Calás Lozano for patching this in the most reasonable manner i could imagine.
462 | * Privatized @#find_family_view_for_state@, @#render_view_for@, and all *ize methods in Cell::Rails.
463 | * New signature: @#render_view_for(state, *args)@
464 |
465 | ## 3.4.4
466 |
467 | h3. Changes
468 | * Cells.setup now yields Cell::Base, so you can really call append_view_path and friends here.
469 | * added Cell::Base.build for streamlining the process of deciders around #render_cell, "see here":http://nicksda.apotomo.de/2010/12/pragmatic-rails-thoughts-on-views-inheritance-view-inheritance-and-rails-304
470 | * added TestCase#in_view to test helpers in a real cell view.
471 |
472 |
473 | ## 3.4.3
474 |
475 | h3. Changes
476 | * #render_cell now accepts a block which yields the cell instance before rendering.
477 |
478 | h3. Bugfixes
479 | * We no longer use TestTaskWithoutDescription in our rake tasks.
480 |
--------------------------------------------------------------------------------