Coming to Ember from Rails, one of the places I'm struggling is trying to figure out Ember's definitions of models, views, and controllers.
I'm just testing out some sample Ember code. I'm getting my user events via the GitHub API, and I want to change the type name into something readable.
I have a jsbin here, but here's the gist:
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function(){
return Ember.$.getJSON('https://api.github.com/users/thenickcox/events').then(function(data){
return data.splice(0,7);
});
}
});
I have a method that types a type and returns a string:
interpretType: function(type){
if (type === 'PushEvent') {
return 'Pushed';
}
return name;
}
In Rails, this would go on the model. But the only model here is the one that Ember created in memory by default (right?). So then I thought, it's something that each member of the array needs, because here's the view:
<h3> Some events</h3>
<ul>
{{#each}}
<li>I {{interpretType(type)}} to {{repo.name}}</li>
{{/each}}
</ul>
So is that something that goes on Ember.ArrayController? I tried that, like this:
App.IndexController = Ember.ArrayController.extend({
interpretType: function(type){
if (type === 'PushEvent') {
return 'Pushed';
}
return name;
}.property()
});
That just gave me an error. Where do I put this?
PS. So you don't have to look at the GitHub API, here's an example JSON object:
{
id: "1890853674",
type: "CreateEvent",
actor: {
id: 702327,
login: "thenickcox",
gravatar_id: "63f35d9e50dfd73281126b051a51668a",
url: "https://api.github.com/users/thenickcox",
avatar_url: "https://2.gravatar.com/avatar/63f35d9e50dfd73281126b051a51668a?d=https%3A%2F%2Fa248.e.akamai.net%2Fassets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png&r=x"
},
repo: {
id: 14463966,
name: "thenickcox/whiskey_taster",
url: "https://api.github.com/repos/thenickcox/whiskey_taster"
},
payload: {
ref: "master",
ref_type: "branch",
master_branch: "master",
description: "My first ember/rails app"
},
public: true,
created_at: "2013-11-17T09:00:17Z"
},
Here is an updated JSBin
Basically, the each can specify an itemController to decorate the model.
App.EventController = Ember.ObjectController.extend({
interpretType: function(){
var type = this.get('model.type');
if (type === 'PushEvent') {
type = 'Pushed';
}
return type;
}.property('model.type')
});
Handlebars doesn't have functions as you've written it, but since we are now using the event controller which wraps the single model, we just refer to interpretType to do the translation:
{{#each itemController='event'}}
<li>{{interpretType}} to {{repo.name}}</li>
{{/each}}
Put it inside an Ember.ObjectController
ArrayController's deal with methods related to the collection of data from the model, whereas ObjectController deals with methods related to the specific object.
I'm also learning Ember from a Rails background.
If you haven't already come across this, you will definetely want to check out ember-tools, it's a command line generator very similar to what we've got in rails. I cant imagine building an Ember app without something like it..
Related
While trying to execute the following code on show() we get an exception that the links attribute cannot find the model either if it is specified by class or if it is specified by entityName.
Ext.define('myapp.view.film.FilmsViewController', {
//extend: 'myapp.view.base.ViewController',
extend: 'Ext.app.ViewController',
alias: 'controller.films',
onAdd: function(button, event, options) {
this.createDialog(null)
},
createDialog: function(record) {
var me = this;
var view = me.getView(); //here is film panel
me.isEdit = !!record; //convert record to boolean
me.dialog = view.add({ //#3
xtype: 'filmwindow',
viewModel: { //#4
data: { //#5
title: record ? 'Edit: ' + record.get('title') : 'Add New Film',
},
links: { //#6
currentFilm: record || { //#7
//type: 'Film',
type: 'myapp.model.film.Film',
create: true
}
}
},
//session: true
});
me.dialog.show();
},
If we comment the links part of the code the rest is working ok.
Here is the interesting part of the exception:
[E] Ext.app.ViewModel.getRecord(): Invalid model name: myapp.model.film.Film
log # ext-all-rtl-debug.js?_dc=1446847440066:9121
Ext.apply.raise # ext-all-rtl-debug.js?_dc=1446847440066:2606
Ext.raise # ext-all-rtl-debug.js?_dc=1446847440066:2691
Ext.define.privates.getRecord # ext-all-rtl-debug.js?_dc=1446847440066:99865
Ext.define.linkTo # ext-all-rtl-debug.js?_dc=1446847440066:99748
Ext.define.privates.applyLinks # ext-all-rtl-debug.js?_dc=1446847440066:100120
If you dive into the source code you will find that the if statement that checks whether myapp.model.film.Film is a class fails..
After spending more than an entire day and using our wildest imagination we managed to figure out what is going on:
First of all check this link: https://www.sencha.com/forum/showthread.php?299699-Any-use-of-a-model-schema-breaks-Tree-model-even-if-not-extending.&p=1118964&viewfull=1#post1118964
You will find out that if you use more than one schema in your source code for no apparent reason these schemas conflict with each other and you are forced to provide a unique schema id.
Now this custom configuration should be propagated to all other configurations meaning that ViewModels will NOT work unless you specify the schema id that is going to be used.
In other words view model will only work if you add a schema like this:
viewModel: {
schema: "youruniqueschemaid",
data: {
title: record ? 'Edit: ' + record.get('title') : 'Add New Film',
},
links: {
currentFilm: record || {
//type: 'Film',
type: 'myapp.model.film.Film',
create: true
}
}
}
Yes the type attribute inside the links could not be more misleading!
You can also use the, shorter version, type: "Film" if you have set the entityName attribute inside the model as Film.
Refactor now
What Sencha should have done instead is force all developers to set the schema explicitly inside a ViewModel and use null if the model is not setup using a schema.
Of course as you can understand solving such an issue could not be done by diving into documentation nor diving inside the source code but rather using a wild guess of what kind of crazy conventions have been used.
In general the framework should be more explicit.
I´m pretty new to ember development and need help in handling this kind of task:
Currently I am working with Fixtures in an ember-cli app.
The two models concerned are:
var Recipe = DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string'),
ingredients: DS.hasMany('ingredients',{async: true}),
recipeCategory: DS.belongsTo('recipeCategory', {async: true})
});
var Ingredient = DS.Model.extend({
title: DS.attr('string'),
portion: DS.attr('string'),
groupTag: DS.attr('string'),
recipe: DS.belongsTo('recipe')
});
While there are no problems in listing all ingredients - also sorted - for a specific recipe called via nested routes,
this.resource('recipes',function(){
this.resource('recipe', {path: '/:recipe_id'});
});
I am encountering big problems while grouping ingredients by groupTag. The logic for grouping is not the problem, but I either run into race conditions accessing the models in controller for computed properties or getting framework errors when trying to handle promises in templates.
Here are the concerned templates:
//recipe.hbs
<strong>{{recipeCategory.title}}</strong>
<h3>{{title}}</h3>
{{render 'ingredients' ingredients}}
//ingredients.hbs
<strong>Zutaten</strong>
<ul>
{{#each groupedIngredients}}
<li>{{group}}
<ul>
{{#each items}}
<li>{{portion}} {{title}}</li>
{{/each}}
</ul>
</li>
{{/each}}
</ul>
My Ingredients-Controller looks like this:
var IngredientsController = Ember.ArrayController.extend({
sortProperties: ['title'],
sortedIngredients: Ember.computed.sort('model', 'sortProperties'),
groupedIngredients: function(){
return this.get('model').then(function(ingredients){
var groupTags = ingredients.mapBy('groupTag').uniq();
var groupedIngredients = groupTags.map(function(gtag){
return {
group: gtag,
items: ingredients.map(function(item){
if ( item.get('groupTag') == gtag){
return item;
}
}).compact()
};
});
console.log(groupedIngredients);
return groupedIngredients;
});
}.property('model')
});
The console log inside the promise is fine, but I can not return the promise for evaluation to the template:
Uncaught Error: Assertion Failed: The value that #each loops over must be an Array. You passed {_id: 158, _label: undefined, _state: undefined, _result: undefined, _subscribers: }
When I remove the promise and just work on this.get('model'), the computed array is full of undefined values, cause the model seems not to be fully loaded.
How can I fix this issue to work on async model data in this way?
Thanks!
You don't need to do your computation in a then hanging off of get('model'). By the time you have reached this point in your code, the model is already resolved and ready to go. The router has already ensured that the model promise is resolved before proceeding.
Therefore:
groupedIngredients: function(){
var ingredients = this.get('model');
var groupTags = ingredients.mapBy('groupTag').uniq();
var groupedIngredients = groupTags.map(function(gtag){
return {
group: gtag,
items: ingredients.map(function(item){
if ( item.get('groupTag') == gtag){
return item;
}
}).compact()
};
});
console.log(groupedIngredients);
return groupedIngredients;
}.property('#each.groupTag')
To avoid having to do compact, just switch to using filter:
items: ingredients.filter(function(item){
return item.get('groupTag') === gtag;
}
which is the same as
items: ingredients.filterBy('groupTag', gtag)
Here's an implementation of groupBy as a computed property, which you might be able to adapt, and if it works would let you simply do
groupedIngredients: Ember.computed.groupBy('groupTag')
I had similar issues with my code and it usually dealt with setting the wrong dependency for the computed property.
Based on your code I would say your groupedIngredients: property should probably be along the lines of:
.property('#each.groupTag')
Once set correctly, you should be able to remove the promises from your controller, since it should automatically update once the promise is fulfilled.
I'm working on a Backbone app, but everything I've read so far is either about displaying a list of items (a TODO list for example) or a single item.
Right now I have users, each user has a list of skills (pretty much like any game). I can easily get all users or a single user, the same for the skills but what if I want to get all the skills for a given user? How would I do that?
I thought about just adding a property to the users with a new instance of a collection, something like this:
var Users = Backbone.Model.extend({
skills: new Skills({ user: this })
});
var Skills = Backbone.Collection.extend({
model: Skill,
url: '/someUrl',
initialize: function (options) {
// fetch all skills from an user
this.fetch({ data: { user: options.user.get('id') } });
}
});
But I don't have much experience with Backbone and I don't really like the idea of that, also the request would look something like /someUrl?user=1 which I'd rather avoid, /someUrl/user/1 would be much better.
I've also noticed BackboneRelational but I haven't really tried it, it seems a bit of an overkill for my problem, but maybe I'm wrong.
What approach should I take to fetch all of my users skills? Thanks in advance.
I highly recommend you to checkout this post, sure you will find an answer. If short you may have following approach without any additional plugins and build nested model :
expect following json:
{
name: 'Gorbachov',
age: '75',
skills: [
{
name: 'kalashnikov'
},
{
name: 'vodka'
},
{
name: 'balalaika'
}
]
}
lets update User model:
User = Backbone.Model.extend({
initialize: function(){
var skills = this.get("skills");
if (skills){
this.skills = new Skills(skills);
this.unset("skills");
}
}
})
Then create SkillsCollection:
SkillsCollection = Backbone.Collection.extend({
model: Skill
})
and Skill model:
Skill = Backbone.Model.extend({
defaults: {
name: 'unnnamed'
}
})
I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Link to the problematic page: http://www.kangarooelectronics.com/fakeDB/#/tables/edit/2
My issue is that when I go to remove a column I get:
Object # has no method 'deleteRecord'
As I understand this is due to the object I'm iterating through having no references to the controller because of the way I am constructing the array that I use to create my list.
Removing tables works fine, which are listed in the following fashion:
{{#each model itemController='TableList'}}
<a {{action removeTable this}}>Delete</a>
{{/each}}
I'm iterating through the columns via:
{{#each column in currentColumns itemController='TablesEdit'}}
<a {{action removeColumn column}}>Drop</a>
{{/each}}
Snippet from FIXTURES object:
FakeDB.Table.FIXTURES = [
{
id: 1,
name: 'Users',
columns: {
1:{'colId':1, 'name':'name'},
2:{'colId':2, 'name':'favorite color'},
3:{'colId':3, 'name':'phone number'}
},
// ...snip... //
I am getting 'currentColumns' via:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: DS.attr('object'),
rows: DS.attr('object'),
currentColumns: function() {
var newColumns = $.map(this.get('columns'), function(k, v) {
return [k];
});
return newColumns;
}.property('columns'),
// ..snip.. //
Here you can see my problem... it's obvious that my 'column' isn't going to have any methods from my controller. I tried something like this:
FakeDB.Adapter = DS.FixtureAdapter.extend();
FakeDB.Adapter.map('FakeDB.Table', {
columns: {embedded: 'load'},
rows: {embedded: 'load'}
});
FakeDB.Columns = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
FakeDB.Rows = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
But I couldn't get {{#each column in columns}} to work with that.
Any suggestions? I'm going to read the docs again and will post back if I find a solution.
Thanks!
EDIT:
So I think I found another solution, but I'm still running into a little issue.
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
});
Still not sure what to replace 'item.tableID == 1' with so that I get items with the tableID referencing to the current page...
Columns are structured as...
FakeDB.Columns.FIXTURES = [
{
id: 1,
tableID: 1,
name: 'name'
},
// ...snip... //
But now I get:
assertion failed: Your application does not have a 'Store' property defined. Attempts to call 'find' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'
I am in fact defining a 'Store' property...
I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Most databases do contain a list of tables, rows and columns. Most web applications contain a fixed set of tables with pre-defined columns and a dynamic list of rows. If this is your first ember app i would recommend starting with something that keeps you on the happy path.
I am in fact defining a 'Store' property...
True but ember is complaining because store is not available before ember app is initialized. Anything that accesses the store should be in a framework hook of some kind. It can't be used when defining your objects, which wouldn't make a lot of sense anyway.
Probably what you meant to do was make a computed property called columns like this:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: function() {
FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
}.property('')
});
I can't get embedded hasMany to work correctly with ember data.
I have something like this
App.Post = DS.Model.extend({
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.extend({
post: DS.hasMany('App.Post'),
name: attr('string')
});
And my API returns the following for GET /post:
[
{
id: 1
comments: [{name: 'test'}, {name: 'test2'}]
},
...
]
I need to send this with POST /post:
[
{
comments: [{name: 'test'}, {name: 'test2'}]
},
...
]
I want to work with Ember models and have them make the appropriate requests:
var post = App.store.createRecord(App.Post, hash_post_without_comments);
post.get('comments').createRecord(hash_comment);
App.store.commit(); // This should call the POST api
and
var posts = App.store.find(App.Post); // This should call the GET api
When I try something like post: DS.hasMany('App.Post', {embedded: true}), the GET is working but the POST is trying to make a POST for the two records not only the parent one.
EDIT : My Real use case
1- I've just built ember data from master
2- My adapter: RESTAdapter
3- The serializer: JSONSerializer
4- I added
App.MyAdapter.map('App.Join', {
columns: { embedded: 'always' }
});
5- My Models are:
App.Join = DS.Model.extend({
rowCount: DS.attr('number'),
columns: DS.hasMany('App.JoinColumn'),
});
App.JoinColumn = DS.Model.extend({
join: DS.belongsTo('App.Join')
});
6- When:
var a = App.Join.find(1);
a.get('columns').createRecord({});
App.store.commit();
a POST for joincolumn is sent and the parent is not dirty
What am i missing?
On master, the correct API is:
App.Adapter.map('App.Post', {
comments: { embedded: 'always' }
});
The two possible values of embedded are:
load: The child records are embedded when loading, but should be saved as standalone records. In order for this to work, the child records must have an ID.
always: The child records are embedded when loading, and are saved embedded in the same record. This, of course, affects the dirtiness of the records (if the child record changes, the adapter will mark the parent record as dirty).
If you don't have a custom adapter, you can call map directly on DS.RESTAdapter:
DS.RESTAdapter.map('App.Post', {
comments: { embedded: 'always' }
});
I have the exact same problem.
This bug has been reported on the ember data issue tracker.
The following PR adds 2 failing tests showing the problem: https://github.com/emberjs/data/pull/578
It seems that there is no workaround right now.
EDIT:
sebastianseilund opened a PR 2 days ago which fixes your problem.
Have a look at: https://github.com/emberjs/data/pull/629/files
Adding an update to this incase others come across this post and are having a hard time figuring out what works with the current version of ember-data.
As of Ember Data 1.0.0.beta.7, you need to override the appropriate methods on the serializer. Here's an example:
1) Reopen the serializer (credit to this post):
DS.RESTSerializer.reopen({
serializeHasMany: function(record, json, relationship) {
var hasManyRecords, key;
key = relationship.key;
hasManyRecords = Ember.get(record, key);
if (hasManyRecords && relationship.options.embedded === "always") {
json[key] = [];
hasManyRecords.forEach(function(item, index) {
// use includeId: true if you want the id of each model on the hasMany relationship
json[key].push(item.serialize({ includeId: true }));
});
} else {
this._super(record, json, relationship);
}
}
});
2) Add the embedded: 'always' option to the relationship on the model:
App.Post = DS.Model.extend({
comments: DS.hasMany('comment', {
embedded: 'always'
})
});
This is what worked for me (Ember 1.5.1+pre.5349ffcb, Ember Data 1.0.0-beta.7.f87cba88):
App.Post = DS.Model.extend({
comments: DS.hasMany('comment', { embedded: 'always' })
});
App.PostSerializer = DS.ActiveModelSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs: {
comments: { embedded: 'always' }
}
});