SAPUI5: Execute Code if Binding has Data - events

Element binding snippet
var oModel = oView.getModel();
var oPromiseMetadataLoaded = oModel.metadataLoaded();
oPromiseMetadataLoaded.then(function() {
var sObjectPath = oModel.createKey("Project", {
ProjectID: sProjectId
});
oView.bindElement("/" + sObjectPath);
// <HERE>
});
Now I want to execute a function (marked with '// ' where it should go) which uses data from the bound Object. When the data is not there yet (the model is obviously an OData model), I need to attach to the dataReceived event, but when when the data is already there, this event won't fire.
What is the most (UI5) idiomatic way to execute code in both cases? Is there a Promise like oModel.metadataLoaded()? Do I need to consider something, e.g. to probably not read data from an object previously bound to the view?

Maybe you can attach to the change-Event?
oView.bindElement({
path: "/" + sObjectPath,
events : {
change: this._onBindingChange.bind(this),
dataRequested: function (oEvent) {
oView.setBusy(true);
},
dataReceived: function (oEvent) {
oView.setBusy(false);
}
}
});
_onBindingChange : function (oEvent) {
if (this.getView().getBindingContext()) {
//HERE
}
else { //Invalid Binding Context };
}

Related

parse.com destroyAll not working

In the code following this description, I am trying to find and remove all these bad ListConfig objects that didn't have a group object set. It is correctly finding them, however it does not remove them. Is there something I am missing in the following code?
var Groups = [];
function queryForGroups(callback) {
var Group = Parse.Object.extend("Group");
var query = new Parse.Query(Group);
query.limit(1000);
query.find().then(function(result) {
Groups = result;
callback();
});
};
function removeConfigs(){
var Config = Parse.Object.extend("ListConfig");
var query = new Parse.Query(Config);
query.limit(10000);
query.notContainedIn("group", Groups);
query.find().then(function(configs){
return Parse.Object.destroyAll(configs, {useMasterKey:true});
});
}
function removeBadConfigs() {
queryForGroups(function() {
removeConfigs();
});
};
removeBadConfigs();
The code could be a little cleaner with respect to mixing promises, callbacks and an unnecessary global. Beyond that, it looks like it should work as long as your data model supports it. Specifically, your ListConfig object must have a "group" property, and it must have a Parse.Object value set for that property. The most common error I've seen is something like this:
var myGroup = // a parse object of type Group
myListConfig.set("group", myGroup.id); // WRONG
myListConfig.set("group", myGroup); // RIGHT
Assuming you've got that right, then it's mysterious why you're not seeing some deletes, but here's the code cleaned up with promises...
function queryForGroups() {
let query = new Parse.Query("Group")
query.limit(1000);
return query.find();
};
function removeConfigsWithGroups(groups){
let query = new Parse.Query("Config");
query.notContainedIn("group", groups);
return query.find().then(function(configs){
return Parse.Object.destroyAll(configs, {useMasterKey:true});
});
}
function removeBadConfigs() {
return queryForGroups(function(groups) {
return removeConfigsWithGroups(groups);
});
};
removeBadConfigs();
I figured it out. I removed "useMasterKey: true" because 1) it isn't needed for objects not with elevated privileges and 2) I was not running it in Cloud Code.

Angular Meteor objects not acting as expected

