Add has_and_belongs_to_many via has_many :through
This code implements has_and_belongs_to_many in Hyperloop by using the existing has_many :through functionality and dynamically generating the join table.
if RUBY_ENGINE == "opal"
module ActiveRecord
module ClassMethods
def has_and_belongs_to_many(assoc, scope=nil, **options)
other = assoc.singularize
name = self.name.downcase
rassoc = name.pluralize
habtm_name = [assoc, rassoc].sort.join("_") # alphabetical order
habtm_class = habtm_name.singularize.camelize
has_many habtm_name # regular has_many makes the through work
has_many assoc, **options.merge(through: habtm_name)
return if ::Object.const_defined?(habtm_class) # prevent duplication
# double colon before Object and Class are needed - Opal bug?
::Object.const_set(
habtm_class, ::Class.new(::ActiveRecord::Base) do
belongs_to other, foreign_key: "#{other}_id", inverse_of: assoc
belongs_to name, foreign_key: "#{name}_id", inverse_of: rassoc
end
)
end
end
end
end
Note that this patch alone only enables read only access. This is because Hyperloop doesn't support writing to has_many :through e.g. with the << method (but Rails does). You can't work around it by trying to create the join table directly because it doesn't exist on the server side so execute_remote fails.
The inverse_of isn't necessary but can't hurt I think. I hoped it would make the association writeable but it doesn't.
Needs tests adding to test_app.
Tested in lap17 and Opal 0.10
Well I came to the conclusion that it's not easy to get saving of HABTM working. Because it translates to has_many :through which Hyperloop doesn't support saving to. But even if it did, there is no server-side join model, so Rails complains.
For now at least, I've decided the best thing to do is leave my HABTM patch for read-only associations, but for associations that need writing, convert them to has_many through on the server side. It's not a difficult conversion, and allows you to add validations which might otherwise have been on the parent but work better with hyperloop on the join. So after manual conversion, this is how it looks:
What you start with, HABTM
class Post < ApplicationRecord
has_and_belongs_to_many :tags
end
class Tag < ApplicationRecord
has_and_belongs_to_many :posts
end
# what works with my patch in a component
render(DIV) do
Post.all.first.tags.each do |tag|
SPAN { tag.name } # yay
end
end
# but saving doesn't work
render(DIV) do
# no execute_remote, not saved
Post.all.first.tags << Tag.find_by(name: "Hyperloop")
end
How you refactor to has_many through
class Post < ApplicationRecord
has_many :posts_tags
has_many :tags, through: :posts_tags
end
# * the order of the class names must be alphabetical.
# * yes the first is plural and second is not.
class PostsTag < ApplicationRecord
belongs_to :post
belongs_to :tag
# optional validation example, bonus of manual conversion
validate :maximum_comments
private
def maximum_comments
return unless post.comments.count > 10
errors.add(:comments, "maximum 10 comments, stop spammin!")
end
end
class Tag < ApplicationRecord
has_many :posts_tags
has_many :posts, through: :posts_tags
end
# This listing example above still works
# This is what you want to do but still doesn't work
Post.all.first.tags << Tag.find_by(name: "Hyperloop")
# This also doesn't work but feels like it should
Post.all.first.posts_tags.create tag: Tag.find_by(name: "Hyperloop")
# This does work but feels gross. Permission works nicely at least
PostsTag.create(post: Post.all.first, tag: Tag.find_by(name: "Hyperloop"))