I have a route called tickets which has it's model setup like so
model() {
return Ember.RSVP.hash({
event: null,
tickets: null
});
},
actions: {
didTransition(){
if(!this.controller.get('model.length')){
new Ember.RSVP.hash({
event: this.modelFor('events.event'),
tickets: this.store.query('ticket',{user_id:this.get('user.user.user_id'),event_code:this.modelFor('events.event').get('event_code')})
}).then((hash)=>{
if(!hash.tickets.get('length')){
this.controller.set('noTickets',true);
}
this.controller.set('model',hash);
});
}
}
}
The template manages to loop over these model.tickets just fine in a {{#each}} block
In my controller I'm attempting to setup a groupBy computed, but in my computed is where I get the error
ticketsByPurchase: Ember.computed('model.tickets.[].ticket_purchase_code',function(){
let tickets = this.get('model.tickets');
tickets.forEach(function(ticket){
console.log(ticket);
});
})
Try guarding against the model.tickets being iterated over, with something like this in the computed property:
if(!tickets){
return Ember.A()
}else{
//your forEach code here
}
or this in your route:
}).then((hash)=>{
if(!hash.tickets.get('length')){
this.controller.set('noTickets',true);
hash.tickets = Ember.Array([])
this.controller.set('model',hash);
}else{
this.controller.set('model',hash);
}
});
this.controller.get('model.length') is always null because model is a hash, not an array. Also didTransition is not an action, it's a regular function, so it probably will not fire if defined in the actions hash.
I would recommend removing the call to didTransition and do all the logic in your model hook like so:
model() {
let event = this.modelFor('events.event');
let event_code = event.get('event_code');
// this probably does not work, you have to get the user
// from another route or a service.
// this might work: this.controllerFor('user').get('user.user_id');
let user_id = this.get('user.user.user_id');
return Ember.RSVP.hash({
event,
tickets: this.store.query('ticket', { user_id, event_code })
});
},
setupController(controller, model) {
// super sets controller model to be route model.
// ie. controller.set('model', model);
this._super(...arguments);
if (!model.get('tickets.length')) {
controller.set('noTickets', true);
}
}
Related
Angular2 doesn't trigger the ChangeDetection after a click event. The code snippets below are to get the data from one component to another.
onClickEvent
(click)="$event.preventDefault(); setApartmentObject(FlatObject)";
ApartmentOverviewComponent
constructor(private _apart:ApartmentService) {}
setApartmentObject(flat:ApartmentObject) {
this._apart.setApartmentDetails(flat);
}
ApartmentService
Injectable()
export class ApartmentService {
apartmentDetails:ApartmentObject
getApartmentDetails():Observable<ApartmentObject> {
return Observable.create((observer) => {
observer.next(this.apartmentDetails);
observer.complete();
});
}
setApartmentDetails(value:ApartmentObject) {
this.apartmentDetails = value;
}
}
ApartmentDetailComponent
constructor(private _apart:ApartmentService)
get apartmentDetails() {
this._apart.getApartmentDetails().subscribe(data => {
this._apartmentDetails = data;
});
return this._apartmentDetails;
}
In the HTML file
<p><strong>{{apartmentDetails.name || 'Musterwohnung'}}</strong></p>
I also tried to fix this problem with an eventemitter, but without success. Only the following dirty fix works:
constructor(private _ref:ChangeDetectorRef) {
this._ref.detach();
setInterval(() => {
this._ref.detectChanges();
}, 300);
}
There are some issues with your code that actually prevent the value from being read.
First of all—in your service—when you set the value, you just do it on the service's instance, instead of feeding it to the observable object. The observable just can't know that value has changed, so it won't emit the change (next) event. This is why the ApartmentOverviewComponent. setApartmentObject() does nothing. To actually feed the observable with data, you need to use a Subject.
In the ApartmentDetailComponent, in this simple scenario (where data is always synchronously provided), you could get the value in the way you try it. But, as mentioned before, the data won't ever change. It's also needles to store the data on the component's instance's _apartmentDetails field. You could use the observable in your template.
The working implementation is like that:
#Injectable()
class ApartmentService {
// BehaviorSubject is a type of an Observable that can be manually fed with data
// and returns it's last value to any subscriber.
apartmentDetails = new BehaviorSubject<ApartmentObject>({name: 'Musterwohnung'});
// Instead of using a property of the service, just inform the
// subject about knew data and let it spread the change for you.
setApartmentDetails(value: ApartmentObject) {
this.apartmentDetails.next(value);
}
}
#Component({
selector: 'overview-cmp',
// Side note: you don't need to .preventDefault() here.
template: `<a (click)="setApartmentObject({name: 'Shiny Aparament'})">click</a>`
})
class ApartmentOverviewComponent {
constructor(private apartService: ApartmentService) {}
// Works same as before.
setApartmentObject(flat: ApartmentObject) {
this.apartService.setApartmentDetails(flat);
}
}
#Component({
selector: 'details-cmp',
// Use the 'async' pipe to access the data stored in an Observable
// object. Also, to secure the code, use '?' to safely access the property.
template: `<p><strong>{{(details | async)?.name}}</strong></p>`
})
class Apartament {
// This is the observable with data.
details: Observable<ApartmentObject>;
constructor(private apartService: ApartmentService) {}
// When component initialises, assign the observable data from the service
ngOnInit() {
this.details = this.apartService.apartmentDetails;
}
}
I'm new with backbone and faced the following problems. I'm trying to emulate some sort of "has many relation". To achieve this I'm adding following code to initialize method in the model:
defaults: {
name: '',
tags: []
},
initialize: function() {
var tags = new TagsCollection(this.get('tags'));
tags.url = this.url() + "/tags";
return this.set('tags', tags, {
silent: true
});
}
This code works great if I fetch models through collection. As I understand, first collection gets the data and after that this collection populates models with this data. But when I try to load single model I get my property being overridden with plain Javascript array.
m = new ExampleModel({id: 15})
m.fetch() // property tags get overridden after load
and response:
{
name: 'test',
tags: [
{name: 'tag1'},
{name: 'tag2'}
]
}
Anyone know how to fix this?
One more question. Is there a way to check if model is loaded or not. Yes, I know that we can add callback to the fetch method, but what about something like this model.isLoaded or model.isPending?
Thanks!
"when I try to load single model I get my property being overridden with plain Javascript array"
You can override the Model#parse method to keep your collection getting overwritten:
parse: function(attrs) {
//reset the collection property with the new
//tags you received from the server
var collection = this.get('tags');
collection.reset(attrs.tags);
//replace the raw array with the collection
attrs.tags = collection;
return attrs;
}
"Is there a way to check if model is loaded or not?"
You could compare the model to its defaults. If the model is at its default state (save for its id), it's not loaded. If it doesn't, it's loaded:
isLoaded: function() {
var defaults = _.result(this, 'defaults');
var current = _.wíthout(this.toJSON(), 'id');
//you need to convert the tags to an array so its is comparable
//with the default array. This could also be done by overriding
//Model#toJSON
current.tags = current.tags.toJSON();
return _.isEqual(current, defaults);
}
Alternatively you can hook into the request, sync and error events to keep track of the model syncing state:
initialize: function() {
var self = this;
//pending when a request is started
this.on('request', function() {
self.isPending = true;
self.isLoaded = false;
});
//loaded when a request finishes
this.on('sync', function() {
self.isPending = false;
self.isLoaded = true;
});
//neither pending nor loaded when a request errors
this.on('error', function() {
self.isPending = false;
self.isLoaded = false;
});
}
In my Backbone app, I have the following
playlistView = new PlaylistView({ model: Playlist });
Playlist.getNewSongs(function() {
playlistView.initialize();
}, genre, numSongs);
Playlist.getNewSongs() is called back when some ajax request is finished. I want to re-initialize the view then. However, I believe the way I'm doing it leads to this problem of a view listening to a same event twice. Is calling initialize() like this acceptable? If not, what should I do instead?
Update:
I wrote this chrome extension in Backbone to learn Backbone, and it's in a design hell at the moment. I am in the middle of refactoring the entire codebase. The snippet below is my PlaylistView initialize() code block.
var PlaylistView = Backbone.View.extend({
el: '#expanded-container',
initialize: function() {
var playlistModel = this.model;
var bg = chrome.extension.getBackgroundPage();
if (!bg.player) {
console.log("aborting playlistView initialize because player isn't ready");
return;
}
this.listenTo(playlistModel.get('songs'), 'add', function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').prepend(songView.render().el);
});
this.$('#song-search-form-group').empty();
// Empty the current playlist and populate with newly loaded songs
this.$('.playlist-songs').empty();
var songs = playlistModel.get('songs').models;
// Add a search form
var userLocale = chrome.i18n.getMessage("##ui_locale");
var inputEl = '<input class="form-control flat" id="song-search-form" type="search" placeholder="John Lennon Imagine">' +
'<span class="search-heart-icon fa fa-heart"></span>'+
'<span class="search-input-icon fui-search"></span>';
}
this.$('#song-search-form-group').append(inputEl);
var form = this.$('input');
$(form).keypress(function (e) {
if (e.charCode == 13) {
var query = form.val();
playlistModel.lookUpAndAddSingleSong(query);
}
});
// Fetch song models from bg.Songs's localStorage
// Pass in reset option to prevent fetch() from calling "add" event
// for every Song stored in localStorage
if (playlistModel.get('musicChart').source == "myself") {
playlistModel.get('songs').fetch({ reset: true });
songs = playlistModel.get('songs').models;
}
// Create and render a song view for each song model in the collection
_.each(songs, function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').append(songView.render().el);
}, this);
// Highlight the currently played song
var currentSong = playlistModel.get('currentSong');
if (currentSong)
var currentVideoId = currentSong.get('videoId');
else {
var firstSong = playlistModel.get('songs').at(0);
if (!firstSong) {
// FIXME: this should be done via triggering event and by Popup model
$('.music-info').text(chrome.i18n.getMessage("try_different_chart"));
$('.music-info').fadeOut(2000);
//console.log("something wrong with the chart");
return;
}
var currentVideoId = firstSong.get('videoId');
}
_.find($('.list-group-item'), function (item) {
if (item.id == currentVideoId)
return $(item).addClass('active');
});
},
It is not wrong but probably not a good practice. You did not post the code in your initialize but maybe you have too much logic here.
If you are simply initializing the view again so that the new data is rendered, you should use event listener as such:
myView = Backbone. View.extend ({
initialize : function() {
// We bind the render method to the change event of the model.
//When the data of the model of the view changes, the method will be called.
this.model.bind( "change" , this.render, this);
// Other init code that you only need once goes here ...
this.template = _.template (templateLoader. get( 'config'));
},
// In the render method we update the view to represent the current model
render : function(eventName) {
$ (this.el ).html(this .template ((this.model .toJSON())));
return this;
}
});
If the logic in your initiialize is something totally else, please include it. Maybe there is a beter place for it.
What is the correct way to pass a view variable from the URL to a View Model to filter the result?
For example:
dataSource: new kendo.DataSource( {
transport: {
read: {
url: 'http://api.endpoint.com/resource',
}
parameterMap: function(options,type) {
if (type === 'read') {
return {
FormID: view.params.FormID
};
}
}
});
In the example above, there's a parameter in the URL called "FormID" and I would like to pass that value right to the parameterMap function. There is no "view" object, so I'm just putting that as an example.
I tried hooking into to the "data-show" and "data-init" functions to set this value to use, but the datasource fetches the data before these functions run.
Thanks
The configuration option options.transport.read can be a function, so you can compose the url there:
dataSource: new kendo.DataSource({
transport: {
read: function (options) {
// get the id from wherever it is stored (e.g. your list view)
var resourceId = getResourceId();
$.ajax({
url: 'http://api.endpoint.com/resource/' + resourceId,
dataType: "jsonp",
success: function (result) {
options.success(result);
},
error: function (result) {
options.error(result);
}
});
}
}
});
To connect this with your list view, you could use the listview's change event:
data-bind="source: pnrfeedsDataSource, events: { change: onListViewChange }"
then in viewModel.onListViewChange you could set the appropriate resource id for the item that was clicked on:
// the view model you bind the list view to
var viewModel = kendo.observable({
// ..., your other properties
onListViewChange: function (e) {
var element = e.sender.select(); // clicked list element
var uid = $(element).data("uid");
var dataItem = this.dataSource.getByUid(uid);
// assuming your data item in the data source has the id
// in dataItem.ResourceId
this._selectedResource = dataItem.ResourceId;
}
});
Then getResourceId() could get it from viewModel._selectedResource (or it could be a getter on the viewModel itself). I'm not sure how all of this is structured in your code, so it's difficult to give more advice; maybe you could add a link to jsfiddle for illustration.
You may use a "global" variable or a field in the viewmodel for that purpose. Something like
var vm = kendo.observable({
FormID: null,
dataSource: new kendo.DataSource( {
transport: {
read: {
url: 'http://api.endpoint.com/resource',
}
parameterMap: function(options,type) {
if (type === 'read') {
return {
FormID: vm.FormID
};
}
}
})
});
function viewShow(e) {
vm.set("FormID", e.view.params.FormID);
// at this point it is usually a good idea to invoke the datasource read() method.
vm.dataSource.read();
}
The datasource will fetch the data before the view show event if a widget is bound to it. You can work around this problem by setting the widget autoBind configuration option to false - all data-bound Kendo UI widgets support it.
This problem just seemed to appear while I updated to Backbone 1.1. I have a nested Backbone model:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""},
parse: function (response) {
response.name = response.set_id;
response.problems = new ProblemList(response.problems);
return response;
}
});
var ProblemList = Backbone.Collection.extend({
model: Problem
});
I initially load in a ProblemSetList, which is a collection of ProblemSet models in my page. Any changes to the open_date or due_date fields of any ProblemSet, first go to the server and update that property, then returns. This fires another change event on the ProblemSet.
It appears that all subsequent returns from the server fires another change event and the changed attribute is the "problems" attribute. This results in infinite recursive calls.
The problem appears to come from the part of set method of Backbone.Model (code listed here from line 339)
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
The comparison on the problems attribute returns false from _.isEqual() and therefore fires a change event.
My question is: is this the right way to do a nested Backbone model? I had something similar working in Backbone 1.1. Other thoughts about how to proceed to avoid this issue?
You reinstantiate your problems attribute each time your model.fetch completes, the objects are different and thus trigger a new cycle.
What I usually do to handle nested models:
use a model property outside of the attributes handled by Backbone,
instantiate it in the initialize function,
set or reset this object in the parent parse function and return a response omitting the set data
Something like this:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""
},
initialize: function (opts) {
var pbs = (opts && opts.problems) ? opts.problems : [];
this.problems = new ProblemList(pbs);
},
parse: function (response) {
response.name = response.set_id;
if (response.problems)
this.problems.set(response.problems);
return _.omit(response, 'problems');
}
});
parse gets called on fetch and save (according to backbone documentation), this might cause your infinite loop. I don't think that the parse function is the right place to create the new ProblemsList sub-collection, do it in the initialize function of your model instead.