Tagged: rails6 Toggle Comment Threads | Keyboard Shortcuts

  • kmitov 11:45 am on April 2, 2021 Permalink |
    Tags: , rails6   

    The magic of class methods for a Ruby on Rails controller and their configurations 

    Have you used gems that provide class methods for Ruby on Rails controllers that extend these controllers?

    Like the ones below:

    class TutorialsController < ApplicationController
      
      # This is from cancancan
      # It allows to loading and authorizing
      load_and_authorize_resource
      ...
    
      # This is from 'has_scope'. 
      has_scope :pref_order, only: :index, default: "suggested"
      
    end
    

    Today’s article is about how to implement such a method for our own concern.

    My specific case is duplication. I would like to develop a DuplicateResourceConcern that extends the controller and allows users to duplicate a record when they create a new record. This is something we’ve been using at FLLCasts and BuildIn3D for many years, but today I revised the implementation, improved it and decided to share the knowledge in a short article.

    The goal is to be able to do:

    class TutorialsController < ApplicationController
      
      include DuplicateResourceConcern
      enable_duplicate
      ...  
    end

    With this logic when the user:

    1. visits /tutorials/new?resource_to_dup=123
    2. The title field in the form for new tutorial is pre-filled the title of Tutorial 123.

    Declare DuplicateResourceConcern and enable_duplicate

    First we declare DuplicateResourceConcern

    module DuplicateResourceConcern
      extend ActiveSupport::Concern
    
      included do
        before_action :dup, only: [:new]
      end
    
      private
      def dup
        # call the duplication
      end
    
    end

    It is a concern with one method that is ‘dup’

    We use the concern in the TutorialsController

    class TutorialsController < ApplicationController
      
      include DuplicateResourceConcern
      
      def new
      end  
    end

    The method ‘dup’ is a private method of the controller. It is called before the :new action. If a param called ‘resource_to_dup=X’ is passed we would like to duplicate the title of Tutorial X.

    Simple Tutorial duplication

    module DuplicateResourceConcern
      extend ActiveSupport::Concern
    
      included do
        before_action :dup, only: [:new]
      end
    
      private
      def dup
        original_tutorial = Tutorial.find_by_id(params.permit(:resource_to_dup)[:resource_to_dup])
        @tutorial = Tutorial.new(title: original_tutorial.title)
      end
    
    end

    This duplication will work for the Tutorials controller. But it will not work for ArticlesController, because we are doing “Tutorial.find” and we are assigning a @tutorial variable.

    Reusing the DuplicateResourceConcern in a different controller

    To reuse the DuplicateResoureConcern we must call Article.find_by_id when we are in an ArticlesController and Tutorial.find_by_id when we are in a TutorialsController.

    Here is how we could do this using ‘controller_name’, ‘.classify’ and ‘.constantize’. We can get the name of the records from the controller name.

    module DuplicateResourceConcern
      extend ActiveSupport::Concern
    
      included do
        before_action :dup, only: [:new]
      end
    
      private
      def dup
        # This will return Tutorial for TutorialsController
        # and will return Article for ArticlesController
        clazz = controller_name.classify.constantize
    
        # If the controller is called 'tutorials' the instance name will be 'tutorial'
        instance_name = controller_name.singularize
    
        original_instance = clazz.find_by_id(params.permit(:resource_to_dup)[:resource_to_dup])
        duplicated_instance = clazz.new(title: original_instance.title)
    
        # After this call we will be able to access @tutorial in the TutorialsController and @article in the ArticlesController
        instance_variable_set("@#{instance_name}", duplicated_instance)
      end
    
    end

    In this way the DuplicateResourceConcern is not dependent on the type of the object. It could work for Articles and for Tutorials. It is still dependent on the property ‘title’, but I will leave this for now.

    How to configure the duplication

    The name of the parameter in the url should be ‘resource_to_dup’. What if we want to modify this name on the controller level. For example to have a parameter
    resource_to_dup‘ for TutorialsController and
    resource_to_copy‘ for ArticlesController.

    What we want to do is pass an argument with the name of the parameter, but we would have to do a little more work.

    class TutorialsController < ApplicationController
      
      include DuplicateResourceConcern
      enable_duplicate param_name: "my_new_param_name"
      
      def new
      end  
    end

    We would use the class_method for the concerns.

    module DuplicateResourceConcern
      extend ActiveSupport::Concern
    
      included do
        before_action :dup, only: [:new]
      end
    
      def self.included(base)
        base.class_eval do
          extend ClassMethods
          # Declare a class-level attribute whose value is inheritable by subclasses. Subclasses can change their own value and it will not impact parent class.
          # In this way we can extend the class and still have separate configurations
          class_attribute :duplicate_resource_configuration, instance_writer: false
      
          self.duplicate_resource_configuration = {
            options: {
              param_name: "resource_to_dup",
            }
          }
        end
      end
    
      class_methods do 
        # The enable_duplicate method will be called on a class level.
        def enable_duplicate(*args) do
          options = args.extract_options!
          options.symbolize_keys!
    
          # We check that there are only parameters with this names.
          options.assert_valid_keys(:param_name)
    
          # Merge the default options with the options passed in the controller
          self.duplicate_resource_configuration[:options].merge!(options)
    
          # We mark that we've enabled the duplication
          self.duplicate_resource_configuration[:enabled] = true
        end
    
      end
    
    
      private
      def dup
        # This will return Tutorial for TutorialsController
        # and will return Article for ArticlesController
        clazz = controller_name.classify.constantize
    
        # If the controller is called 'tutorials' the instance name will be 'tutorial'
        instance_name = controller_name.singularize
    
        # Param name. We are no longer dependent on 
        # the param name being resource_to_dup. It could 
        # be a different name.
        the_param_name = self.duplicate_resource_configuration[:options][:param_name]
    
        original_instance = clazz.find_by_id(params.permit(the_param_name)[the_param_name])
        duplicated_instance = clazz.new(title: original_instance.title)
    
        # After this call we will be able to access @tutorial in the TutorialsController and @article in the ArticlesController
        instance_variable_set("@#{instance_name}", duplicated_instance)
      end
    
    end

    Extensibility

    This is the basic structure. We can now add more options if we want to and more parameters to the configurations.

    What is the system RSpec for duplication.

    It is simple.

    scenario "/tutorials/new?resource_to_dup can duplicate a resource" do
        tutorial.update(title: SecureRandom.hex(10))
    
        visit "/tutorials/new?resource_to_dup=#{tutorial.id}"
        expect(page).to have_text "Duplicate from #{tutorial.title}"
    
        click_on "Create Tutorial"
        expect(page).to have_text "Tutorial was successfully created."
    
        tutorials = Tutorial.where(title: tutorial.title)
        expect(tutorials.count).to eq 2
      end

     
  • kmitov 1:34 pm on March 25, 2021 Permalink |
    Tags: , , rails6,   

    Stimulus 1.1.1 to Stimulus 2.0.0 – practical cost 

    We just migrated Stimulus 1.1.1. to Stimulus 2.0.0 so I decided to share this with our whole team, but I thought this could be useful for the whole community.

    The practical cost is that this migration could be done in a few minutes per Stimulus controller. 10-20 controllers – you should be done in less than a day.

    Overview of the changes

    Here are a few examples of the changes that were committed for a single _form.html.erb

    html.erb

    # app/views/public_users_searches/_form.html.erb
    -            target: "public-users-searches.term",
    +            public_users_searches_target: "term",
    ...
    -    <%= f.submit t('public_users_searches.form.search'), data: {target: "public-users-searches.commit", value: t('public_users_searches.form.search')} do %>
    +    <%= f.submit t('public_users_searches.form.search'), data: {public_users_searches_target: "commit", value: t('public_users_searches.form.search')} do %>
    

    Conclusion

    If you are wondering whether you should migrate or not and how much it will cost probably you should migrate. It is not that expensive.

     
c
compose new post
j
next post/next comment
k
previous post/previous comment
r
reply
e
edit
o
show/hide comments
t
go to top
l
go to login
h
show/hide help
shift + esc
cancel