diff --git a/README.md b/README.md index 0f6d952..182fc48 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ class UsersController end ``` -#### `ControllerAttributes` +#### `ControllerVariables` -Include this module in your Phlex views to get access to the controller's instance variables. It provides an explicit interface for accessing controller instance variables from the view. +Include this module in your Phlex views to get access to the controller's instance variables. It provides an explicit interface for accessing controller instance variables from within the view. ```ruby class Views::Users::Index < Views::Base - include Phlexible::Rails::ControllerAttributes + include Phlexible::Rails::ControllerVariables - controller_attribute :first_name, :last_name + controller_variable :first_name, :last_name def template h1 { "#{@first_name} #{@last_name}" } @@ -54,13 +54,29 @@ end ##### Options -- `attr_reader:` - If set to `true`, an `attr_reader` will be defined for the given attributes. -- `alias:` - If set, the given attribute will be aliased to the given alias value. +`controller_variable` accepts one or many symbols, or a hash of symbols to options. + +- `as:` - If set, the given attribute will be renamed to the given value. Helpful to avoid naming conflicts. +- `allow_undefined:` - By default, if the instance variable is not defined in the controller, an + exception will be raised. If this option is to `true`, an error will not be raised. + +You can also pass a hash of attributes to `controller_variable`, where the key is the controller +attribute, and the value is the renamed value, or options hash. ```ruby -controller_attribute :users, attr_reader: true, alias: :my_users +class Views::Users::Index < Views::Base + include Phlexible::Rails::ControllerVariables + + controller_variable last_name: :surname, first_name: { as: :given_name, allow_undefined: true } + + def template + h1 { "#{@given_name} #{@surname}" } + end +end ``` +Please note that defining a variable with the same name as an existing variable in the view will be overwritten. + #### `Responder` If you use [Responders](https://github.com/heartcombo/responders), Phlexible provides a responder to support implicit rendering similar to `ActionController::ImplicitRender` above. It will render the Phlex view using `respond_with` if one exists, and fall back to default rendering. @@ -90,7 +106,7 @@ end This responder requires the use of `ActionController::ImplicitRender`, so don't forget to include that in your `ApplicationController`. -If you use `ControllerAttributes` in your view, and define a `resource` attribute, the responder will pass that to your view. +If you use `ControllerVariables` in your view, and define a `resource` attribute, the responder will pass that to your view. #### `AElement` @@ -138,7 +154,36 @@ Phlexible::Rails::ButtonTo.new(:root, method: :patch) { 'My Button' } - `:form_attributes` - Hash of HTML attributes for the form tag. - `:data` - This option can be used to add custom data attributes. - `:params` - Hash of parameters to be rendered as hidden fields within the form. -- `:method` - Symbol of the HTTP verb. Supported verbs are :post (default), :get, :delete, :patch, and :put. +- `:method` - Symbol of the HTTP verb. Supported verbs are :post (default), :get, :delete, :patch, + and :put. + +#### `MetaTags` + +A super simple way to define and render meta tags in your Phlex views. Just render the +`Phlexible::Rails::MetaTagsComponent` component in the head element of your page, and define the +meta tags using the `meta_tag` method in your controllers. + +```ruby +class MyController < ApplicationController + meta_tag :description, 'My description' + meta_tag :keywords, 'My keywords' +end +``` + +```ruby +class MyView < Phlex::HTML + def template + html do + head do + render Phlexible::Rails::MetaTagsComponent + end + body do + # ... + end + end + end +end +``` ### `AliasView` diff --git a/fixtures/dummy/app/views/articles/show.rb b/fixtures/dummy/app/views/articles/show.rb new file mode 100644 index 0000000..f358563 --- /dev/null +++ b/fixtures/dummy/app/views/articles/show.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Views::Articles::Show < Phlex::HTML + include Phlexible::Rails::ControllerVariables + + def template; end +end diff --git a/lib/phlexible/rails.rb b/lib/phlexible/rails.rb index 3e5296a..973cc50 100644 --- a/lib/phlexible/rails.rb +++ b/lib/phlexible/rails.rb @@ -4,7 +4,7 @@ module Phlexible module Rails - autoload :ControllerAttributes, 'phlexible/rails/controller_attributes' + autoload :ControllerVariables, 'phlexible/rails/controller_variables' autoload :Responder, 'phlexible/rails/responder' autoload :AElement, 'phlexible/rails/a_element' diff --git a/lib/phlexible/rails/action_controller/implicit_render.rb b/lib/phlexible/rails/action_controller/implicit_render.rb index 57c127d..cbe4474 100644 --- a/lib/phlexible/rails/action_controller/implicit_render.rb +++ b/lib/phlexible/rails/action_controller/implicit_render.rb @@ -26,24 +26,12 @@ def default_render render_plex_view({ action: action_name }) || super end - def assign_phlex_accessors(pview) - pview.tap do |view| - if view.respond_to?(:__controller_attributes__) - view.__controller_attributes__.each do |attr| - raise ControllerAttributes::UndefinedVariable, attr unless view_assigns.key?(attr.to_s) - - view.instance_variable_set :"@#{attr}", view_assigns[attr.to_s] - end - end - end - end - def method_for_action(action_name) super || ('default_phlex_render' if phlex_view(action_name)) end def default_phlex_render - render assign_phlex_accessors(phlex_view(action_name).new) + render phlex_view(action_name).new end # @param options [Hash] At a minimum this may contain an `:action` key, which will be used @@ -54,7 +42,7 @@ def render_plex_view(options) return unless (view = phlex_view(options[:action])) - render assign_phlex_accessors(view.new), options + render view.new, options end private diff --git a/lib/phlexible/rails/controller_attributes.rb b/lib/phlexible/rails/controller_attributes.rb deleted file mode 100644 index 68d2581..0000000 --- a/lib/phlexible/rails/controller_attributes.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -# Include this module in your Phlex views to get access to the controller's instance variables. It -# provides an explicit interface for accessing controller instance variables from the view. Simply -# call `controller_attribute` with the name of any controller instance variable you want to access -# in your view. -# -# @example -# class Views::Users::Index < Views::Base -# controller_attribute :user_name -# -# def template -# h1 { @user_name } -# end -# end -# -# Options -# - `attr_reader:` - If set to `true`, an `attr_reader` will be defined for the given attributes. -# - `alias:` - If set, the given attribute will be aliased to the given alias value. -# -# NOTE: Phlexible::Rails::ActionController::ImplicitRender is required for this to work. -# -module Phlexible - module Rails - module ControllerAttributes - module Layout - def self.included(klass) - klass.extend ClassMethods - end - - module ClassMethods - def render(view, _locals) - component = new - - # Assign controller attributes to the layout. - view.controller.assign_phlex_accessors component if view.controller.respond_to? :assign_phlex_accessors - - component.call(view_context: view) do |yielded| - output = yielded.is_a?(Symbol) ? view.view_flow.get(yielded) : yield - - component.unsafe_raw(output) if output.is_a?(ActiveSupport::SafeBuffer) - - nil - end - end - end - end - - def self.included(klass) - klass.class_attribute :__controller_attributes__, instance_predicate: false, default: Set.new - klass.extend ClassMethods - end - - class UndefinedVariable < NameError - def initialize(name) - @variable_name = name - super "Attempted to expose controller attribute `#{@variable_name}`, but instance " \ - 'variable is not defined in the controller.' - end - end - - module ClassMethods - def controller_attribute(*names, **kwargs) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity - include Layout if include?(Phlex::Rails::Layout) && !include?(Layout) - - self.__controller_attributes__ += names - - return if kwargs.empty? - - names.each do |name| - attr_reader name if kwargs[:attr_reader] - - if kwargs[:alias] - if kwargs[:attr_reader] - alias_method kwargs[:alias], name - else - define_method(kwargs[:alias]) { instance_variable_get :"@#{name}" } - end - end - end - end - end - end - end -end diff --git a/lib/phlexible/rails/controller_variables.rb b/lib/phlexible/rails/controller_variables.rb new file mode 100644 index 0000000..8fd8095 --- /dev/null +++ b/lib/phlexible/rails/controller_variables.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +# Include this module in your Phlex views to get access to the controller's instance variables. It +# provides an explicit interface for accessing controller instance variables from the view. Simply +# call `controller_variable` with the name of any controller instance variable you want to access +# in your view. +# +# @example +# class Views::Users::Index < Views::Base +# controller_variable :user_name +# +# def template +# h1 { @user_name } +# end +# end +# +# Options +# - `as:` - If set, the given attribute will be renamed to the given value. Helpful to avoid +# naming conflicts. +# - `allow_undefined:` - If set to `true`, the view will not raise an error if the controller +# instance variable is not defined. +# +module Phlexible + module Rails + module ControllerVariables + def self.included(klass) + klass.class_attribute :__controller_variables__, instance_predicate: false, default: Set.new + klass.extend ClassMethods + end + + class UndefinedVariable < NameError + def initialize(name) + @variable_name = name + super "Attempted to expose controller variable `#{@variable_name}`, but instance " \ + 'variable is not defined in the controller.' + end + end + + def before_template + if respond_to?(:__controller_variables__) + view_assigns = helpers.controller.view_assigns + + __controller_variables__.each do |k, v| + allow_undefined = true + if k.ends_with?('!') + allow_undefined = false + k = k.chop + end + + raise ControllerVariables::UndefinedVariable, k if !allow_undefined && !view_assigns.key?(k) + + instance_variable_set(:"@#{v}", view_assigns[k]) + end + end + + super + end + + module ClassMethods + def controller_variable(*names, **kwargs) # rubocop:disable Metrics/* + if names.empty? && kwargs.empty? + raise ArgumentError, 'You must provide at least one variable name and/or a hash of ' \ + 'variable names and options.' + end + + allow_undefined = kwargs.delete(:allow_undefined) + as = kwargs.delete(:as) + + if names.count > 1 && as + raise ArgumentError, 'You cannot provide the `as:` option when passing multiple ' \ + 'variable names.' + end + + names.each do |name| + name_as = as || name + name = "#{name}!" unless allow_undefined + + self.__controller_variables__ += { name.to_s => name_as.to_s } + end + + kwargs.each do |k, v| + if v.is_a?(Hash) + name = v.key?(:as) ? v[:as].to_s : k.to_s + + if v.key?(:allow_undefined) + k = "#{k}!" unless v[:allow_undefined] + elsif !allow_undefined + k = "#{k}!" + end + else + name = v.to_s + k = "#{k}!" unless allow_undefined + end + + self.__controller_variables__ += { k.to_s => name } + end + end + end + end + end +end diff --git a/test/phlexible/rails/controller_variables.rb b/test/phlexible/rails/controller_variables.rb new file mode 100644 index 0000000..3e63089 --- /dev/null +++ b/test/phlexible/rails/controller_variables.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'phlex/testing/rails/view_helper' + +describe Phlexible::Rails::ControllerVariables do + include Phlex::Testing::Rails::ViewHelper + + def before + Views::Articles::Show.__controller_variables__ = Set.new + end + + it 'exposes controller variable' do + controller.instance_variable_set :@article, 'article1' + Views::Articles::Show.controller_variable :article + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@article)).to be == 'article1' + end + + it 'sets name with :as' do + controller.instance_variable_set :@article, 'article1' + Views::Articles::Show.controller_variable :article, as: :article_name + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@article)).to be_nil + expect(view.instance_variable_get(:@article_name)).to be == 'article1' + end + + it 'accepts hash' do + controller.instance_variable_set :@article, 'article1' + Views::Articles::Show.controller_variable(article: :article_name) + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@article)).to be_nil + expect(view.instance_variable_get(:@article_name)).to be == 'article1' + end + + it 'accepts multiple names' do + controller.instance_variable_set :@first_name, 'Joel' + controller.instance_variable_set :@last_name, 'Moss' + Views::Articles::Show.controller_variable(:first_name, :last_name) + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@first_name)).to be == 'Joel' + expect(view.instance_variable_get(:@last_name)).to be == 'Moss' + end + + it 'accepts names and hash' do + controller.instance_variable_set :@first_name, 'Joel' + controller.instance_variable_set :@last_name, 'Moss' + Views::Articles::Show.controller_variable(:first_name, last_name: :surname) + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@first_name)).to be == 'Joel' + expect(view.instance_variable_get(:@last_name)).to be_nil + expect(view.instance_variable_get(:@surname)).to be == 'Moss' + end + + it 'accepts hash with :as key' do + controller.instance_variable_set :@article, 'article1' + Views::Articles::Show.controller_variable(article: { as: :article_name }) + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@article)).to be_nil + expect(view.instance_variable_get(:@article_name)).to be == 'article1' + end + + it 'raises on undefined var' do + Views::Articles::Show.controller_variable :article + + expect do + render Views::Articles::Show.new + end.to raise_exception(Phlexible::Rails::ControllerVariables::UndefinedVariable) + end + + it 'allow_undefined: true' do + Views::Articles::Show.controller_variable :article, allow_undefined: true + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@article)).to be_nil + end + + it 'allow_undefined: false' do + Views::Articles::Show.controller_variable :article, allow_undefined: false + + expect do + render Views::Articles::Show.new + end.to raise_exception(Phlexible::Rails::ControllerVariables::UndefinedVariable) + end + + it 'with hash and allow_undefined: true' do + Views::Articles::Show.controller_variable article: { allow_undefined: true } + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@article)).to be_nil + end + + it 'with hash and allow_undefined: false' do + Views::Articles::Show.controller_variable article: { allow_undefined: false } + + expect do + render Views::Articles::Show.new + end.to raise_exception(Phlexible::Rails::ControllerVariables::UndefinedVariable) + end + + it 'with hash and allow_undefined in both args' do + Views::Articles::Show.controller_variable article: { allow_undefined: false }, allow_undefined: true + + expect do + render Views::Articles::Show.new + end.to raise_exception(Phlexible::Rails::ControllerVariables::UndefinedVariable) + end + + it 'with hash and allow_undefined in both args' do + Views::Articles::Show.controller_variable article: { allow_undefined: true }, allow_undefined: false + + render(view = Views::Articles::Show.new) + + expect(view.instance_variable_get(:@article)).to be_nil + end +end