Ember.js Many to many relationships. How to access data - ember.js

We're working on a messaging system which will allow a user to submit a message to many social media accounts, which would result in a post to each account. In our application we have Accounts, Messages, and Posts. A Message is a single instance of my content which can then be sent as a Post.
Accounts can have many messages.
Messages can be tied to many Accounts.
Messages can have many Posts.
Posts belong to Messages.
We Model the data like so:
Social.Account = DS.Model.extend({
username: DS.attr('string'),
messages: DS.hasMany('Social.Message')
});
Social.Message = DS.Model.extend({
user_id: DS.attr('number'),
text: DS.attr('string'),
accounts: DS.hasMany('Social.Account'),
posts: DS.hasMany('Social.Post')
});
Social.Post = DS.Model.extend({
created: DS.attr('date'),
text: DS.attr('string'),
message: DS.belongsTo('Social.Message')
});
and FIXTURE data would look like this:
Social.Account.FIXTURES = [{
id: 1,
username: "commadelimited",
messages: [1, 2]
}];
Social.Message.FIXTURES = [{
id: 1,
user_id: 1,
text: 'This is message #1 sent by account #1',
accounts: [1],
posts: [1]
}, {
id: 2,
user_id: 1,
text: 'This is message #2 sent by account #1',
accounts: [1],
posts: [2]
}];
Social.Post.FIXTURES = [{
id: 1,
created: "5 minutes ago",
text: 'This is post #1 sent by account #1, tied to message #1'
}, {
id: 1,
created: "5 minutes ago",
text: 'This is post #2 sent by account #1, tied to message #2'
}];
Even though in our system a Message can result in man Posts, as far as the UI is concerned Messages are a 1 to 1 relationship.
I'm having trouble thinking about how to output the resulting Post when I have to start by looping over the Message first. This code outputs the message correctly, but I need to output the post instead.
{{#each messages in messages}}
<article>
<time>{{post.ts}}</time>
<div class="post-content">
<h2>{{post.text}}</h2>
</div>
</article>
{{/each}}
I've tried {{#each messages in messages.posts}} and it's nothing. Within the each loop I've outputting {{message}} within the loop which returns <Social.Message:ember391:1>. I can access the Post data in the console with this statement
Social.Account.find().objectAt(0).get('messages').objectAt(0).get('posts')
which returns an object of the correct type.
So, how do I get at the Post array for each Message?

So, how do I get at the Post array for each Message?
You can access the post array via the posts property of each message. So in handlebars:
{{#each message in messages}}
<p>Message: {{message}}</p>
<p>Message posts: {{message.posts}}</p>
{{#each post in message.posts}}
<article>
<time>{{post.ts}}</time>
<div class="post-content">
<h2>{{post.text}}</h2>
</div>
</article>
{{/each}}
{{/each}}

My example might be a bit more "finished" than you want but since I know you are working w/ a django backend ... here is the entire m2m setup (using ember.js and the django-rest-framework)
First up -the django models
class Tag(models.Model):
description = models.CharField(max_length=200)
class Session(models.Model):
name = models.CharField(max_length=150)
tags = models.ManyToManyField(Tag)
The basic "find all" and "find each tag" urls
url(r'^/sessions/$', views.SessionList.as_view()),
url(r'^/sessions/(?P<session_pk>\d+)/tags/$', views.TagBySessionList.as_view()),
The django views (django-rest-framework enabled if you will)
from rest_framework import generics
class SessionList(generics.ListCreateAPIView):
model = Session
serializer_class = SessionSerializer
class TagBySessionList(generics.ListCreateAPIView):
model = Tag
serializer_class = TagSerializer
def get_queryset(self):
session_pk = self.kwargs.get('session_pk', None)
if session_pk is not None:
return Tag.objects.filter(session__pk=session_pk)
return []
Now the django-rest-framework serializer to get the REST api setup
from rest_framework import serializers
class SessionSerializer(serializers.ModelSerializer):
tags = serializers.ManyPrimaryKeyRelatedField()
class Meta:
model = Session
fields = ('id', 'name', 'tags')
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('id', 'description')
Next up -the ember-data models (notice -no fixture adapter here, real REST api calls)
CodeCamp.Session = DS.Model.extend({
name: DS.attr('string'),
tags: DS.hasMany('CodeCamp.Tag')
});
CodeCamp.Tag = DS.Model.extend({
description: DS.attr('string')
});
The root element that simply loads the parent element (basic find all)
CodeCamp.SessionsRoute = Ember.Route.extend({
model: function() {
return CodeCamp.Session.find();
}
});
(no need to ask for the m2m here as the DjangoRESTAdapter takes care of that given the endpoint above)
The handlebars template that does show the parent and each tag under it (m2m each working)
{{#each session in controller}}
{{session.name}}<br />
{{#each tag in session.tags}}
{{tag.description}}<br />
{{/each}}
{{/each}}

Related

How do you use v-for and/or v-if to show objects from a Django Postgres backend?

I'm trying to build something like a hierarchy tree in Vue3 that is pulling data from a Django Postgres backend. I've setup all of the models and have it all displaying in the frontend using Django Rest and Axios. The problem is it's showing everything in every branch of my hierarchy tree instead of just the "children" of that branch because I can't figure out how to filter it based on the foreign keys of my Django models. Basically, in the tree I have 4 Grandparents, each with 2 Parents under them, each with 2 Children under them. So 1 branch of the tree should be 1 Grandparent, then 2 Parents, then each with 2 Children. I can only get it to show each branch with 1 Grandparant with 4 Parents and 16 Children. The models look like this:
class Grandparent(models.Model):
grandparent_id = models.AutoField(primary_key=True)
grandparent_name = models.CharField(max_length=40)
class Parent(models.Model):
parent_id = models.AutoField(primary_key=True)
parent_name = models.CharField(max_length=40)
grandparent_id = models.ForeignKey(Grandparent, on_delete=models.CASCADE)
class Child(models.Model):
child_id = models.AutoField(primary_key=True)
child_name = models.CharField(max_length=40)
parent_id = models.ForeignKey(Parent, on_delete=models.CASCADE)
The hopefully relevant parts from my vue file look like this:
created () {
this.getGrandparents(),
this.getParents(),
this.getChildren(),
},
data () {
return {
grandparents: [],
parents: [],
children: []
}
},
methods: {
getGrandparents() {
axios({
method: 'get',
url: 'http://ipaddress:port/grandparents/',
auth: {
username: 'username',
password: 'password'
}
}).then(response => this.grandparents = response.data)
},
getParents() {
axios({
method: 'get',
url: 'http://ipaddress:port/parents/',
auth: {
username: 'username',
password: 'password'
}
}).then(response => this.parents = response.data)
},
getChildren() {
axios({
method: 'get',
url: 'http://ipaddress:port/children/',
auth: {
username: 'username',
password: 'password'
}
}).then(response => this.children = response.data)
}
},
With the relevant HTML parts of the same vue file:
<div class="level-2-wrapper">
<template v-for="grandparent in grandparents" v-bind:key="grandparent.grandparent_id">
<div class="level-2-list">
<div class="level-2 rectangle">{{ grandparent.grandparent_name }}</div>
<div class="level-3-wrapper">
<template v-for="parent in parents" v-bind:key="parent.parent_id">
<div class="level-3-list">
<div v-if="parent.grandparent_id === grandparent.grandparent_id" v-bind:key="parent.parent_id" class="level-3 rectangle">
{{ parent.parent_name }}
</div>
<div class="level-4-wrapper">
<template v-for="child in children" v-bind:key="child.child_id">
<div class="level-4-list">
<div v-if="child.parent_id === parent.parent_id" v-bind:key="child.child_id" class="level-4 rectangle">
{{ child.child_name }}
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
I've tried to change between using Templates and Divs in vue as that's what Vue is using in it's documentation but that doesn't seem to make a difference. I've tried changing where the v-for and v-if statements go, either to no effect or just not loading anything at all. I've looked at conditional rendering but all of the examples seem to use static objects in the vue file that just contain things like numbers, and then apply a number filter to them to just display even numbers but I can't seem to get anything similar to work with referencing ID's or similar. I'm at a loss and going around in circles at this point.

Model hook throwing error. Ember

In my application users have many meals. meals belong to a user. I have it so that a user can currently post a meal by creating a record and saving it. Immediately after completing this my console threw the error:
Error: Assertion Failed: You need to pass a model name to the store's modelFor method at new Error (native)
I then wrote my model hook for getting meals, when I started to get the error:
TypeError: Cannot read property 'some' of undefined
I am not certain what is causing this. Here is my code:
User Model:
import DS from 'ember-data';
import { hasMany } from 'ember-data/relationships';
export default DS.Model.extend({
email: DS.attr('string'),
height: DS.attr('number'),
weight: DS.attr('number'),
age: DS.attr('number'),
tdee: DS.attr('number'),
gender: DS.attr('string'),
activity_level: DS.attr('number'),
meals: hasMany('meal')
});
Meal model:
import DS from 'ember-data';
import { belongsTo, hasMany } from 'ember-data/relationships';
export default DS.Model.extend({
meal_type: DS.attr('string'),
created_at: DS.attr('date'),
user: belongsTo('user'),
// meal_items: hasMany('meal_item')
});
Meal route:
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('meal');
},
actions: {
createMeal(data) {
let meal = this.get('store').createRecord('meal', data);
meal.save();
}
}
});
I am new to ember, but have a hunch it may be related to explicit inverses? An advice is appreciated. I can post more code if something needs clarification.
EDIT:
The GET request to the server successful. The meal data is in the DS.
So I pass the createMeal action down to my create-meal component:
{{create-meal meals=model createMeal="createMeal"}}
The component sends this action back up on submit:
import Ember from 'ember';
export default Ember.Component.extend({
form: {},
actions: {
submit (){
this.sendAction('createMeal', this.get('form'));
}
}
});
Here is my template for that component:
<label class="col-md-3">Create A Meal</label>
<div class="input-group col-md-8 col-centered create-meal-div">
{{input value=form.meal_type type="text" class="form-control" placeholder="Enter Meal Name" aria-describedby="basic-addon2"}}
<span class="input-group-addon" id="basic-addon2"><button type="submit" class="btn-primary" {{action "submit" form on "submit"}}> Start Adding Food</button></span>
</div>
<div class="col-md-6 food-search-div">
{{food-search}}
</div>
EDIT 2:
rails controller meal create action:
def create
# binding.pry
#meal = current_user.meals.build(meal_params)
if #meal.save
render json: #meal, status: :created, location: #meal
else
render json: #meal.errors, status: :unprocessable_entity
end
end
SOLVED:
REMOVING the user: belongsTo('user'), in my meal model go rid of the error.
I think the problem is here in this line - router
return this.get('store').findAll('meal');
Try this but it is weird it should work
model() {
return this.store.findAll('meal');
}
Inside controllers you need to use this.get('store') when you are inside route just this.store. Try that and let me know if it works. Also inside your route set the action to accept the model as param, like this
actions: {
update(model){
model.save().then( ....
This can be triggered from template like this
<form {{action 'update' model on='submit'}} autocomplete="off">
In this way you do not need to get this.store in your route - you will have a model passed and you can just go to save
Hope it helps
EDIT: also this would prevent ember cli to precompile
user: belongsTo('user'),
// meal_items: hasMany('meal_item')
so it should be without ,
user: belongsTo('user')
// meal_items: hasMany('meal_item')

Calling a relationship in template returns a DS.PromiseObject

I'm using firebase in combination with Ember CLI. I have the following setup:
ember.debug.js:6401 DEBUG: Ember : 2.4.5
ember.debug.js:6401 DEBUG: Ember Data : 2.5.1
ember.debug.js:6401 DEBUG: Firebase : 2.4.2
ember.debug.js:6401 DEBUG: EmberFire : 1.6.6
ember.debug.js:6401 DEBUG: jQuery : 2.2.3
I have two simple models
<!-- app/models/user.js -->
import Model from 'ember-data/model';
export default Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
email: DS.attr('string'),
profile: DS.belongsTo('profile', {async: true})
});
And a second model for my profile
<!-- app/models/profile.js -->
import Model from 'ember-data/model';
export default Model.extend({
companyName: DS.attr('string'),
user: DS.belongsTo('user', {async: true})
});
I have the following profile route:
<!-- app/routes/profile.js -->
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.query('user', { orderBy: 'email', equalTo: this.get('session.currentUser.email')}).then(function(user){
console.log(user);
return user;
});
}
});
I check if the current session email address is equalTo a email address in the database. And return the user objet. This is working. (don't know if this is the right way to do this?)
In my profile handlebars template i have the following code.
<!-- app/templates/profile.hbs -->
{{#each model as |user|}}
{{user.firstName}}
{{user.lastName}}
{{user.profile}}
{{/each}}
This returns the following on screen:
frank spin <DS.PromiseObject:ember545>
My guess is that the relationship data has not yet been received. I don't know how to solve this issue. And second question: Is my checking for the current logged in user the right way?
You're correct that the relationship data has not been received. But if you write your template like this, you should still see the profile information when it loads:
{{#each model as |user|}}
{{user.firstName}}
{{user.lastName}}
{{user.profile.companyName}}
{{/each}}
PromiseObjects in templates
The goal of promise objects (and promise arrays) is to allow you to bind data in Ember before it's loaded, and have those bindings update once the promise resolves. This is great for secondary-importance information, which can safely be rendered after the rest of the page loads.
If you try to render your profile model properties when the promise has not resolved, you'll get a blank space. You can display loading state information using the isPending property:
{{#each model as |user|}}
{{user.firstName}}
{{user.lastName}}
{{#if user.profile.isPending}}
<span class="spinner">…</span>
{{else}}
{{user.profile.companyName}}
{{/if}}
{{/each}}
And you can also use the isRejected property to detect API failures, and allow users to retry.
Load before render
If this asynchronous behaviour is not what you want, you can force the relationship promise to resolve before rendering the template in the afterModel hook of your route:
export default Ember.Route.extend({
model() {
return this.store.query('user', { orderBy: 'email',
equalTo: this.get('session.currentUser.email')});
},
afterModel(users) {
return Ember.RSVP.all(users.invoke('get', 'profile');
}
});
(With a simpler single model, you could just write return model.get('profile') in the afterModel hook.)
Any promise returned from afterModel will block loading of the route until it resolves. Then your template will always have the profile of the user available when rendering.

Ember Routing with renderTemplate and setupController

I am building an Ember app to show a simple Twitter-like tagging system. When a user visits /items, he or she will see a list of all items. When the user visits /tags, the user will see a list of tags as links. When the user clicks one of these links, the user should be directed to /tags/:id and will see all items tagged with that specific tag. Then the user will be able to search/sort/manipulate the items as he/she would be able to from the ItemsRoute.
How can I make TagRoute use ItemsController and render the items template, using the tag's associated items as the model?
I have tried different combinations of the hooks in TagRoute, and I'm not able to find a recipe that works. There seems to be a fundamental misunderstanding on my part.
Here is my relevant code:
router.js.coffee
App.Router.map ()->
#resource 'items'
#resource 'tags', ->
#resource 'tag', path: ':tag_id'
routes/tag.js.coffee
App.TagRoute = Ember.Route.extend
model: (params)->
#get('store').find 'tag', params.tag_id
controllerName: 'items'
setupController: (controller, model)->
#controllerFor('items').set('model', model.items)
renderTemplate: ->
#render 'items', ->
into: 'tags'
controller: 'items'
templates/tags.hbs
<ul class="tag-list">
{{#each tag in model}}
<li>
{{#link-to 'tag' tag}}
{{tag.name}}
{{/link-to}}
</li>
{{/each}}
</ul>
{{outlet}}
models/items.js.coffee
App.Item = DS.Model.extend(
body: DS.attr('string')
checked: DS.attr('boolean')
tags: DS.hasMany('tag')
)
models/tags.js.coffee
App.Tag = DS.Model.extend(
name: DS.attr('string')
taggings_count: DS.attr('number')
items: DS.hasMany('item')
)
Currently, this give me an error:
Error while processing route: tag Cannot assign to read only property 'name' of function () {
return {
into: 'tags',
controller: 'items'
};
} TypeError: Cannot assign to read only property 'name' of function () {
return {
into: 'tags',
controller: 'items'
};
}
Looking at the Ember Routes Inspector in Chrome, the controllerName property is the only one which overrides Ember's defaults, and Ember still tries to render a generated tag template.
As ahmed.hoban suggested, I have solved this using query params. This helps me avoid duplicating routes and having a tangled router. It hits the database, which is not preferable, but I'm not sure at this point if I'll make that a requirement. I have control over the full-stack, so I was able to make adjustments on the back-end to support the request.
router.js.coffee
App.Router.map ()->
#resource 'tags', path: '/', ->
#resource 'items'
routes/tag.js.coffee - deleted
templates/tags.hbs
<ul class="tag-list">
{{#each tag in model}}
<li>
{{#link-to 'items' (query-params tag=tag.id)}}
{{tag.name}}
{{/link-to}}
</li>
{{/each}}
</ul>
{{outlet}}
controllers/items.js.coffee
App.ItemsController = Ember.ArrayController.extend(
needs: 'tags'
queryParams: ['tag']
tag: null
items: (->
tag = #get 'tag'
if tag
#store.find 'item', tag: tag
else
#get 'model'
).property('tag', 'model')
)

Accessing relations from templates with ember-data 1.0.0 beta

App.User = DS.Model.extend({
posts: DS.hasMany('post', {async: true})
});
App.Post = DS.Model.extend({
body: DS.attr(),
user: DS.belongsTo('user')
});
App.ProfileRoute = Ember.Route.extend({
model: function(params) {
return this.get('store').find('user', params.user_id)
}
});
and in template
{{#each post in model.posts}}
{{post.body}}
{{/each}}
json for user. I don't want embed posts in user json
{user: { posts: [1, 2, 3] }}
This don't render anything. It receives posts json from server after this error occur
Assertion failed: You looked up the 'posts' relationship on '' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.attr({ async: true }))
In chrome inspector I see all data loaded properly.
How can I solve this? Should I preload all models I want to use in templates?
The model function in your route is missing the return, so the error is being thrown when you try to access model.posts because there is no model.
App.ProfileRoute = Ember.Route.extend({
model: function(params) {
return this.get('store').find('user', params.user_id);
}
});
Have you tried to just write {{#each posts}}?
Worked for my project.
Then write {{body}} within the each block.
Let me know, thanks!