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!