├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── Appraisals ├── CHANGES.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── cells-rails.gemspec ├── gemfiles ├── 5.2.gemfile ├── 6.0.gemfile ├── 6.1.gemfile ├── 7.1.gemfile ├── 7.2.gemfile └── 8.0.gemfile ├── lib ├── cell │ ├── caching │ │ └── notification.rb │ ├── helper │ │ └── asset_helper.rb │ ├── rails.rb │ ├── rails │ │ ├── collection.rb │ │ ├── constant_for.rb │ │ └── testing.rb │ ├── railtie.rb │ ├── test_case.rb │ └── translation.rb ├── cells-rails.rb ├── cells │ ├── rails.rb │ └── rails │ │ └── version.rb └── rails │ └── generators │ ├── cell │ ├── cell_generator.rb │ └── templates │ │ ├── cell.rb.erb │ │ ├── view.erb │ │ ├── view.haml │ │ └── view.slim │ ├── concept │ ├── concept_generator.rb │ └── templates │ │ ├── concept.rb.erb │ │ ├── view.erb │ │ ├── view.haml │ │ └── view.slim │ └── test_unit │ ├── cell │ ├── cell_generator.rb │ └── templates │ │ └── unit_test.rb.erb │ └── concept │ ├── concept_generator.rb │ └── templates │ └── unit_test.rb.erb └── test ├── dummy ├── Gemfile ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ ├── .keep │ │ │ └── logo.jpeg │ │ └── stylesheets │ │ │ └── application.css.erb │ ├── cells │ │ ├── form_for │ │ │ └── show.erb │ │ ├── form_for_cell.rb │ │ ├── form_tag │ │ │ └── show.erb │ │ ├── form_tag_cell.rb │ │ ├── formtastic │ │ │ └── show.erb │ │ ├── formtastic_cell.rb │ │ ├── simple_form │ │ │ └── show.erb │ │ ├── simple_form_cell.rb │ │ ├── song │ │ │ ├── song.css │ │ │ └── with_escaped.erb │ │ └── song_cell.rb │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── index_controller.rb │ │ ├── musician_controller.rb │ │ └── songs_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── artist.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── song.rb │ └── views │ │ ├── index │ │ └── index.html.erb │ │ ├── layouts │ │ └── application.html.erb │ │ └── songs │ │ ├── show.html.erb │ │ └── with_escaped.html.erb ├── config │ ├── application.rb │ ├── boot.rb │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── formtastic.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── engines │ └── my_engine │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── README.rdoc │ │ ├── Rakefile │ │ ├── app │ │ ├── assets │ │ │ ├── images │ │ │ │ └── my_engine │ │ │ │ │ └── .keep │ │ │ └── stylesheets │ │ │ │ └── my_engine │ │ │ │ └── application.css │ │ ├── concepts │ │ │ └── user │ │ │ │ ├── cell.rb │ │ │ │ └── views │ │ │ │ ├── show.erb │ │ │ │ └── user.scss │ │ ├── controllers │ │ │ └── my_engine │ │ │ │ ├── application_controller.rb │ │ │ │ └── user_controller.rb │ │ ├── models │ │ │ └── my_engine │ │ │ │ └── user.rb │ │ └── views │ │ │ ├── layouts │ │ │ └── my_engine │ │ │ │ └── application.html.erb │ │ │ └── my_engine │ │ │ └── user │ │ │ └── show.html.erb │ │ ├── bin │ │ └── rails │ │ ├── config │ │ └── routes.rb │ │ ├── db │ │ └── migrate │ │ │ └── 20150530135920_create_my_engine_users.rb │ │ ├── lib │ │ ├── my_engine.rb │ │ ├── my_engine │ │ │ ├── engine.rb │ │ │ └── version.rb │ │ └── tasks │ │ │ └── my_engine_tasks.rake │ │ ├── my_engine.gemspec │ │ └── test │ │ └── fixtures │ │ └── my_engine │ │ └── users.yml └── test │ ├── caching_test.rb │ ├── cell_generator_test.rb │ ├── concept_generator_test.rb │ ├── context_test.rb │ ├── integration │ ├── asset_helper_test.rb │ ├── asset_pipeline_test.rb │ ├── controller_test.rb │ ├── form_for_test.rb │ ├── form_tag_test.rb │ ├── formtastic_test.rb │ ├── simple_form_test.rb │ └── url_helper_test.rb │ ├── public_test.rb │ ├── rails_extensions_test.rb │ ├── test_helper.rb │ └── translation_test.rb └── test_helper.rb /.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, 3.2, 3.3, head] 10 | gemfile: 11 | - gemfiles/8.0.gemfile 12 | - gemfiles/7.2.gemfile 13 | - gemfiles/7.1.gemfile 14 | - gemfiles/6.0.gemfile 15 | - gemfiles/5.2.gemfile 16 | exclude: 17 | - ruby: 2.5 18 | gemfile: gemfiles/7.2.gemfile 19 | - ruby: 2.5 20 | gemfile: gemfiles/7.1.gemfile 21 | - ruby: 2.5 22 | gemfile: gemfiles/8.0.gemfile 23 | - ruby: 2.6 24 | gemfile: gemfiles/7.2.gemfile 25 | - ruby: 2.6 26 | gemfile: gemfiles/7.1.gemfile 27 | - ruby: 2.6 28 | gemfile: gemfiles/8.0.gemfile 29 | - ruby: 2.7 30 | gemfile: gemfiles/7.2.gemfile 31 | - ruby: 2.7 32 | gemfile: gemfiles/7.1.gemfile 33 | - ruby: 2.7 34 | gemfile: gemfiles/8.0.gemfile 35 | - ruby: 3.0 36 | gemfile: gemfiles/5.2.gemfile 37 | - ruby: 3.0 38 | gemfile: gemfiles/8.0.gemfile 39 | - ruby: 3.0 40 | gemfile: gemfiles/7.2.gemfile 41 | - ruby: 3.1 42 | gemfile: gemfiles/5.2.gemfile 43 | - ruby: 3.1 44 | gemfile: gemfiles/6.0.gemfile 45 | - ruby: 3.1 46 | gemfile: gemfiles/8.0.gemfile 47 | - ruby: 3.2 48 | gemfile: gemfiles/5.2.gemfile 49 | - ruby: 3.2 50 | gemfile: gemfiles/6.0.gemfile 51 | - ruby: 3.3 52 | gemfile: gemfiles/5.2.gemfile 53 | - ruby: 3.3 54 | gemfile: gemfiles/6.0.gemfile 55 | - ruby: head 56 | gemfile: gemfiles/5.2.gemfile 57 | - ruby: head 58 | gemfile: gemfiles/6.0.gemfile 59 | 60 | runs-on: ubuntu-latest 61 | env: 62 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: ruby/setup-ruby@v1 67 | with: 68 | ruby-version: ${{ matrix.ruby }} 69 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 70 | - run: bundle exec rake 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /log 10 | tmp 11 | .DS_Store 12 | log/ 13 | *.iml 14 | .idea 15 | gemfiles/*.lock 16 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 2.7.5 2 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "8.0" do 2 | gem "railties", "~> 8.0.0" 3 | end 4 | 5 | appraise "7.2" do 6 | gem "railties", "~> 7.2.0" 7 | end 8 | 9 | appraise "7.1" do 10 | gem "railties", "~> 7.1.0" 11 | end 12 | 13 | appraise "6.1" do 14 | gem "railties", "~> 6.1.0" 15 | end 16 | 17 | appraise "6.0" do 18 | gem "railties", "~> 6.0" 19 | end 20 | 21 | appraise "5.2" do 22 | gem "railties", "~> 5.2.0" 23 | end 24 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.1.6 2 | 3 | * Rails 8 is supported. 4 | * Ruby 3.4.0 is supported. 5 | 6 | ## 0.1.5 7 | 8 | * Support for Rails 7. 9 | * Migrate CI to Github Actions 10 | 11 | ## 0.1.4 12 | 13 | * Support Ruby 3.0. 14 | 15 | ## 0.1.3 16 | 17 | * Fix previous fixes. 18 | 19 | ## 0.1.2 20 | 21 | * Fix `LocalJumpError` from our Railtie. 22 | 23 | ## 0.1.1 24 | 25 | * The `Cell.view_paths` are now set automatically for `Trailblazer::Cell`s, too, if the gem is present. This allows starting Rails in daemon mode `rails s -d` and cells finding its views. 26 | 27 | ## 0.1.0 28 | 29 | * Support for Rails 6.0 Thank to @Szeliga 30 | 31 | ## 0.0.9 32 | 33 | * Prevent Forwardable from printing warnings in Ruby 2.4+ 34 | 35 | ## 0.0.8 36 | 37 | * Fix testing for `rspec-cells` in combination with Rails 5.1. Thanks @tiagoamaro. 38 | 39 | ## 0.0.7 40 | 41 | * Add `config.cells.include_template_engine = false` to turn off automatic 42 | inclusion of, e.g., `Cell::Slim`. 43 | * Require Cells >= 4.1.6. 44 | 45 | ## 0.0.6 46 | 47 | * `Testing` fixed with Rails 5. 48 | 49 | ## 0.0.5 50 | 51 | * Fix an annoying bundler loading bug. Thanks to @RKushnir. 52 | 53 | ## 0.0.4 54 | 55 | * Fix `Cell::Translation`. Thanks to @zavan! 56 | 57 | ## 0.0.3 58 | 59 | * `Cell::Helper::AssetHelper` is now always included and fixes https://github.com/apotonick/cells/issues/214 (hopefully). 60 | 61 | ## 0.0.2 62 | 63 | * Moved all Rails files from `cells` to `cells-rails`. 64 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in cells-rails.gemspec 4 | gemspec 5 | 6 | gem "my_engine", path: "test/dummy/engines/my_engine" 7 | 8 | group :development, :test do 9 | gem "minitest-spec-rails" 10 | gem "capybara" 11 | end 12 | 13 | gem "simple_form" 14 | gem "formtastic" 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Nick Sutterer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cells::Rails 2 |  3 | 4 | [Cells](https://github.com/apotonick/cells) is a generic view model implementation for Ruby. Cells-rails brings Rails-specific bindings. 5 | 6 | ## Rails Features 7 | 8 | * All asset-related helpers are now simply delegated to the global asset helper instance. This happens by automatically including `Cell::Helper::AssetHelper` into `ViewModel`. 9 | * The global controller is passed to all cells via the context object. It's available via `ViewModel#controller`. 10 | * `ViewModel#call` and `Collection#call` are automatically `html_safe`ed. 11 | 12 | 13 | ## Installation 14 | 15 | Note that `cells-rails` is designed to work with Cells >= 4.1. 16 | 17 | Add this line to your application's Gemfile and keep it real: 18 | 19 | ```ruby 20 | gem 'cells-rails' 21 | ``` 22 | 23 | ## License 24 | 25 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/dummy/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /cells-rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'cells/rails/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'cells-rails' 9 | spec.version = Cells::Rails::VERSION 10 | spec.authors = ['Nick Sutterer'] 11 | spec.email = ['apotonick@gmail.com'] 12 | spec.license = 'MIT' 13 | 14 | spec.summary = 'Convenient Rails support for Cells.' 15 | spec.homepage = 'https://trailblazer.to' 16 | 17 | spec.metadata['homepage_uri'] = spec.homepage 18 | spec.metadata['source_code_uri'] = 'https://github.com/trailblazer/cells-rails' 19 | spec.metadata['bug_tracker_uri'] = "#{spec.metadata['source_code_uri']}/issues" 20 | spec.metadata['changelog_uri'] = "#{spec.metadata['source_code_uri']}/blob/HEAD/CHANGES.md" 21 | spec.metadata['documentation_uri'] = 'https://trailblazer.to/2.1/docs/cells#cells-rails' 22 | 23 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|\.github)/}) } 24 | spec.require_paths = ['lib'] 25 | 26 | spec.add_dependency 'actionpack', '>= 5.0' 27 | spec.add_dependency 'cells', '>= 4.1.6', '< 5.0.0' 28 | 29 | spec.add_development_dependency 'appraisal' 30 | spec.add_development_dependency 'bundler' 31 | spec.add_development_dependency 'cells-erb' 32 | spec.add_development_dependency 'minitest' 33 | spec.add_development_dependency 'rails' 34 | spec.add_development_dependency 'rake' 35 | spec.add_development_dependency 'sass-rails' 36 | spec.add_development_dependency 'debug' 37 | end 38 | -------------------------------------------------------------------------------- /gemfiles/5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "my_engine", path: "../test/dummy/engines/my_engine" 6 | gem "simple_form" 7 | gem "formtastic" 8 | gem "railties", "~> 5.2.0" 9 | 10 | group :development, :test do 11 | gem "minitest-spec-rails" 12 | gem "capybara" 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /gemfiles/6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "my_engine", path: "../test/dummy/engines/my_engine" 6 | gem "simple_form" 7 | gem "formtastic" 8 | gem "railties", "~> 6.0" 9 | 10 | group :development, :test do 11 | gem "minitest-spec-rails" 12 | gem "capybara" 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /gemfiles/6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "my_engine", path: "../test/dummy/engines/my_engine" 6 | gem "simple_form" 7 | gem "formtastic" 8 | gem "railties", "~> 6.1.0" 9 | 10 | group :development, :test do 11 | gem "minitest-spec-rails" 12 | gem "capybara" 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /gemfiles/7.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "my_engine", path: "../test/dummy/engines/my_engine" 6 | gem "simple_form" 7 | gem "formtastic" 8 | gem "railties", "~> 7.1.0" 9 | 10 | group :development, :test do 11 | gem "minitest-spec-rails" 12 | gem "capybara" 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /gemfiles/7.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "my_engine", path: "../test/dummy/engines/my_engine" 6 | gem "simple_form" 7 | gem "formtastic" 8 | gem "railties", "~> 7.2.0" 9 | 10 | group :development, :test do 11 | gem "minitest-spec-rails" 12 | gem "capybara" 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /gemfiles/8.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "my_engine", path: "../test/dummy/engines/my_engine" 6 | gem "simple_form" 7 | gem "formtastic" 8 | gem "railties", "~> 8.0.0" 9 | 10 | group :development, :test do 11 | gem "minitest-spec-rails" 12 | gem "capybara" 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /lib/cell/caching/notification.rb: -------------------------------------------------------------------------------- 1 | module Cell 2 | module Caching 3 | module Notifications 4 | def fetch_from_cache_for(key, options) 5 | ActiveSupport::Notifications.instrument('read_fragment.cells', key: key) do 6 | cache_store.fetch(key, options) do 7 | ActiveSupport::Notifications.instrument('write_fragment.cells', key: key) do 8 | yield 9 | end 10 | end 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/cell/helper/asset_helper.rb: -------------------------------------------------------------------------------- 1 | module Cell 2 | module Helper 3 | # Delegate all asset-related helpers to the global helpers instance. 4 | # This is the cleanest solution to leverage Rails' asset management and 5 | # doesn't pollute your cell with weird asset modules from Rails. 6 | module AssetHelper 7 | # Extend if we forgot anything. 8 | # This delegates asset helpers to the global Rails helper instance. 9 | 10 | # http://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html 11 | %w{ 12 | javascript_include_tag 13 | stylesheet_link_tag 14 | 15 | asset_path 16 | asset_url 17 | image_tag 18 | video_tag 19 | audio_path 20 | audio_url 21 | compute_asset_extname 22 | compute_asset_host 23 | compute_asset_path 24 | favicon_link_tag 25 | font_path 26 | font_url 27 | image_path 28 | image_url 29 | javascript_path 30 | javascript_url 31 | path_to_asset 32 | path_to_audio 33 | path_to_font 34 | path_to_image 35 | path_to_javascript 36 | path_to_stylesheet 37 | path_to_video 38 | stylesheet_path 39 | stylesheet_url 40 | url_to_asset 41 | url_to_audio 42 | url_to_font 43 | url_to_image 44 | url_to_javascript 45 | url_to_stylesheet 46 | url_to_video 47 | video_path 48 | video_url 49 | }.each do |method| 50 | define_method(method) do |*args| 51 | ::ActionController::Base.helpers.send(method, *args) 52 | end 53 | end 54 | end # AssetHelper 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/cell/rails.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_support/concern' 3 | module Cell 4 | def self.rails_version 5 | Gem::Version.new(ActionPack::VERSION::STRING) 6 | end 7 | 8 | # These methods are automatically added to all controllers and views. 9 | module RailsExtensions 10 | module ActionController 11 | def cell(name, model=nil, options={}, constant=::Cell::ViewModel, &block) 12 | options[:context] ||= {} 13 | options[:context][:controller] = self 14 | 15 | constant.cell(name, model, options, &block) 16 | end 17 | 18 | def concept(name, model=nil, options={}, &block) 19 | cell(name, model, options, ::Cell::Concept, &block) 20 | end 21 | end 22 | 23 | module ActionView 24 | # Returns the cell instance for +name+. You may pass arbitrary options to your 25 | # cell. 26 | # 27 | # = cell(:song, title: "Creeping Out Sara").(:show) 28 | def cell(name, *args, &block) 29 | controller.cell(name, *args, &block) 30 | end 31 | 32 | # # See Cells::Rails::ActionController#render_cell. 33 | # def render_cell(name, state, *args, &block) 34 | # ::Cell::Rails.render_cell(name, state, controller, *args, &block) 35 | # end 36 | 37 | def concept(name, *args, &block) 38 | controller.concept(name, *args, &block) 39 | end 40 | end 41 | 42 | # Gets included into Cell::ViewModel in a Rails environment. 43 | module ViewModel 44 | extend ActiveSupport::Concern 45 | 46 | # DISCUSS: who actually uses forgery protection with cells? it is not working since 4, anyway? 47 | # include ActionController::RequestForgeryProtection 48 | included do 49 | extend Uber::Delegates 50 | delegates :parent_controller, :session, :params, :request, :config, :env, :url_options, :default_url_options 51 | # forgery protection. 52 | delegates :parent_controller, :request_forgery_protection_token 53 | end 54 | 55 | def call(*) 56 | super.html_safe 57 | end 58 | 59 | def parent_controller 60 | context[:controller] 61 | end 62 | alias_method :controller, :parent_controller 63 | 64 | def perform_caching? 65 | ::ActionController::Base.perform_caching 66 | end 67 | 68 | def cache_store # we want to use DI to set a cache store in cell/rails. 69 | ::ActionController::Base.cache_store 70 | end 71 | 72 | # In Ruby 2.4.0+, Forwardable prints a warning when you delegate 73 | # to a private or protected method - so `delegates :protect_against_forgery?` 74 | # or `delegates :form_authenticity_token` will print warnings all 75 | # over the place 76 | # 77 | # This workaround prevents warnings being printed 78 | def protect_against_forgery? 79 | controller.send(:protect_against_forgery?) 80 | end 81 | 82 | def form_authenticity_token(**options) 83 | controller.send(:form_authenticity_token, **options) 84 | end 85 | 86 | module ClassMethods 87 | def expand_cache_key(key) 88 | ::ActiveSupport::Cache.expand_cache_key(key, :cells) 89 | end 90 | end 91 | end 92 | 93 | # In Rails, there are about 10 different implementations of #url_for. Rails doesn't like the idea of objects, so they 94 | # have helpers in modules. Those module are now included sequentially into other modules and/or classes. While they 95 | # get included, they might or might not include methods, depending on the including module/class 96 | # (example here: https://github.com/rails/rails/blob/cad20f720c4c6e04584253cd0a23f22b3d43ab0f/actionpack/lib/action_dispatch/routing/url_for.rb#L87). 97 | # 98 | # The outcome is that several module override #url_for, and if you're lucky, this works. If you're not, then #url_for breaks 99 | # due to a raise in one of its basic implementations, introduced in 3.x, fixed in 4.0 and then re-introduced in 4.2 100 | # 101 | # This is extremely frustrating as no one in Rails core seems to tackle this problem and introduces a url object instead 102 | # of this module madness. I have to constantly test and fix it in Cells. With the module below, I'll stop doing this. 103 | # 104 | # Either Rails works with Cells and we fix this in form of a URL object that gets passed into the cell (I'm happy with 105 | # a global object here, too! Wow!) or URL helpers will stop working in Cells and a lot of people will be unhappy. 106 | # 107 | # Anyway, this is the reason we need this patch module. If you have trouble with URLs in Cells, then please ask Rails to 108 | # fix their implementation. Thank you. 109 | module HelpersAreShit 110 | def url_for(options = nil) # from ActionDispatch:R:UrlFor. 111 | case options 112 | when nil 113 | _routes.url_for(url_options.symbolize_keys) 114 | when Hash 115 | _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) 116 | when String 117 | options 118 | when Array 119 | polymorphic_url(options, options.extract_options!) 120 | else 121 | polymorphic_url(options) 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/cell/rails/collection.rb: -------------------------------------------------------------------------------- 1 | module Cell 2 | module RailsExtension 3 | module Collection 4 | def call(*) 5 | super.html_safe 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/cell/rails/constant_for.rb: -------------------------------------------------------------------------------- 1 | module Cell 2 | module RailsExtension 3 | module ConstantFor 4 | def constant_for(name) 5 | name.camelize.constantize 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/cell/rails/testing.rb: -------------------------------------------------------------------------------- 1 | module Cell 2 | module RailsExtensions 3 | # This modules overrides Cell::Testing#controller_for and provides Rails-specific logic. 4 | module Testing 5 | RAILS_9_0 = Gem::Version.new("9.0.0") 6 | RAILS_5_1 = Gem::Version.new("5.1.0") 7 | RAILS_5_0 = Gem::Version.new("5.0.0") 8 | 9 | def controller_for(controller_class) 10 | return unless controller_class 11 | 12 | controller_class.new.tap do |ctl| 13 | ctl.request = action_controller_test_request(controller_class) 14 | ctl.instance_variable_set :@routes, ::Rails.application.routes.url_helpers 15 | end 16 | end 17 | 18 | def action_controller_test_request(controller_class) 19 | version = ::Rails.gem_version 20 | 21 | if version >= RAILS_5_1 && version < RAILS_9_0 22 | ::ActionController::TestRequest.create(controller_class) 23 | elsif version >= RAILS_5_0 && version < RAILS_5_1 24 | ::ActionController::TestRequest.create 25 | else 26 | ::ActionController::TestRequest.new 27 | end 28 | end 29 | end # Testing 30 | end 31 | end 32 | 33 | Cell::Testing.send(:include, Cell::RailsExtensions::Testing) 34 | -------------------------------------------------------------------------------- /lib/cell/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails/railtie" 2 | require "cell/rails" 3 | 4 | module Cell 5 | class Railtie < Rails::Railtie 6 | 7 | config.cells = ActiveSupport::OrderedOptions.new 8 | 9 | initializer("cells.attach_router") do |app| 10 | ViewModel.class_eval do 11 | include app.routes.url_helpers # TODO: i hate this, make it better in Rails. 12 | end 13 | end 14 | 15 | initializer "cells.update_view_paths" do |app| 16 | # Add Rails.root to view_paths 17 | Cell::ViewModel.view_paths = [Rails.root.join("app", "cells")] 18 | end 19 | 20 | config.after_initialize do |app| 21 | Array(app.config.cells.with_assets).each do |cell_class| 22 | app.config.assets.paths += cell_class.camelize.constantize.prefixes # Song::Cell.prefixes 23 | end 24 | end 25 | 26 | initializer "cells.rails_extensions" do |app| 27 | ActiveSupport.on_load(:action_controller) do 28 | self.class_eval do 29 | include ::Cell::RailsExtensions::ActionController 30 | end 31 | end 32 | 33 | ActiveSupport.on_load(:action_view) do 34 | self.class_eval do 35 | include ::Cell::RailsExtensions::ActionView 36 | end 37 | end 38 | 39 | require "cell/rails/collection" 40 | require "cell/rails/constant_for" 41 | 42 | Cell::Collection.send :include, Cell::RailsExtension::Collection 43 | Cell::ViewModel.send :include, Cell::RailsExtension::ConstantFor 44 | end 45 | 46 | initializer "cells.include_default_helpers" do 47 | # include asset helpers (image_path, font_path, ect) 48 | ViewModel.class_eval do 49 | include ActionView::Helpers::FormHelper # includes ActionView::Helpers::UrlHelper, ActionView::Helpers::FormTagHelper 50 | include ::Cell::RailsExtensions::HelpersAreShit 51 | 52 | require "cell/helper/asset_helper" 53 | include Cell::Helper::AssetHelper 54 | 55 | # set VM#cache_store, etc. 56 | include RailsExtensions::ViewModel 57 | end 58 | end 59 | 60 | IncludeTemplateModules = ->(app) do 61 | return if app.config.cells.include_template_engine == false 62 | 63 | # yepp, this is happening. saves me a lot of coding in each extension. 64 | ViewModel.send(:include, Cell::Erb) if Cell.const_defined?(:Erb, false) 65 | ViewModel.send(:include, Cell::Haml) if Cell.const_defined?(:Haml, false) 66 | ViewModel.send(:include, Cell::Hamlit) if Cell.const_defined?(:Hamlit, false) 67 | ViewModel.send(:include, Cell::Slim) if Cell.const_defined?(:Slim, false) 68 | end 69 | 70 | initializer( "cells.include_template_module", after: "cells.include_default_helpers", &IncludeTemplateModules) 71 | 72 | initializer("cells.development") do |app| 73 | if Rails.env == "development" 74 | require "cell/development" 75 | ViewModel.send(:include, Development) 76 | end 77 | end 78 | 79 | initializer "trailblazer.cells.update_view_paths" do |app| 80 | if Object.const_defined?(:Trailblazer) && Trailblazer.const_defined?(:Cell, false) 81 | # Add Rails.root to view_paths 82 | Trailblazer::Cell.view_paths = [Rails.root.join("app", "concepts")] 83 | end 84 | end 85 | 86 | rake_tasks do 87 | load "tasks/cells.rake" 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/cell/test_case.rb: -------------------------------------------------------------------------------- 1 | require "cell/testing" 2 | 3 | module Cell 4 | # Test your cells. 5 | # 6 | # TODO: document me, Capybara, etc. 7 | class TestCase < ActiveSupport::TestCase 8 | include Testing 9 | end 10 | end 11 | 12 | Cell::Testing.capybara = true if Object.const_defined?(:"Capybara") 13 | -------------------------------------------------------------------------------- /lib/cell/translation.rb: -------------------------------------------------------------------------------- 1 | module Cell::Translation 2 | def self.included(includer) 3 | super 4 | includer.inheritable_attr :translation_path 5 | end 6 | 7 | def initialize(*) 8 | super 9 | @virtual_path = translation_path 10 | end 11 | 12 | private 13 | # If you override this to change this path, please report it on the trailblazer/chat gitter channel, 14 | # so we can find out best practices. 15 | def translation_path 16 | self.class.translation_path or self.class.controller_path.gsub("/", ".") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cells-rails.rb: -------------------------------------------------------------------------------- 1 | require "cells/rails/version" 2 | require "cells/rails" 3 | -------------------------------------------------------------------------------- /lib/cells/rails.rb: -------------------------------------------------------------------------------- 1 | require "cells" 2 | require "cell/railtie" 3 | require "cell/caching/notification" 4 | require "cell/rails/testing" 5 | 6 | module Cell 7 | autoload :Translation, "cell/translation" 8 | autoload :TestCase, "cell/test_case" 9 | end 10 | -------------------------------------------------------------------------------- /lib/cells/rails/version.rb: -------------------------------------------------------------------------------- 1 | module Cells 2 | module Rails 3 | VERSION = "0.1.6" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/rails/generators/cell/cell_generator.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | module Generators 3 | class CellGenerator < NamedBase 4 | source_root File.expand_path('../templates', __FILE__) 5 | 6 | class_option :parent, type: :string, desc: 'The parent class for the generated cell' 7 | class_option :e, type: :string, desc: 'The template engine' 8 | 9 | check_class_collision suffix: 'Cell' 10 | 11 | argument :actions, type: :array, default: [], banner: 'action action2' 12 | 13 | def create_cell_file 14 | template 'cell.rb.erb', File.join('app/cells', class_path, "#{file_name}_cell.rb") 15 | end 16 | 17 | def create_view_files 18 | states.each do |state| 19 | @state = state 20 | @path = File.join('app/cells', class_path, file_name, "#{state}.#{template_engine}") 21 | template "view.#{template_engine}", @path 22 | end 23 | end 24 | 25 | hook_for :test_framework 26 | 27 | private 28 | 29 | def parent_class_name 30 | options[:parent] || 'Cell::ViewModel' 31 | end 32 | 33 | # The show state is included by default 34 | def states 35 | (['show'] + actions).uniq 36 | end 37 | 38 | def template_engine 39 | (options[:e] || Rails.application.config.app_generators.rails[:template_engine] || 'erb').to_s 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/rails/generators/cell/templates/cell.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Cell < <%= parent_class_name %> 2 | <% states.each do |state| -%> 3 | def <%= state %> 4 | render 5 | end 6 | 7 | <% end -%> 8 | end 9 | -------------------------------------------------------------------------------- /lib/rails/generators/cell/templates/view.erb: -------------------------------------------------------------------------------- 1 |
6 | Find me in <%= @path %> 7 |
8 | -------------------------------------------------------------------------------- /lib/rails/generators/cell/templates/view.haml: -------------------------------------------------------------------------------- 1 | %h1 2 | <%= class_name %>#<%= @state %> 3 | %p 4 | Find me in <%= @path %> -------------------------------------------------------------------------------- /lib/rails/generators/cell/templates/view.slim: -------------------------------------------------------------------------------- 1 | h1 <%= class_name %>#<%= @state %> 2 | p Find me in <%= @path %> 3 | -------------------------------------------------------------------------------- /lib/rails/generators/concept/concept_generator.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | module Generators 3 | class ConceptGenerator < NamedBase 4 | source_root File.expand_path('../templates', __FILE__) 5 | 6 | class_option :e, type: :string, desc: 'The template engine' 7 | argument :actions, type: :array, default: [], banner: 'action action2' 8 | 9 | check_class_collision suffix: 'Concept' 10 | 11 | 12 | def create_concept 13 | template 'concept.rb.erb', File.join('app/concepts', class_path, file_name, 'cell.rb') 14 | end 15 | 16 | def create_views 17 | states.each do |state| 18 | @state = state 19 | @path = File.join('app/concepts', class_path, file_name, 'views', "#{state}.#{template_engine}") 20 | template "view.#{template_engine}", @path 21 | end 22 | end 23 | 24 | hook_for :test_framework 25 | 26 | private 27 | 28 | def template_engine 29 | (options[:e] || Rails.application.config.app_generators.rails[:template_engine] || 'erb').to_s 30 | end 31 | 32 | # The show state is included by default 33 | def states 34 | (['show'] + actions).uniq 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/rails/generators/concept/templates/concept.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>::Cell < Cell::Concept 2 | <% states.each do |state| -%> 3 | def <%= state %> 4 | render 5 | end 6 | <% end -%> 7 | end 8 | -------------------------------------------------------------------------------- /lib/rails/generators/concept/templates/view.erb: -------------------------------------------------------------------------------- 1 |6 | Find me in <%= @path %> 7 |
8 | -------------------------------------------------------------------------------- /lib/rails/generators/concept/templates/view.haml: -------------------------------------------------------------------------------- 1 | %h1 2 | <%= class_name %>#<%= @state %> 3 | %p 4 | Find me in <%= @path %> -------------------------------------------------------------------------------- /lib/rails/generators/concept/templates/view.slim: -------------------------------------------------------------------------------- 1 | h1 <%= class_name %>#<%= @state %> 2 | p Find me in <%= @path %> 3 | -------------------------------------------------------------------------------- /lib/rails/generators/test_unit/cell/cell_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/test_unit' 2 | 3 | module TestUnit # :nodoc: 4 | module Generators # :nodoc: 5 | class CellGenerator < Base # :nodoc: 6 | source_root File.expand_path('../templates', __FILE__) 7 | argument :actions, type: :array, default: [] 8 | check_class_collision suffix: 'CellTest' 9 | 10 | def create_test_file 11 | template 'unit_test.rb.erb', File.join('test/cells', class_path, "#{file_name}_cell_test.rb") 12 | end 13 | 14 | private 15 | 16 | def states 17 | (['show'] + actions).uniq 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/rails/generators/test_unit/cell/templates/unit_test.rb.erb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class <%= class_name %>CellTest < Cell::TestCase 4 | <% states.each do |state| -%> 5 | test "<%= state %>" do 6 | html = cell("<%= file_name %>").(:<%= state %>) 7 | assert html.match // 8 | end 9 | 10 | <% end %> 11 | end 12 | -------------------------------------------------------------------------------- /lib/rails/generators/test_unit/concept/concept_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/test_unit' 2 | 3 | module TestUnit # :nodoc: 4 | module Generators # :nodoc: 5 | class ConceptGenerator < Base # :nodoc: 6 | source_root File.expand_path('../templates', __FILE__) 7 | argument :actions, type: :array, default: [] 8 | check_class_collision suffix: 'ConceptTest' 9 | 10 | def create_test_file 11 | template 'unit_test.rb.erb', File.join('test/concepts', class_path, file_name, 'cell_test.rb') 12 | end 13 | 14 | private 15 | 16 | def states 17 | (['show'] + actions).uniq 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/rails/generators/test_unit/concept/templates/unit_test.rb.erb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class <%= class_name %>ConceptTest < Cell::TestCase 4 | <% states.each do |state| -%> 5 | test "<%= state %>" do 6 | html = concept("<%= file_name %>/cell").(:<%= state %>) 7 | assert html.match /
/ 8 | end 9 | 10 | <% end %> 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 8.0.0' 4 | 5 | gem "my_engine", path: "engines/my_engine" 6 | 7 | gem 'sass-rails' 8 | gem "sprockets" 9 | 10 | group :development, :test do 11 | gem "minitest-spec-rails" 12 | gem "capybara" 13 | end 14 | 15 | gem "cells", ">= 4.1.6" 16 | # gem "cells", path: "../../../cells"# ">= 4.1.0" 17 | gem "cells-rails", path: "../../../cells-rails"# ">= 4.1.0" 18 | gem "cells-erb" 19 | gem "simple_form" 20 | gem "formtastic" 21 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link logo.jpeg 2 | //= link application.css 3 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailblazer/cells-rails/77ff22ae8f94472cf477672ca2f0c86757d627d8/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/images/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trailblazer/cells-rails/77ff22ae8f94472cf477672ca2f0c86757d627d8/test/dummy/app/assets/images/logo.jpeg -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css.erb: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | *= require user 16 | *= require song 17 | */ 18 | <%# capture do %> 19 | <%# end %> -------------------------------------------------------------------------------- /test/dummy/app/cells/form_for/show.erb: -------------------------------------------------------------------------------- 1 | <%= form_for Song.new do |f| %> 2 | First 3 | <%= f.text_field :id %> 4 | <%= f.fields_for :artist do |a| %> 5 | <%= a.text_field :id %> 6 | <% end %> 7 | <% end %> -------------------------------------------------------------------------------- /test/dummy/app/cells/form_for_cell.rb: -------------------------------------------------------------------------------- 1 | class FormForCell < Cell::ViewModel 2 | include ActionView::RecordIdentifier 3 | 4 | def show 5 | render 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/cells/form_tag/show.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag songs_path do %> 2 | Second 3 | <%= text_field_tag :id %> 4 | <%= submit_tag "Save" %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /test/dummy/app/cells/form_tag_cell.rb: -------------------------------------------------------------------------------- 1 | class FormTagCell < Cell::ViewModel 2 | def show 3 | render 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/cells/formtastic/show.erb: -------------------------------------------------------------------------------- 1 | <%= semantic_form_for Song.new do |f| %> 2 | First 3 | <%= f.input :id %> 4 | <%= f.semantic_fields_for :artist do |a| %> 5 | <%= a.input :id %> 6 | <% end %> 7 | 8 | <%= f.actions do %> 9 | <%= f.submit 'Go!', as: :button, class: 'btn btn-primary' %> 10 | <% end %> 11 | 12 | <% end %> -------------------------------------------------------------------------------- /test/dummy/app/cells/formtastic_cell.rb: -------------------------------------------------------------------------------- 1 | class FormtasticCell < Cell::ViewModel 2 | include ActionView::RecordIdentifier 3 | include Formtastic::Helpers::FormHelper 4 | include ActionView::Helpers::FormOptionsHelper 5 | 6 | def show 7 | render 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/app/cells/simple_form/show.erb: -------------------------------------------------------------------------------- 1 | <%= simple_form_for Song.new do |f| %> 2 | First 3 | <%= f.input :id %> 4 | <%= f.fields_for :artist do |a| %> 5 | <%= a.input :id %> 6 | <% end %> 7 | <% end %> -------------------------------------------------------------------------------- /test/dummy/app/cells/simple_form_cell.rb: -------------------------------------------------------------------------------- 1 | class SimpleFormCell < Cell::ViewModel 2 | include ActionView::RecordIdentifier 3 | include SimpleForm::ActionViewExtensions::FormHelper 4 | 5 | # include ActiveSupport::Configurable 6 | # include ActionController::RequestForgeryProtection # FIXME: this does NOT activate any protection. 7 | 8 | def show 9 | render 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/app/cells/song/song.css: -------------------------------------------------------------------------------- 1 | .song { background: red; } -------------------------------------------------------------------------------- /test/dummy/app/cells/song/with_escaped.erb: -------------------------------------------------------------------------------- 1 | <%= title %> -------------------------------------------------------------------------------- /test/dummy/app/cells/song_cell.rb: -------------------------------------------------------------------------------- 1 | class SongCell < Cell::ViewModel 2 | include Escaped 3 | property :title 4 | 5 | def show 6 | "happy" 7 | end 8 | 9 | def with_escaped 10 | render 11 | end 12 | 13 | def with_title(options) 14 | "