I have a custom component that expects data and not a promise, but I am unsure if they way that I am obtaining the data is the right way.
Is this the right way to do it?
component hbs
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
Doesn't work
controller (this is the way I expect things to work
import Ember from 'ember';
export default Ember.Controller.extend({
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data);
});
}.on('init'),
components/x-dropdown.js
import Ember from 'ember';
export default Ember.Component.extend({
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item[labelPath],
value: item[valuePath],
};
});
}.property('content'),
This works
controller
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data.get('content')); // pass the content instead of just the data
});
}.on('init'),
component
...
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item._data[labelPath], // access through the _data attribute
value: item._data[valuePath],
};
});
}.property('content'),
Ember Data returns a Proxy Promise. This means you can use the promise as if it were a collection or model itself, as long as you aren't dependent on the property being completely populated when you use it. If you really want the promise resolved, you should probably be setting it up in the route.
If you want it on your controller, you can be lazy and do it like so:
Controller
salutations: function() {
this.store.find('salutation');
}.property(),
Component
...
list: function() {
var content = this.get('content'),
valuePath = this.get('valuePath'),
labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item.get(labelPath),
value: item.get(valuePath),
};
});
}.property('content.[]'),
Template
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
The real trick is to watch if the collection is changing. Hence you'll see I changed the property argument to content.[]
Related
I have a use-case where I want to register routes dynamically in an initializer.
Because the application is a self-defining app I don't know the routes at development time.
Currently I created an instance-initializer:
import Ember from 'ember';
const myTempRouteList = ['home']; // this is retrieved from the backend
export function initialize(instance) {
let container = instance.container;
let router = container.lookup('router:main');
myTempRouteList.forEach(function (name) {
let routeName = name.dasherize();
router.map(function(){ // router.map is undefined here
this.resource(routeName, {path: routeName});
});
container.register(`route:${routeName}`, Ember.Route.extend({
}));
}, this);
}
export default {
name: 'register-routes',
initialize: initialize
};
The problem is that the router instance is present but is has no method map. In the documentation it is described as a public method. Some other methods I checked are present, f.i. hasRoute.
It turns out I had to call the lookupFactory method instead of the lookup method on the container.
export function initialize(instance) {
let container = instance.container;
let router = container.lookupFactory('router:main');
...
}
For people who are working on latest ember with ember-cli (Ember > 2.0). This might be helpful
//initializer.js
export function initialize(application) {
var routeNames = [];
var router = application.__container__.lookupFactory('router:main');
application.deferReadiness();
//if you want to have your custom routes on the highest level
if (routeNames.length > 0) {
router.map(function() {
var _this = this;
routeNames.forEach(function(item,index) {
_this.route(item);
});
});
}
//if you want to have your custom routes as a child of another parent route
if (routeNames.length > 0) {
router.map(function() {
this.route('parentRoute', {path:'/'}, function(){
var _this = this;
routeNames.forEach(function(item,index) {
_this.route(item);
});
});
});
}
application.advanceReadiness();
}
This code worked in ember 1.7.0:
var ViewTemplateHelper = Ember.Handlebars.makeBoundHelper(function(templateString, options) {
var dummy = Ember.View.extend({
classNames: ['view-template'],
template: Ember.Handlebars.compile(templateString)
});
var view = dummy.create();
if (options && options.hash) {
options.hash.content = template;
}
// Hack to prevent appendChild error
if (options.data.view._state === 'inDOM') {
options.data.view.rerender();
options.data.view.renderToBuffer();
}
return Ember.Handlebars.helpers.view.call(this, view, options); // undefined is not a function
});
export
default ViewTemplateHelper;
But now in ember 1.10.0 is gives the undefined is not a function error.
I tried to use Ember.Handlebars.helpers.view.helperFunction.call.
What do I miss?
The solution for this problem was not in a helper, but to use a component instead.
// components/content-element.js
import Ember from 'ember';
export
default Ember.Component.extend({
updateLayout: function() {
var store = this.container.lookup('store:main');
var projectId = this.get('project.id');
store.find('contentElement', {
key: this.get('key'),
project_id: projectId
}).then(function(contentElement) {
if (!Ember.isEmpty(contentElement.get('firstObject.value'))) {
var template = contentElement.get('firstObject.value');
var compiled = Ember.Handlebars.compile(template);
this.set('layout', compiled);
this.rerender();
}
}.bind(this));
}.observes('key').on('init')
});
We use the model contentElement for our templates. After setting the layout to the compiled Handlebars you have to run this.rerender();
For components you have to bind all to be used variables like this:
{{content-element key="name.of.element" project=project}}
In this case we use project in our dynamic template so we bound it. The key is used to get the right contentElement from the store.
I feel like this should be pretty straight-forward, but I'm unable to get the contents of a controller in a different view. Here is my code:
App.MapView = Ember.View.extend({
elementId: ['map-canvas'],
didInsertElement: function() {
var self = this;
var controller = this.get('controllers.markers');
}
});
If I console.log(controller) I get undefined.
In a controller I would do something like:
App.MarkersController = Ember.ArrayController.extend({
needs: ['map']
});
App.MapController = Ember.ObjectController.extend({
plot: function() {
var markers = this.get('controllers.markers');
}
});
You place the needs on the controller that needs another controller, and where you'll be accessing the other controller.
And from a view, in order to grab the controller you do this.get('controller') and the controllers object lives on the controller, so controller.controllers.markers
Additionally, the view is only created with the controller by default if ember creates it, if you are doing something like {{view App.MapView}} it isn't creating the MapController and associating it with it, it's using the controller that was in scope when you created the view.
App.MapView = Ember.View.extend({
elementId: ['map-canvas'],
didInsertElement: function() {
var self = this;
var controller = this.get('controller.controllers.markers');
}
});
App.MarkersController = Ember.ArrayController.extend({
});
App.MapController = Ember.ObjectController.extend({
needs: ['markers'],
plot: function() {
var markers = this.get('controllers.markers');
}
});
Check out this implementation of it:
http://emberjs.jsbin.com/ODuZibod/1/edit
I want to have a dynamic segment path in Ember without using the :id attribute
As per the Ember Guides, I'm using the serialize method to achieve this.
Heres my Router:
App.Router.map(function() {
this.resource("orders", function(){
this.resource('order', { path: ':order_sequence'}, function(){
this.route('edit');
})
});
});
And my Route:
var OrderRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('order', params.order_sequence)
},
serialize: function(model) {
return { order_sequence: model.get('sequence') };
}
});
module.exports = OrderRoute;
However, my URL's still behave using the id attribute in the path instead of the sequence attribute..
Any ideas?
Is your browser caching something, because that's correct. Are you passing in the id instead of the sequence/model in any of your transitionTo/transitionToRoute/link-to?
Oh, you aren't talking about the slug in the url, nor the route, you are talking about the id of your model. You need to create a serializer for that particular model and override the primary key
App.OrderSerializer = DS.RESTSerializer.extend({
primaryKey: 'sequence'
});
The Fixture Adapter has a constraint on defining the id, but you can lazily get around it by extending the fixture adapter and overriding a single method
App.OrderAdapter = DS.FixtureAdapter.extend({
fixturesForType: function(type) {
if (type.FIXTURES) {
var fixtures = Ember.A(type.FIXTURES);
return fixtures.map(function(fixture){
// aka we massasge the data a bit here so the fixture adapter won't whine so much
fixture.id = fixture.sequence;
var fixtureIdType = typeof fixture.id;
if(fixtureIdType !== "number" && fixtureIdType !== "string"){
throw new Error(fmt('the id property must be defined as a number or string for fixture %#', [fixture]));
}
fixture.id = fixture.id + '';
return fixture;
});
}
return null;
},
});
I have two models:
App.User = DS.Model.create({
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.create({
user: DS.belongsTo('App.User')
});
When a user is deleted, it also will delete all its comments on the backend, so I should delete them from the client-side identity map.
I'm listing all the comments on the system from another place, so after deleting a user it would just crash.
Is there any way to specify this kind of dependency on the association? Thanks!
I use a mixin when I want to implement this behaviour. My models are defined as follows:
App.Post = DS.Model.extend(App.DeletesDependentRelationships, {
dependentRelationships: ['comments'],
comments: DS.hasMany('App.Comment'),
author: DS.belongsTo('App.User')
});
App.User = DS.Model.extend();
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post')
});
The mixin itself:
App.DeletesDependentRelationships = Ember.Mixin.create({
// an array of relationship names to delete
dependentRelationships: null,
// set to 'delete' or 'unload' depending on whether or not you want
// to actually send the deletions to the server
deleteMethod: 'unload',
deleteRecord: function() {
var transaction = this.get('store').transaction();
transaction.add(this);
this.deleteDependentRelationships(transaction);
this._super();
},
deleteDependentRelationships: function(transaction) {
var self = this;
var klass = Ember.get(this.constructor.toString());
var fields = Ember.get(klass, 'fields');
this.get('dependentRelationships').forEach(function(name) {
var relationshipType = fields.get(name);
switch(relationshipType) {
case 'belongsTo': return self.deleteBelongsToRelationship(name, transaction);
case 'hasMany': return self.deleteHasManyRelationship(name, transaction);
}
});
},
deleteBelongsToRelationship: function(name, transaction) {
var record = this.get(name);
if (record) this.deleteOrUnloadRecord(record, transaction);
},
deleteHasManyRelationship: function(key, transaction) {
var self = this;
// deleting from a RecordArray doesn't play well with forEach,
// so convert to a normal array first
this.get(key).toArray().forEach(function(record) {
self.deleteOrUnloadRecord(record, transaction);
});
},
deleteOrUnloadRecord: function(record, transaction) {
var deleteMethod = this.get('deleteMethod');
if (deleteMethod === 'delete') {
transaction.add(record);
record.deleteRecord();
}
else if (deleteMethod === 'unload') {
var store = this.get('store');
store.unloadRecord(record);
}
}
});
Note that you can specify via deleteMethod whether or not you want to send the DELETE requests to your API. If your back-end is configured to delete dependent records automatically, then you will want to use the default.
Here's a jsfiddle that shows it in action.
A quick-and-dirty way would be to add the following to your user model
destroyRecord: ->
#get('comments').invoke('unloadRecord')
#_super()
I adapted the answer of #ahmacleod to work with ember-cli 2.13.1 and ember-data 2.13.0. I had an issue with nested relationships and the fact that after deleting an entity from the database its id was reused. This lead to conflicts with remnants in the ember-data model.
import Ember from 'ember';
export default Ember.Mixin.create({
dependentRelationships: null,
destroyRecord: function() {
this.deleteDependentRelationships();
return this._super()
.then(function (model) {
model.unloadRecord();
return model;
});
},
unloadRecord: function() {
this.deleteDependentRelationships();
this._super();
},
deleteDependentRelationships: function() {
var self = this;
var fields = Ember.get(this.constructor, 'fields');
this.get('dependentRelationships').forEach(function(name) {
self.deleteRelationship(name);
});
},
deleteRelationship (name) {
var self = this;
self.get(name).then(function (records) {
if (!records) {
return;
}
var reset = [];
if (!Ember.isArray(records)) {
records = [records];
reset = null;
}
records.forEach(function(record) {
if (record) {
record.unloadRecord();
}
});
self.set(name, reset);
});
},
});
Eventually, I had to set the relationship to [] (hasMany) or null (belongsTo). Else I would have run into the following error message:
Assertion Failed: You cannot update the id index of an InternalModel once set. Attempted to update <id>.
Maybe this is helpful for somebody else.