Ember js - saving models with relationship - ember.js

My server is returning a json data and I have no problem loading the models (page.js, event.js, choice.js) with Ember Data. But when the form is submitted, the JSON data submitted to the server doesn't contain the related models (event.js, choice.js).
Below are my files and the json data.
Json data returned by backend api:
{
"data": {
"type": "pages",
"id": "12345",
"attributes": {
"guest_id": null,
"name": null,
"email": null,
"address": null
},
"relationships": {"events": {"data": [
{
"type": "events",
"id": "67891"
},
{
"type": "events",
"id": "90908"
}
]}}
},
"included": [
{
"type": "events",
"id": "67891",
"attributes": {
"event_id": "67891",
"name": "Event 1"
},
"relationships": {"choices": {"data": [
{
"type": "choices",
"id": "67891-11111"
},
{
"type": "choices",
"id": "67891-22222"
}
]}}
},
{
"type": "events",
"id": "90908",
"attributes": {
"event_id": "90908",
"name": "Event 2"
},
"relationships": {"choices": {"data": [
{
"type": "choices",
"id": "90908-11111"
},
{
"type": "choices",
"id": "90908-22222"
}
]}}
},
{
"type": "choices",
"id": "67891-11111",
"attributes": {
"choice_id": "67891-11111",
"name": "Diet choice",
"value": "0"
},
"relationships": null
},
{
"type": "choices",
"id": "",
"attributes": {
"choice_id": "67891-22222",
"name": "No. of adult guest",
"value": "0"
},
"relationships": null
}
{
"type": "choices",
"id": "90908-11111",
"attributes": {
"choice_id": "90908-11111",
"name": "Diet choice",
"value": "0"
},
"relationships": null
},
{
"type": "choices",
"id": "90908-22222",
"attributes": {
"choice_id": "90908-22222",
"name": "No. of adult guest",
"value": "0"
},
"relationships": null
}
]
}
JSON data submitted to the server
{
"data": {
"id":"e47e8358-0f18-4607-b958-2877155bf5be",
"attributes":{
"guest_id":null,
"name":"my name",
"email":"myemail#gmail.com",
"address":"myaddress"
},
"relationships":{
"events":{
"data":[
{
"type":"events",
"id":"67891"
},
{
"type":"events",
"id":"90908"
}
]
}
},
"type":"pages"
}
}
/pages/show.hbs
<p>
<label>Name: </label>
{{input type="text" value=model.name id="name"}}
</p>
{{#each model.events as |event|}}
<h3>
{{event.name}}
<!-- Rounded switch -->
<label class="switch">
<input type="checkbox" class="switch_input" id="{{event.id}}">
<span class="slider round"></span>
</label>
</h3>
{{#each event.choices as |choice|}}
{{#if (is-equal choice.name "Diet choice")}}
<p>
<label for="diet_choice">{{choice.name}}:</label>
<select id="diet_choice" value=choice.value>
<option value="anything">Anything and Everything</option>
<option value="vegetarian">Vegetarian</option>
<option value="hala">Hala</option>
</select>
</p>
{{/if}}
{{#if (is-equal choice.name "No. of adult guest")}}
<p>
Adult guest
<div>
<button type="button" name="btnMinusGuest" {{action "minusCounter" choice 0 "Minimum 0 guest"}}>-</button>
{{input type="text" value=choice.value}}
<button type="button" name="btnPlusGuest" {{action "addCounter" choice 1 "Maximum 1 guest"}}>+</button>
</div>
</p>
{{/if}}
{{/each}}
{{/each}}
<p>
<label for="email">Email:</label>
{{input type="text" value=model.email}}
</p>
<p>
<label for="address">Address:</label>
{{input type="text" value=model.address}}
</p>
<p>
<input type="submit" name="btnSubmit" value="Submit" {{action "submit"}} />
<input type="submit" name="btnCancel" value="Cancel" {{action "cancel"}} />
</p>
{{outlet}}
/routes/pages/show.js
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
event: ''
},
model(params) {
return this.get('store').findRecord('page', params.page_id, { adapterOptions: {query: {'event': params.event}}});
},
actions: {
submit() {
// Create rec
page.save().then(function() {
console.log('submitted');
}).catch(function(reason) {
console.log(reason);
});
},
cancel() {
alert("Are you sure?");
},
addCounter(item, max_val, msg) {
let current_val = parseInt(item.get('value'));
if (current_val >= max_val) {
alert(msg)
} else {
item.set('value', current_val + 1);
}
},
minusCounter(item, min_val, msg) {
let current_val = parseInt(item.get('value'));
if (current_val <= min_val) {
alert(msg);
} else {
item.set('value', current_val - 1)
}
},
}
});
/models/page.js
import DS from 'ember-data';
export default DS.Model.extend({
guest_id: DS.attr(),
name: DS.attr(),
email: DS.attr(),
address: DS.attr(),
is_e_invite: DS.attr(),
data_time_submitted: DS.attr(),
events: DS.hasMany('event')
});
/models/event.js
import DS from 'ember-data';
export default DS.Model.extend({
event_id: DS.attr(),
name: DS.attr(),
choices: DS.hasMany('choice')
});
/models/choice.js
import DS from 'ember-data';
export default DS.Model.extend({
choice_id: DS.attr(),
name: DS.attr(),
value: DS.attr()
});

One way to solve it, is to save each model independently, as #Lux has said in a comment, but you can also write a custom serializer which will semi-manually prepare a data to be pushed after saving a parent model.
Assuming you're using ember-data, it should probably look something like this (see https://guides.emberjs.com/v3.0.0/models/customizing-serializers/):
Create a file called serializers/page.js and put there:
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
events: {embedded: 'save'}
},
serializeIntoHash: function (data, type, record, options) {
const object = this.serialize(record, options);
for (let key in object) {
data[key] = object[key];
}
},
serializeHasMany: function (record, json, relationship) {
const key = relationship.key;
const hasManyRecords = record.hasMany(key);
if (!hasManyRecords || !this.attrs[key] || this.attrs[key].embedded !== 'save') {
return this._super(record, json, relationship);
}
json[key] = [];
hasManyRecords.forEach(item => {
let recordData = item.serialize({includeId: true});
if (relationship.options.deepEmbedded) {
relationship.options.deepEmbedded.forEach(deepKey => {
if (!item.hasMany(deepKey)) {
return true;
}
const deepRecords = item.hasMany(deepKey);
recordData[deepKey] = [];
deepRecords.forEach(deepRecord => {
recordData[deepKey].push(deepRecord.serialize({includeId: true}));
});
});
}
json[key].push(recordData);
});
}
});
and in your page model change events: DS.hasMany('event') to events: DS.hasMany('event', {deepEmbedded: ['choices']}).
Above serializer tells ember-data to put each event of a page as a serialized map into output map of a page. In addition, for each event, it also walks through each deepEmbedded model of a relationship - choices in this case - and also serializes them.
Note: the deepEmbedded option is not part of ember-data and I created it for my specific needs, so it may not work perfectly in every case.

Related

Load multiple model data in same api call emberjs?

So here is two models that i have defined in emberjs
match.js
import DS from 'ember-data';
export default DS.Model.extend({
team: DS.belongsTo('team', {async:true}),
opponent: DS.belongsTo('team', {async: true}),
type: DS.attr('string'),
squad: DS.attr('boolean')
});
and
team.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
logo: DS.attr('string')
});
I am already loading the match as a model. In the same api call i also want to load the model data for team. The api response that i have till now is
{
"meta":{
"type":"match"
},
"data":[
{
"id":1119536,
"type":"match",
"attributes":{
"id":1119536,
"team":{
"type":"team",
"id":1,
"attributes":{
"id":1,
"name":"England",
"logo":null
}
},
"opponent":{
"type":"team",
"id":3,
"attributes":{
"id":3,
"name":"Pakistan",
"logo":null
}
}
}
}
]
}
The match model data get loaded properly but i am having issues for the same with team data. The response is from network in browser and i already checked the model using ember plugin on browser that team data doesn't load. How can i use the same api call to load multiple models.
a few things to notice:
dont put the id in attributes
dont name an attribute type. Really dont! It's a reserved keyword.
relationships are not attributes and should be under relationships
use the included array to sideload data
ids must be strings
so for example this would be a valid payload:
{
"meta": {
"type": "match"
},
"data": [
{
"id": "1119536",
"type": "team",
"attributes": {
"match-type": "match"
},
"relationships": {
"team": {
"data": {
"type": "team",
"id": "1"
}
},
"opponent": {
"data": {
"type": "team",
"id": "3"
}
}
}
}
],
"included": [
{
"type": "team",
"id": "1",
"attributes": {
"name": "England",
"logo": null
}
},
{
"type": "team",
"id": "3",
"attributes": {
"name": "Pakistan",
"logo": null
}
}
]
}

