Defaultblog

New Modules, Classes, Variables from Gem not loading in my Rails Application

ruby on rails ruby gems software development

So I just spent about 3 to 4 hours trying to debug this problem. Here's the setup

  • We have a Rails application that uses a Gem that we wrote
  • The Gem is pretty simple - a collection of classes that the Rails application can use (https://github.com/bitesite/spire-ruby)
  • Our top level gem file autoloads in files and some are within modules/namespaces

So my main task was adding a new class, within a module and no matter what I did, I would constantly get a 'constant not found' errors or something similar. I would restart my rails console, re-bundle - nothing would work.

At one point, if I manually called 'load' it would work, but not upon the initial load. I found out later that 'spring' was the issue. I'm not sure the specifics, but spring must cache the classes, modules, etc. it uses from gems.

So what's the fix? When adding new classes, modules, and requiring them in your top level my-gem.rb file, be sure to stop spring using

spring stop

so that when you restart your Rails server or console, it properly loads them. This is a super good article to learn the internals of how all this works: https://medium.com/@connorstack/understanding-ruby-load-require-gems-bundler-and-rails-autoloading-from-the-bottom-up-3b422902ca0

Caseyli
Casey Li
CEO & Founder, BiteSite
Defaultblog

Ruby on Rails 6 with JQuery UJS

ajax ruby on rails software development

Alright, like a lot of you, I'm loving that in Rails 6, we're moving all of our Javascript management to Webpacker by default. However, within this transition, we're losing a lot of the automatic functionality that we're used to getting with the asset pipeline. One of the biggest things that doesn't seem to be quite ironed out yet is UJS and specifically with AJAX calls.

In the past, we had both Rails UJS and JQuery UJS to help us automatically append a CSRF token to our non-get AJAX requests to seamlessly use Ajax calls in our Rails applications. However, now with Rails 6, it doesn't seem so easy, nor is it very clear what the correct way to do it is.

I've read a lot of articles suggesting that when going to Rails 6 with Webpacker, to replace calls like $.ajax with Rails.ajax, but I've also read contradictory articles saying that the Rails javascript library is meant as an internal library.

With that in mind, I turned my attention to JQuery UJS and it seems that even including it via Webpack is not super easy in terms of getting it working.

So as it stands now, I don't really see a clear answer on how to properly make AJAX calls in Rails 6.

I did find one article Working with Javascript in Rails, which I definitely trust, but there is one thing they mention at the very end: "When using another library to make Ajax calls, it is necessary to add the security token as a default header for Ajax calls in your library."

So that had me thinking - ok, the proper way to do it is to append the CSRF token to everyone of your AJAX calls in the header if using something like JQuery to make your Ajax calls.

While that may be correct, it seemed tedious, not to mention what UJS seemed to be doing.

So in the end, I decided for now, for Rails 6.0 apps, to just go back to the old way of doing things - let the asset pipeline manage JQuery and JQuery UJS and just let webpacker manage all the other javascript. Hopefully there will be clearer answers in the near future as to how to handle a simple ajax call via Fetch or JQuery that doesn't involve appending something to your header every time.

Alright, so how to restore the asset pipeline way of doing things...

  1. Add the jquery-rails gem to your Gemfile and bundle
  2. Edit your app/assets/config/manifest.js file to include a new javascripts folder

    //= link_tree ../images
    //= link_directory ../stylesheets .css
    //= link_directory ../javascripts .js
    
  3. Create the app/assets/javascripts folder and add a application.js file (just like old times)

  4. Add the following to the newly created application.js file (just like old times)

    //= require jquery
    //= require jquery_ujs
    
  5. As to not conflict with Rails UJS, remove the require("@rails/ujs").start() from the app/javascript/packs/application.js file

  6. Add the old include tag to layout application.html.erb ABOVE any webpack import

    <%= javascript_include_tag 'application' %>
    
  7. Code the rest of your Javascript in Webpacker

Basically what we're doing is we're using Sprockets/Asset Pipeline to set up one global javascript function: JQuery + JQuery UJS. Everything else though, we run through Webpacker.

Essentially - we're going back to Rails 5.1 ways of doing things and the catch was step 2 (restoring the Javascripts folder in the asset pipeline).

All this being said - I would love to hear what other Rails 6+ developers are doing. Are you using fetch? Are you using $.ajax? Are you using Rails.ajax? If so, are you manually appending the CSRF token? As far as I can tell, there's no real accepted way of doing this.

Let me know. Thanks for reading.

Caseyli
Casey Li
CEO & Founder, BiteSite
Defaultblog

Ruby on Rails QuickTip: Wrapping simple chunks of code in a method to give it meaning

ruby ruby on rails software

Let's say you have a model of a User. Let's say that if the user's birth year is 1990 or before, then you can't edit that user's first name. So you might have something like this:

# User Class
class User < ApplicationRecord
end

and you might have something like this

# User controller
class UsersController < ApplicationController
  def edit
    if user.birth_year <= 1990
       # don't allow editing of first name
    end
end

However, I would argue, that even though it's a really simply check, that you should put this in its own method:

# User Class
class User < ApplicationRecord
  def first_name_locked?
    birth_year <= 1990
  end
end
# User controller
class UsersController < ApplicationController
  def edit
    if user.first_name_locked?
       # don't allow editing of first name
    end
end

Very simple implementation and achieves the same result, but here are some advantages

  • The condition is now given meaning so that developers who are using it can understand its intent
  • Any developer reading any code that uses that method will understand more easily what's happening
  • If the business logic changes for locking the first name, you'll have to edit only one piece of code

So even the simplest pieces of code can benefit from wrapping them in a method.

Caseyli
Casey Li
CEO & Founder, BiteSite
Defaultblog

Ruby on Rails QuickTip: Adding Parameters to your Methods Safely

ruby ruby on rails software

This is going to be a quick post and applies to a lot of different languages, but it's something we've been doing in our Rails projects a decent amount.

If you're ever working on a larger codebase and you decide you want to add a parameter to a method, but are afraid to do so because it might break code elsewhere, consider simply adding a default value.

Take the following setup for example

class MyModel < ApplicationRecord
  def my_method(user_id)
    user = User.find user_id
    user
  end
end

Now, let's say you want to modify this method so you can add another parameter. Here's a safe way you can do it:

class MyModel < ApplicationRecord
  def my_method(user_id, new_parameter=nil)
    user = User.find user_id
    if new_parameter
      do something
    end
    user
  end
end

Now that new parameter's default value could have been anything, but the key here:

  1. Have a default value so that any code calling this method with the original parameters will still work
  2. When the new parameter is not passed in, have the method behave the exact same way it did before

With these two principles you'll be able to extend your code without breaking any existing code.

Caseyli
Casey Li
CEO & Founder, BiteSite
Ruby on rails accepts nested attributes for deleting records

Ruby on Rails accepts_nested_attributes_for is deleting my associated record!

ruby on rails software

So I've been programming Ruby on Rails for about 9 years now and I'm still learning new things everyday. This is one that definitely caused us some issues.

Setup

So here's the setup. We have a User model

class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  accepts_nested_attributes_for :profile
end

And we have an associated Profile model

class Profile < ApplicationRecord
  belongs_to :user
end

Notice that on the User model, we have

accepts_nested_attributes_for :profile

If you don't know what that does, it basically allows you to run creates and updates on the User model and Profile model in one single call. So for example, you can do this:

user = User.first
user.update({ email: 'test@example.com', profile_attributes: { first_name: 'Casey', last_name: 'Li' })

If you set up your form correctly and with proper strong params, you can put the User and Profile all in one single form for the user making it easy to update both models at the same time. BUT BE VERY CAREFUL!

Problem

If you have a very similar setup to what we had, basically a one-to-one relationship, then you have to be very careful of your update calls. If you call update on the parent record (in our case the User record), and pass in child attributes, if you DON'T pass in the ID, it will actually delete your existing child record and create a new one!. Yah. I didn't know that either. Try it out, and look at your Rails logs, you'll see a SQL delete statement followed by an insert.

Again, this is for one-to-one relationships only while doing accepts_nested_attributes_for updates.

Solution

If this is the behaviour you want, obviously you're ok. But for us, we wanted to update the existing child record rather than destroying what already existed.

If you want to update the existing record, there are two things you can do.

Solution 1: Pass in the ID of the child record

While I haven't tried this myself, if you pass in the child record's ID, it should perform an update rather than a delete/insert.

user = User.first
profile = user.profile
user.update({ email: 'test@example.com', profile_attributes: { id: profile.id, first_name: 'Casey', last_name: 'Li' })

Solution 2: Use the 'update_only' option

This is the solution we went with. When declaring your accepts_nested_attributes_for, you can pass in the update_only option:

class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  accepts_nested_attributes_for :profile, update_only: true
end

Learn something everyday. Hope this helps out some peeps. Thanks for reading!

Caseyli
Casey Li
CEO & Founder, BiteSite