Search code examples
ruby-on-railsmany-to-manybidirectional

Bidirectional many-to-many association with Rails : how to create from both ways?


I have a question about many-to-many associations in Ruby on Rails.

I have 3 models in my app : Topic, Meeting and Todo associated with a manu-to-many association.

class Todo < ApplicationRecord
  belongs_to :topic
  belongs_to :meeting
end

then

class Meeting < ApplicationRecord
  has_many :todos
end

and

class Topic < ApplicationRecord
  has_many :todos
end

I made my routes and controller to be able to create new todos via a meeting :

Rails.application.routes.draw do
  resources :meetings, only: [:index, :show, :new, :create, :edit, :update]  do
    resources :todos, only: [:index, :new, :create]
  end
  resources :todos, only: [:index, :show, :edit, :update, :destroy]
end

and

class TodosController < ApplicationController

  def new
    @topic = Topic.find(params[:topic_id])
    @todo = Todo.new
  end

  def create
    @todo = Todo.new(todo_params)
    @meeting = Meeting.find(params[:meeting_id])
    @todo.meeting = @meeting
    @todo.save
    redirect_to meeting_path(@meeting)
  end

  private
  def todo_params
    params.require(:todo).permit(:topic_id, :meeting_id, :note, :deadline, :title)
  end
end

and my view :

<h3><%= @meeting.date %></h3>
<%= simple_form_for [@meeting, @todo] do |f| %>
  <%= f.input :title %>
  <%= f.input :note %>
  <%= f.date_field :deadline %>
  <%= f.association :topic, label_method: :nom, value_method: :id %>
  <%= f.submit "Add a todo" %>
<% end %>

My problem is that I want to be able to create todo via topics aswell and when I add my routes :

  resources :topics, only: [:index, :show, :new, :create] do
    resources :todos, only: [:index, :new, :create]
  end

When I tried to complete my controller and test it, it seems to be tricky. If I add:

@topic = Topic.find(params[:topic_id])

Then it tells me that it needs a meeting...

Any idea ?


Solution

  • You can create the different routes with:

    resources :meetings do
      resources :todos, only: [:index, :new, :create]
    end
    
    resources :topics do
      resources :todos, only: [:index, :new, :create]
    end
    

    You can avoid duplication by using routing concerns:

    concerns :todoable do
      resources :todos, only: [:index, :new, :create]
    end
    
    resources :topics, concerns: :todoable
    resources :meetings, concerns: :todoable
    

    In your controller you can check for the presences of the meeting_id or topic_id parameters:

    class TodosController < ApplicationController
      before_action :set_parent
    
      def new
        @todo = Todo.new
      end
    
      def create
        @todo = @parent.todos.new(todo_params)
        if @todo.save
          redirect_to @parent
        else
          render :new
        end
      end
    
      private
    
      def parent_class
        @parent_class ||= if params[:meeting_id].present?
          Meeting
        else if params[:topic_id].present?
          Topic
        else
          # raise an error?
        end
      end
    
      def set_parent
        id = params["#{parent_class.model_name.param_key}_id"]
        @parent = parent_class.find(id)
      end
    
      def todo_params
        params.require(:todo)
              .permit(:topic_id, :meeting_id, :note, :deadline, :title)
      end
    end
    
    <%= simple_form_for [@parent, @todo] do |f| %>
      <%= f.input :title %>
      <%= f.input :note %>
      <%= f.date_field :deadline %>
      <%= f.association :topic, label_method: :nom, value_method: :id if @parent.is_a?(Meeting) %>
      <%= f.association :meeting, label_method: :nom, value_method: :id if @parent.is_a?(Topic) %>
      <%= f.submit "Add a todo" %>
    <% end %>