knockout - how to use template data in a computed function

The objective is to show a filtered list of items for each possible state. I'm trying a template because such a list may need to display in many places. The template calls filterItems to get its particular list of items.
The filterItems code below uses a field in the viewModel (current_filter), so all the lists have the same items (3 and 8 which are in a ready state) :( That's not the idea. Each template instance has $data which contains the correct filter value. Is there a way to make this filter value available to the filterItems function?
<script>
var viewModel = {
items: ko.observableArray(
[
{ "iid": 1, "state": "entered" },
{ "iid": 3, "state": "ready" },
{ "iid": 4, "state": "delivered" },
{ "iid": 8, "state": "ready" },
{ "iid": 13, "state": "entered" }
]),
states: ko.observableArray(
[
{ "sid": 1, "filter": "entered", "color": "yellow" },
{ "sid": 2, "filter": "ready", "color": "red" },
{ "sid": 3, "filter": "delivered", "color": "blue" }
]),
current_filter: 'ready'
}
viewModel.filterItems = ko.computed(function () {
var filtered_items = [];
for (var i = 0; i < viewModel.items().length; ++i)
if(viewModel.items()[i].state == viewModel.current_filter)
filtered_items.push(viewModel.items()[i]);
return filtered_items;
}, viewModel);
</script>
<div data-bind="foreach: states">
<div data-bind="template: {name: 'state', data: $data}"></div>
<script type="text/html" id="state">
<h2 data-bind="text: filter"></h2>
<ul data-bind="foreach:viewModel.filterItems()">
<li>
<div data-bind="text: iid"></div>
</li>
</ul>
</script>
</div>
<script>ko.applyBindings(viewModel);</script>
jsFiddle at https://jsfiddle.net/mthrock/kxpfdfva/8/#&togetherjs=JKJOos6VJc
how about a component instead of a template?
run snippet below
ko.components.register('mycomponent', {
viewModel: function(params) {
var self = this;
this.items = ko.observableArray(
[{
"iid": 1,
"state": "entered"
}, {
"iid": 3,
"state": "ready"
}, {
"iid": 4,
"state": "delivered"
}, {
"iid": 8,
"state": "ready"
}, {
"iid": 13,
"state": "entered"
}]);
this.filter = params.filter;
this.filterItems = ko.computed(function() {
var filtered_items = [];
for (var i = 0; i < this.items().length; ++i)
if (this.items()[i].state == this.filter)
filtered_items.push(this.items()[i]);
return filtered_items;
}, this);
},
template: ' <h2 data-bind="text: filter"></h2>\
<ul data-bind="foreach:filterItems()">\
<li>\
<div data-bind="text: iid"></div>\
</li>\
</ul>'
});
function model() {
var self = this;
this.states = ko.observableArray(
[{
"sid": 1,
"filter": "entered",
"color": "yellow"
}, {
"sid": 2,
"filter": "ready",
"color": "red"
}, {
"sid": 3,
"filter": "delivered",
"color": "blue"
}]);
}
var mymodel = new model();
$(document).ready(function() {
ko.applyBindings(mymodel);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div data-bind="foreach: states">
<mycomponent params="filter: filter"></mycomponent>
</div>

Ember: Access sideloaded model relationship data in template

I am trying to display a model's relationships in a template from sideloaded data, but there seems to be some problem. With Ember Inspector, I see that the relationships are correctly loaded from the data. However, the data is not displayed on the page.
Looking for solutions or suggestions on where to start debugging. Much appreciated.
Handlebars:
<dt>Categories</dt>
<dd>
<ul>
{{#each model.categories as |category| }}
<li>{{ category.name }}</li>
{{/each}}
</ul>
</dd>
The route:
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model(params) {
return this.store.findRecord('datasheet', params.id);
}
});
The models:
// app/models/datasheet.js
export default DS.Model.extend({
name: DS.attr('string'),
name_en: DS.attr('string'),
news: DS.attr('string'),
news_en: DS.attr('string'),
basic_information: DS.attr('string'),
basic_information_en: DS.attr('string'),
id_gradient_default: DS.attr('string'),
icon_name: DS.attr('string'),
icon_color: DS.attr('string'),
order: DS.attr('string'),
item_count: DS.attr('string'),
categories: DS.hasMany('category')
});
// app/models/category.js
export default DS.Model.extend({
name: DS.attr('string')
});
This is the JSON returned from the adapter method:
{
"data": {
"type": "datasheet",
"id": "21",
"attributes": {
"name": "Projekty",
"name_en": "Projects",
"news": "",
"news_en": "",
"basic_information": "",
"basic_information_en": "",
"id_gradient_default": "27",
"icon_name": "pin_flag",
"icon_color": "",
"order": "14"
},
"relationships": {
"categories": ["18", "19", "20", "51", "52"]
}
},
"included": [{
"type": "category",
"id": "18",
"attributes": {
"name": "Project"
}
}, {
"type": "category",
"id": "19",
"attributes": {
"name": "Activity"
}
}, {
"type": "category",
"id": "20",
"attributes": {
"name": "Project phase"
}
}, {
"type": "category",
"id": "51",
"attributes": {
"name": "Program"
}
}, {
"type": "category",
"id": "52",
"attributes": {
"name": "Milestone"
}
}]
}
Ember Inspector screenshot:
This is not correct JSONAPI:
"relationships": {
"categories": ["18", "19", "20", "51", "52"]
}
This is the correct JSONAPI equivalent:
"relationships": {
"categories": {
data: [{
id: '18',
type: 'category'
},{
id: '19',
type: 'category'
},{
id: '20',
type: 'category'
},{
id: '51',
type: 'category'
},{
id: '52',
type: 'category'
}]
}
}
So your data are loaded but not correctly linked. you can see this in the ember-inspector when you check the categories relationship.

How to display attributes of belongsTo object in Ember.js template

In my Ember app, a survey belongsTo a user; a user hasMany surveys. In my template, I would like to display a list of surveys, and the name of the user that created them. For now, I am pushing side-loaded data into the store via the application route, and it is showing up in the ember inspector->Data. The survey info is displaying correctly in the template, but the corresponding user's firstName will not appear. Help/guidance appreciated.
survey.js (model)
import DS from 'ember-data';
export default DS.Model.extend({
user: DS.belongsTo('user', {async: true}), //tried without async as well
title: DS.attr(),
post: DS.attr()
});
user.js (model)
import DS from 'ember-data';
export default DS.Model.extend({
surveys: DS.hasMany('survey', {async: true}),
firstName: DS.attr()
});
application.js (application route)
export default Ember.Route.extend({
model() {
this.store.push({
data: [{
id: 1,
type: 'survey',
attributes: {
title: 'My First Survey',
post: 'This is my Survey!'
},
relationships: {
user: 1
}
}, {
id: 2,
type: 'survey',
attributes: {
title: 'My Second Survey',
post: 'This is survey 2!'
},
relationships: {
user: 1
}
}, {
id: 1,
type: 'user',
attributes: {
firstName: 'Tyler'
},
relationships: {
surveys: [1, 2]
}
}]
});
}
});
surveys.js (route)
export default Ember.Route.extend({
model () {
return this.store.findAll('survey');
}
});
surveys.hbs (template)
<ul>
{{#each model as |survey|}}
<li>
<strong>{{survey.title}}</strong> //This works
<br>
{{survey.post}} //This works
<br>
Author: {{survey.user.firstName}} //This does not work
</li>
{{/each}}
</ul>
SOLUTION - updated application.js
export default Ember.Route.extend({
model() {
this.store.push({
"data": [ //Added double quotes throughout to conform to documentation
{
"id": "1",
"type": "survey",
"attributes": {
"title": "My First Survey",
"post": "This is my Survey!"
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "user"
}
}
}
}, {
"id": "2",
"type": "survey",
"attributes": {
"title": "My Second Survey",
"post": "This is survey 2!"
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "user"
}
}
}
}
],
"included": [
{
"id": "1",
"type": "user",
"attributes": {
"firstName": "Tyler"
} //no need to include user's relationships here
}
]
});
}
});
Payload relationship part is not correct. Should be:
relationships: {
user: {
data: {
id: 1,
type: 'user'
}
}
}
Also I think "user" payload should be in "included" section.
JSONAPISerializer api

Ember data rendering hasMany

In my ember app I have a models:
App.Schedule = DS.Model.extend({
manager:DS.belongsTo('App.Manager', { embedded: true }),
entries:DS.hasMany('App.Reservation', { embedded: true })
});
App.Reservation = DS.Model.extend({
name:DS.attr('string')
});
and a handelbars view:
{{#each schedule in controller}}
<td>
{{#each reservation in schedule.entries)}}
<div>{{reservation.name}}</div>
{{/each}}
</td>
{{/each}}
But with this view a've got exception
Expecting 'ID', got 'undefined'
This workaround works, but I know this is wrong way.
{{#each reservation in schedule._data.hasMany.entries}}
Any ideas?
EDIT.
After Mike Aski answer.
My JSON, returning from backend.
{
"schedules": [
{
"id": "476a3881-4fe8-42f5-8bdb-650d38f911e8",
"entries": [
{
"name": "test1",
"begin": "2012-11-22T10:00:00+06:00",
"end": "2012-11-22T11:00:00+06:00",
"id": "71c6da83-8ae2-4210-90f8-e65b06f819d7"
},
{
"name": "test2",
"begin": "2012-11-22T12:00:00+06:00",
"end": "2012-11-22T14:00:00+06:00",
"id": "d234c8c0-66f5-4e98-b921-4472b39a98f7"
}
]
},
{
"id": "8d9b1539-8a1f-4a5d-acfc-5918e61e3990",
"entries": []
},
{
"id": "a279d9a5-ea88-4012-8094-8a30125fd32b",
"entries": []
}
]
}
Did you ensure you have an id property in the reservations JSON objects?
You should have them, even if relations are embedded.