I am working with Angular Meteor and am having an issue with my objects/arrays. I have this code:
angular.module("learn").controller("CurriculumDetailController", ['$scope', '$stateParams', '$meteor',
function($scope, $stateParams, $meteor){
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
$scope.resources = _.map($scope.curriculum.resources, function(obj) {
return ResourceList.findOne({_id:obj._id})
});
console.log($scope.resources)
}]);
I am attempting to iterate over 'resources', which is a nested array in the curriculum object, look up each value in the 'ResourceList' collection, and return the new array in the scope.
Problem is, sometimes it works, sometimes it doesnt. When I load up the page and access it through a UI-router link. I get the array as expected. But if the page is refreshed, $scope.resources is an empty array.
My thought is there is something going on with asynchronous calls but have not been able for find a solution. I still have the autopublish package installed. Any help would be appreciated.
What you're going to do is return a cursor containing all the information you want, then you can work with $meteor.object on the client side if you like. Normally, publishComposite would look something like this: (I don't know what your curriculum.resources looks like)
Use this method if the curriculum.resources has only ONE id:
// this takes the place of the publish method
Meteor.publishComposite('curriculum', function(id) {
return {
find: function() {
// Here you are getting the CurriculumList based on the id, or whatever you want
return CurriculumList.find({_id: id});
},
children: [
{
find: function(curr) {
// (curr) will be each of the CurriculumList's found from the parent query
// Normally you would do something like this:
return ResourceList.find(_id: curr.resources[0]._id);
}
}
]
}
})
This method if you have multiple resources:
However, since it looks like your curriculum is going to have a resources list with one or many objects with id's then we need to build the query before returning anything. Try something like:
// well use a function so we can send in an _id
Meteor.publishComposite('curriculum', function(id){
// we'll build our query before returning it.
var query = {
find: function() {
return CurriculumList.find({_id: id});
}
};
// now we'll fetch the curriculum so we can access the resources list
var curr = CurriculumList.find({_id: id}).fetch();
// this will pluck the ids from the resources and place them into an array
var rList = _.pluck(curr.resources, '_id');
// here we'll iterate over the resource ids and place a "find" object into the query.children array.
query.children = [];
_.each(rList, function(id) {
var childObj = {
find: function() {
return ResourceList.find({_id: id});
}
};
query.children.push(childObj)
})
return query;
});
So what should happen here (I didn't test) is with one publish function you will be getting the Curriculum you want, plus all of it's resourceslist children.
Now you will have access to these on the client side.
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
// collection if more than one, object if only one.
$scope.resources = $meteor.collection(ResoursesList, false);
This was thrown together somewhat quickly so I apologize if it doesn't work straight off, any trouble I'll help you fix.

Should I use a Marionette CompositeView or Layout?

I'm using a marionette compositeview to display a collection, the code is below.
However, inside my collection I have 5 filters that are bound to events that then update the collection from the API.
The way I see it, theres two options, I would like opinions on which is better:
1) Use a layout view, somehow figure out how a compositeview can catch the filter views options and update the collection.
2) Use onRender to display the filter views and again catch the events in the compositeview
define(["marionette", "text!app/templates/posts/collection.html", "app/collections/posts", "app/views/posts/item"],
function(Marionette, Template, Collection, Item) {
"use strict"
return Backbone.Marionette.CompositeView.extend({
template: Template,
itemView: Item,
itemViewContainer: "tbody",
filter: {
from: 0,
to: 15,
publish_target: null,
status: null,
type: null,
publish_from_date: null, //publish_from_date=2014-01-07
publish_to_date: null, //publish_to_date=2014-01-07
publish_from_time: null, //publish_from_time=01%3A00%20AM
publish_to_time: null, //publish_to_time=12%3A30%20AM
location_id: null,
client_id: null
},
events: {
'change .filterBy': 'onClickFilter',
'change .filterByDate': 'onClickFilterDate'
},
collectionEvents: {
'sync': 'hideLoading'
},
initialize: function(options) {
//set loading, important we do this because we re-trigger the collection
this.setLoading();
// don't call a new collection unless its the init load, we lose collection automatically triggered events otherwise
if (_.isEmpty(options) || !_.has(options, 'newCollection')) {
this.collection = new Collection()
}
//strip any null key values from this.filter so the api doesnt filter crap
this.filter = _.cleanNullFieldsFromObject(this.filter);
//fetch the collection
return this.collection.fetch({data: this.filter})
},
// date was triggered, so get the details
onClickFilterDate: function() {
var publishFrom = new Date($('#publish_from_date').val());
var publishTo = new Date($('#publish_to_date').val());
this.filter.publish_from_date = _.dateToYMD(publishFrom);
this.filter.publish_to_date = _.dateToYMD(publishTo);
this.filter.publish_from_time = _.dateToHM(publishFrom);
this.filter.publish_to_time = _.dateToHM(publishTo);
// from time is greater than two time, then fetch the collection
if ( (publishFrom.getTime() / 1000) < (publishTo.getTime() / 1000) ) {
this.initialize({newCollection: true});
}
},
// a typical filter is clicked, so figure out whats happening
onClickFilter: function (ev) {
var type = $('#'+ev.currentTarget.id).data('type')
switch (type) {
case 'status':
this.filter.status = $('#filterStatus').val();
break;
case 'publish_target':
this.filter.publish_target = $('#filterPublishTarget').val();
break;
case 'type':
this.filter.type = $('#filterType').val();
break;
case 'client_id':
this.filter.client_id = $('#filterClientId').val();
break;
case 'location_id':
this.filter.location_id = $('#filterLocationId').val();
break;
}
this.initialize({newCollection: true});
},
hideLoading: function() {
this.$el.find('.loading-latch').removeClass('loading-active');
},
//set loading by appending to the latch
setLoading: function() {
this.$el.find('.loading-latch').addClass('loading-active');
}
})
})
define(["marionette", "text!app/templates/posts/item.html"],
function(Marionette, Template) {
"use strict"
return Backbone.Marionette.ItemView.extend({
template: Template,
tagName: "tr",
initialize: function () {
this.model.set('statusReadable', this.model.getStatus());
}
})
})
I actually went ahead and built both. I prefer the layout view.
Both have the concept of having a filter object that fetches and passes query params to the api.
Here are both solutions.
Using a layout that catches filter:changes vents to then update a filter object that gets passed into the collection view (I favour this because only the collection gets redrawn when a filter is changed)
http://pastebin.com/XNmQjs1i
Using a collection view that catches class events and redraws the view (I don't favour this because everything gets redrawn everytime the user changes a filter)
http://pastebin.com/WML2iiM4
I'm sure this might come in handy for marionette newbies, theres a lot of my learning poured into this. Enjoy.

AngularJS and "smart" Caching

I want to implement "smart" caching in my application. I want to always first return data from the cache (if none is available an empty object/array is returned), then always fetch the data from the server and replace the cached response with the updated server response. The objective is to always quickly show something to the user.
I want to do it in an "angular" fashion, i.e - adher to the promise paradigm.
I found a solution that uses the $resource service (http://www.bennadel.com/blog/2432-Applying-A-Cached-Response-To-An-AngularJS-Resource.htm), but $resource pretty much sucks if you don't use only the 4-5 default REST methods that it offers. Its custom method functionality is severly lacking. I'd really like to use the low level $http service, since it gives me better control over my requests, while keeping my controllers oblivious to the whole caching functionlity (i.e - avoid fetching data from the cache first in the controller itself and then querying the service).
Has anyone dealt with this problem and has a better solution?
Thanks :-)
http://jsfiddle.net/G23h7/
I created two services to accomplish what I think you're trying to accomplish. The first provides the core functionality to take in a promise and an object (or array) and updates said object or array when the promise resolves. There's a GUI for you to play around with it.
The second service integrates that into $http. Basically you can do smartHttp.forArray(config) and smartHttp.forObj(config) in lieu of $http(config). If you end up using this and want to use the $http shortcut methods then that should be straightforward to implement. This is untested - so consider it as pseudocode. If you're instantly returning a cached value/dud value it doesn't really make sense to use a promise for the return value of your smartHttp service (unless you were trying to make the service interchangeable with $http). If you'd like it to be a promise for that or whatever reason you can change:
var general = function (obj, methodName) {
// ...
return obj;
};
to the following:
var general = function (obj, methodName) {
// ...
return $q.when(obj);
};
And then ask for the $q service, of course. The real issue here is equality between requests - I assume $http does that nicely; I made a naive key - you may want to change that (as long as you have simple requests/same order for everything I don't think it should matter).
myApp.factory('smartCache', function () {
var service = {};
service.forArray = function (array, promise, clear) {
promise.then(function (promiseResult) {
if (clear) {
array.length = 0;
}
angular.forEach(promiseResult, function (promiseResultElement) {
array.push(promiseResultElement);
});
});
};
service.forObj = function (obj, promise, clear) {
promise.then(function (promiseResult) {
if (clear) {
for (var prop in obj) {
delete obj[prop];
}
}
for (var prop in promiseResult) {
obj[prop] = promiseResult[prop];
}
});
};
return service;
});
myApp.factory('smartHttp', function ($http, smartCache, $cacheFactory) {
var cache = $cacheFactory('smartHttp');
var service = {};
var general = function (config, methodName, initialValue) {
var obj;
var key = JSON.stringify([ config.url, config.method, config.params, config.data ]);
var cachedObj = cache.get(key);
if (cachedObj !== undefined) {
obj = cachedObj;
} else {
obj = initialValue;
}
var promise = $http(config);
var smartCachePromise = promise.then(function (result) {
return result.data;
});
smartCache[methodName](obj, smartCachePromise, true);
return obj;
};
service.forObj = function (config) {
return general(config, 'forObj', {});
}
service.forArray = function (config) {
return general(config, 'forArray', []);
}
return service;
});

Backbone.js + MVC3. Nested collection doesn't get populated

I have a backbone collection on the client.
Model of the collection has some properties along with another collection
When I do fetch() my action method on the server returns some data, collection gets populated, all the properties too, except that nested collection.
What could be the reason?
var Job = Backbone.Model.extend();
var Jobs = Backbone.Collection.extend({model: Job})
var Foo = Backbone.Model.extend({
initialize:function(){
this.jobs = new Jobs();
}})
var FooCollection = Backbone.Collection.extend({model: Foo})
var fooCol = new FooCollection()
fooCol.fetch();
fooCol.first().get('name') // => returns name
fooCol.first().jobs.toJSON() // returns nothing
// although this will
fooCol.first().get('jobs') //it will return an array
So somehow nested Backbone collection becomes just a regular property (Array)
OK - with your extra information, I can give you an answer.
First - "get" doesn't get a property off of the model. It gets a property off of the model's attributes property. So, the attributes probably look like:
{
name: 'blah',
jobs: [{name: 'job1'}, {name: 'job2'}]
}
Backbone doesn't automagically transform arrays into collections and models, and simply setting this.jobs isn't going to work. What you need to do is a little more complex.
var Foo = Backbone.Model.extend({
initialize:function(){
this.jobs = new Jobs(this.attributes.jobs));
}
});
This will set your 'jobs' property to a new jobs object with the data that was sent over for the jobs. But, alas, it won't automatically fire events on the Jobs collection, nor will it allow you to use helpers like this.get('jobs').each(fn); - you'll only be able to use it as Foo.jobs.each(fn).
In order for you to use the attribute as an actual collection, you'll have to do a lot more complicated things.
var Foo = Backbone.Model.extend({
initialize:function(){
this.createJobs(this.attributes.jobs);
},
toJSON: function () {
var json = Backbone.Model.prototype.toJSON.apply(this);
json.jobs = this.get('jobs').toJSON();
return json;
},
set: function (key, val) {
var attributes;
if(!_.isObject(key)) {
attributes = {}; attributes[key] = val;
} else {
attributes = key;
}
safeAttributes = _.omit(attributes, 'jobs');
Backbone.Model.prototype.set.call(this, safeAttributes);
if(attributes.jobs) { this.get('jobs').reset(attributes.jobs); }
},
clear: function () {
if(this.get('jobs') && this.get('jobs').destroy) {
this.get('jobs').off();
this.get('jobs').destroy();
}
Backbone.Model.prototype.clear.apply(this);
this.createJobs();
},
createJobs: function (jobsArray) {
var jobsCollection = new Jobs(jobsArray);
jobsCollection.on('change', function () {this.trigger('change'); }, this);
this.set('jobs', jobsCollection);
}
});
Note that this is completely untested, but hopefully it shows some of the way you'd do this.

Resources