Persisting Records with Has Many/Belongs To in Ember

In this post, we'll build out a feature in an Ember app that allows a user create and persist new records of the has many/belongs to relationship.

This app has two models, Song and Artist. A song has many artists and an artist belongs to a song (think "artists collaborating on a song").

We'll be using Ember's Embedded Record Mixin, Active Model Serializers and the Ember Multiselect Checkboxes component to post a new song and its associated artists to our backend--a Rails API.

We want our user to be able to visit the page that shows the form for a new song and see a form field for the song's name and a list of checkboxes of existing artists. Our user should be able to select multiple artists from the list and have those artists persisted as the artists of the new song being created. Something like this:

Let's get started!

Note: This post assumes that we already have our Rails API up and running with Rails Active Model Serializer. It assumes our Ember up is also up and running but we'll take a closer look at the Ember models and serialzers. We'll mainly be focusing on implementing Ember's Active Model Serializer with the Embedded Record Mixin as well as implementing the Ember Multiselect Checkbox component.

Ember Models and Serializer


Models

Let's take a closer look at our Ember models:

// app/models/song.js

import DS from 'ember-data';  
export default DS.Model.extend({  
  name: DS.attr('string'),
  artists: DS.hasMany('artist', { async: true })
});
import DS from 'ember-data';

export default DS.Model.extend({  
  name: DS.attr('string'),
  songs: DS.belongsTo('song', { async: true }),
});

Notice that both models use the async: true option when defining their relationships. This is because my Rails API is not side loading associated records but serving a given record along with just the ids of its associated records.

Serializers

Since my Rails API is using Active Model Serializers, and my Ember app is using the Active Model Adapter, we need to use Ember's Active Model Serializers.

Why do we need an Ember serializer at all? Well, we need to tell our Ember app that when it posts a song record to the API, it should include, or embed, the records of any artists associated to that song. For this we need to customize a serializer using the Embedded Record Mixin.

Let's take a look at our Song serializer:

// app/serializers/song.js

import DS from 'ember-data';

import { ActiveModelSerializer } from 'active-model-adapter';

export default ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin, {  
  attrs: {
    artists: {serialize: 'records'}
  }
});

Here we are specifying that when a song record is posted to the API, the records of associated artists should be included in that payload. We are choosing to serialize records, which means "send all of the data regarding an associated artist". We could instead specify just ids if we wanted to.

Now, when we post a song record to the API, the payload that gets sent should look something like this:

{"song"=>
  {"name"=>"The Remix II",
   "artists"=>[
      {"id"=>"1", "name"=>"Justin Bieber"},   
      {"id"=>"5", "name"=>"The Weeknd"}
   ]
  }
}

Using the Multiselect Checkbox Component

First, run ember install ember-multiselect-checkboxes in the command line in the directory of your Ember app.

Now we can use it in our form:

<form>  
  <label>Song Name:</label>
  {{input value=model.name class="form-control"}}

  <label>Artists:</label>
  {{multiselect-checkboxes 
    options=allArtists
    selection=selectedArtists
    labelProperty="name"}}

  <button {{action "save"}}>Save</button>

</form>  

Let's break this down. The component takes in a few arguments.

  • options: The collection with which we want to populate our checkboxes.
  • selection: The property in which we will store the collection of selected items.
  • labelProperty: The attribute of the Ember object that will be used to label each checkbox.

Now that we know what data we need to give our component, let's build out that data.

Passing a collection of all artists into the component

Since this component is being rendered on the songs/new template, we'll be working with a songs/new controller.

In this controller, we'll define a property, allArtists. This will be a computed property that queries the store for and returns all of the artist records.

// app/controllers/songs/new.js

import Ember from 'ember';

export default Ember.Controller.extend({

  allArtists: Ember.computed(function(){
    return this.store.findAll('artist');
  })

});

Now we can successfully pass allArtists for the options property of our component. Likewise, we can set the labelProperty of that component to name and the component will use the name attribute of each artist object from the allArtist collection to label the checkboxes that are generated.

Storing Selected Items

Our component has a property, selected, that needs to be set equal to a collection. This collection will automatically become populated with the selected objects from our checkboxes.

So, we need to assign a property to our songs/new controller to an empty array.

// app/controllers/songs/new.js


import Ember from 'ember';

export default Ember.Controller.extend({

  allArtists: Ember.computed(function(){
    return this.store.findAll('artist');
  }), 

  selectedArtists: []
});

Now we can pass selectedArtists in to our component as the selected property. Then, later on when we need to retrieve the selected artists collection in our controller, we can do so via this.get('selectedArtists').

Let's take a last look at our component now that all of its properties are successfully set:

{{multiselect-checkboxes 
    options=allArtists
    selection=selectedArtists
    labelProperty="name"}}

Saving Songs with Associated Artists from the Selected Collection

Now we're ready to define the save action of our songs/new controller:

import Ember from 'ember';

export default Ember.Controller.extend({

  allArtists: Ember.computed(function(){
    return this.store.findAll('artist');
  }), 

  selectedArtists: [],

  actions: {
    save(){

      let song = this.get('model');
      song.set("artists", this.get("selectedArtists"));
      song.save().then(()=>{
        this.transitionToRoute('songs.song', song);
      })
    }
  }
});

The line we need to pay attention to is this:

song.set("artists", this.get("selectedArtists"));  

Remember that the artist objects that were selected via our checkbox component were then automatically stored in the selectedArtists property of this controller, thanks to our mutliselect-checkbox component.

So, our save action needs to retrieve the collection of selected artists stored in selectedArtists, assign that collection to the artists property of our new song, and then post that new song (with its associated artists) to the API.

We're almost done! We just need to update the #create action of our Rails Songs Controller to save the songs with its artists:

module Api  
  module V1
    class SongsController < ApplicationController

      ...

      def create
        song = Song.new(song_params)
        artists_params[:artists].each do |artist_hash|
          song.artists << Artist.find_by(id: artist_hash[:id], name: artist_hash[:name])
        end
        song.save
        render json: song
      end

      private

        def song_params
          params.require(:song).permit(:name)
        end

        def artists_params
          params.require(:song).permit(:artists => [:id, :name])
        end
    end
  end
end  

And that's it!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus