From 82d13ebbbee025893e782c2fd76182bca3070fe5 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Wed, 31 Jan 2024 22:50:13 +0000 Subject: [PATCH 1/4] ControllerVariables > ControllerAttributes --- README.md | 26 +++-- fixtures/dummy/app/views/articles/show.rb | 7 ++ lib/phlexible/rails.rb | 2 +- .../action_controller/implicit_render.rb | 16 +-- lib/phlexible/rails/controller_attributes.rb | 85 -------------- lib/phlexible/rails/controller_variables.rb | 107 ++++++++++++++++++ test/phlexible/rails/controller_variables.rb | 107 ++++++++++++++++++ 7 files changed, 242 insertions(+), 108 deletions(-) create mode 100644 fixtures/dummy/app/views/articles/show.rb delete mode 100644 lib/phlexible/rails/controller_attributes.rb create mode 100644 lib/phlexible/rails/controller_variables.rb create mode 100644 test/phlexible/rails/controller_variables.rb diff --git a/README.md b/README.md index 0f6d952..6a884db 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,11 +54,21 @@ 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. ```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 ``` #### `Responder` @@ -90,7 +100,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` 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..fcb2382 --- /dev/null +++ b/lib/phlexible/rails/controller_variables.rb @@ -0,0 +1,107 @@ +# 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 # rubocop:disable Metrics/AbcSize + 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]) unless instance_variable_defined?(:"@#{v}") + 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 or a hash of ' \ + 'variable names and options.' + end + + allow_undefined = kwargs.delete(:allow_undefined) + + if names.empty? + 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] # rubocop:disable Metrics/BlockNesting + 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 + + if kwargs.key?(:as) + raise ArgumentError, 'You cannot provide the `as:` option when passing multiple ' \ + 'variable names.' + end + else + if names.count > 1 && kwargs.key?(:as) + raise ArgumentError, 'You cannot provide the `as:` option when passing multiple ' \ + 'variable names.' + end + + names.each do |name| + as = kwargs[:as] || name + name = "#{name}!" unless allow_undefined + + self.__controller_variables__ += { name.to_s => as.to_s } + end + 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..764af94 --- /dev/null +++ b/test/phlexible/rails/controller_variables.rb @@ -0,0 +1,107 @@ +# 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 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 From e3d4461b90ebad52e53629f62ebd4b4306c360ee Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 1 Feb 2024 10:08:45 +0000 Subject: [PATCH 2/4] fix: Allow redefining controller variables --- README.md | 8 +++++++- lib/phlexible/rails/controller_variables.rb | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a884db..c11b9ad 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,11 @@ end `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. +- `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 class Views::Users::Index < Views::Base @@ -71,6 +75,8 @@ class Views::Users::Index < Views::Base 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. diff --git a/lib/phlexible/rails/controller_variables.rb b/lib/phlexible/rails/controller_variables.rb index fcb2382..8be8417 100644 --- a/lib/phlexible/rails/controller_variables.rb +++ b/lib/phlexible/rails/controller_variables.rb @@ -36,7 +36,7 @@ def initialize(name) end end - def before_template # rubocop:disable Metrics/AbcSize + def before_template if respond_to?(:__controller_variables__) view_assigns = helpers.controller.view_assigns @@ -49,7 +49,7 @@ def before_template # rubocop:disable Metrics/AbcSize raise ControllerVariables::UndefinedVariable, k if !allow_undefined && !view_assigns.key?(k) - instance_variable_set(:"@#{v}", view_assigns[k]) unless instance_variable_defined?(:"@#{v}") + instance_variable_set(:"@#{v}", view_assigns[k]) end end From 7ed2eaec17a8e3564d54e0b2fae09e3f16c0005b Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 1 Feb 2024 11:28:31 +0000 Subject: [PATCH 3/4] fix(ControllerVariables): accept mix of names and hash --- lib/phlexible/rails/controller_variables.rb | 56 +++++++++----------- test/phlexible/rails/controller_variables.rb | 23 ++++++++ 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/lib/phlexible/rails/controller_variables.rb b/lib/phlexible/rails/controller_variables.rb index 8be8417..8fd8095 100644 --- a/lib/phlexible/rails/controller_variables.rb +++ b/lib/phlexible/rails/controller_variables.rb @@ -57,48 +57,42 @@ def before_template end module ClassMethods - def controller_variable(*names, **kwargs) # rubocop:disable Metrics + def controller_variable(*names, **kwargs) # rubocop:disable Metrics/* if names.empty? && kwargs.empty? - raise ArgumentError, 'You must provide at least one variable name or a hash of ' \ + 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.empty? - kwargs.each do |k, v| - if v.is_a?(Hash) - name = v.key?(:as) ? v[:as].to_s : k.to_s + if names.count > 1 && as + raise ArgumentError, 'You cannot provide the `as:` option when passing multiple ' \ + 'variable names.' + end - if v.key?(:allow_undefined) - k = "#{k}!" unless v[:allow_undefined] # rubocop:disable Metrics/BlockNesting - elsif !allow_undefined - k = "#{k}!" - end - else - name = v.to_s - k = "#{k}!" unless allow_undefined - end + names.each do |name| + name_as = as || name + name = "#{name}!" unless allow_undefined - self.__controller_variables__ += { k.to_s => name } - end - - if kwargs.key?(:as) - raise ArgumentError, 'You cannot provide the `as:` option when passing multiple ' \ - 'variable names.' - end - else - if names.count > 1 && kwargs.key?(:as) - raise ArgumentError, 'You cannot provide the `as:` option when passing multiple ' \ - 'variable names.' - end + self.__controller_variables__ += { name.to_s => name_as.to_s } + end - names.each do |name| - as = kwargs[:as] || name - name = "#{name}!" unless allow_undefined + kwargs.each do |k, v| + if v.is_a?(Hash) + name = v.key?(:as) ? v[:as].to_s : k.to_s - self.__controller_variables__ += { name.to_s => as.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 diff --git a/test/phlexible/rails/controller_variables.rb b/test/phlexible/rails/controller_variables.rb index 764af94..3e63089 100644 --- a/test/phlexible/rails/controller_variables.rb +++ b/test/phlexible/rails/controller_variables.rb @@ -39,6 +39,29 @@ def before 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 }) From 1d530fcec8b7133c6d5c46369a9311f9c2b5d343 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Fri, 2 Feb 2024 13:20:11 +0000 Subject: [PATCH 4/4] chore: MetaTags docs --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c11b9ad..182fc48 100644 --- a/README.md +++ b/README.md @@ -154,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`