Oracle JET: Passing asynchronous property to composite - javascript

I want to pass an array (or observableArray) to a composite like so:
<add-training-modal id="modal" employees="[[data]]"></add-training-modal>
data is retrieved within the associated viewModel via an asynchronous call:
self.data = ko.observableArray();
UserFactory.getUsers().then(arr => {
self.data(arr);
})
The problem: [[data]] is passed to the composite before the asynchronous call is finished. That would be fine as long as data in the composite is updated. However, this is not the case.
In the composite viewModel:
function model(context) {
//...
context.props.then(function (propertyMap) {
self.properties = propertyMap;
console.log(propertyMap.employees); // is empty []
setTimeout(function () {
console.log(propertyMap.employees); // is full [...]
}, 500);
});
}
model.prototype.attached = function(context) {
context.props.then(function (propertyMap) {
console.log(propertyMap.employees); // is empty []
});
};
I know that I could retrieve the data from within the composite. But that would not be the answer to the underlying problem.

In other words, the composite needs to listen for a change event, correct? Add this code:
self.composite = context.element;
$(self.composite).on('employees-changed', function (event) {
if (event.detail.updatedFrom === 'external') {
console.log(event.detail.value);
}
});
In the latest version (I haven't tested this for myself):
model.prototype.attached = function(context) {
self.composite = context.element;
self.composite.addEventListener('employeesChanged',function(event){
if (event.detail.updatedFrom === 'external'){
console.log(event.detail.value);
}
});
};
For lots more info, check out this blog: https://blogs.oracle.com/groundside/jet-composite-components-v-events
It's the definitive guide on Oracle-JET composites.

Ray's answer does the trick.
And there's also another solution. It requires JET v5.0.0+ which was released on April 16, 2018. It comes with a new composite lifecycle method which is called propertyChanged(context). You can use it like so:
model.prototype.propertyChanged = function(context){
if (context.property === 'employees') {
this.employees(context.value);
}
};
(Be aware of this issue when you upgrade to v5.0.0.)

Related

Jasmine test for javascript getter not working

I'm writing some test for for an angularjs factory and some of the expectations are not working and I really don't know why.
This is my factory (part of it).
'use strict';
angular.module('myAppMod')
.factory('Person', function(BaseModel) {
return BaseModel.extend({
get fullname() {
var name = [];
if (this.first_name) {
name.push(this.first_name);
}
if (this.person_extra && this.person_extra.middle_name) {
name.push(this.person_extra.middle_name);
}
if (this.last_name) {
name.push(this.last_name);
}
return name.join(' ');
}
});
});
and Jasmine tests:
var p;
beforeEach(function() {
p = new Person({
first_name: 'first_name',
person_extra: {
middle_name: 'middle_name',
media_item_id: null
},
last_name: 'last_name',
security_level: 'security_level'
}, true);
});
it("has a fullname", function() {
expect(p.fullname).toEqual('first_name middle_name last_name');
});
p.fullnameis returning ""(empty string) and in the factory, console.log(this.first_name), is undefined.
Any help is really appreciated.
Thank you in advance
EDIT: After further investigation, I have changed my answer.
It is not working because you are using the getter shorthand (get fnName() { }) through the extend method. The getter's this is the anonymous object itself and does not inherit the methods and properties of the Backbone model, whereas the this in function properties do. I have made a codepen that illustrate your problem.
That is, if this is your Model
var Model = BaseModel.extend({
get isBackboneModelThroughGetter() {
return !!this.get;
},
isBackboneModel: function() {
return !!this.get;
},
});
Then an instance of Model will make this test pass:
it('should make you wonder', function() {
var model = new Model();
expect(model.isBackboneModel()).toBe(true);
expect(model.isBackboneModelThroughGetter).not.toBe(true);
});
Thus, to make your Person factory work, you will need:
To replace every property access by the proper Backbone call: this.get('propertyName') instead of this.propertyName
Replace all getters by function properties: full_name : function() { /*...*/ } instead of get full_name() { /* ... */ }
Replace calls to model.full_name by model.full_name();
I assume that you're using the built-in angular.extend. angular.extend does not copy getters and setters. There's been an open issue on GitHub on this specific subject since the 12th of August 2014.
As for why it still isn't implemented:
Angular exposes some of the helper functions that it uses internally. This is the case for extend, copy and many others. There are other libraries that specialize in these functions, keep their
focus is there and can do a better job.
It is not in the best interest of most users to make these helper functions big nor slow, as these are used internally and any change in that direction can have a direct impact in download size and performance. At the same time, apps that need the most accurate version, should be better served with other libraries.
There are many ways to solve this issue. decaf.js provides an example implementation that should work for most cases. GitHub is probably a better environment to dive into their code, but it comes down to this:
function extend (me) {
var args = Array.prototype.slice.call(arguments, 1);
decaf.each(args, function (o) {
for (var key in o) {
if (o.hasOwnProperty(key)) {
var desc = Object.getOwnPropertyDescriptor(o, key);
var g = desc.get;
var s = desc.set;
if (g || s) {
Object.defineProperty(me, key, { get: g, set: s, enumerable: true });
} else {
me[key] = o[key];
}
}
}
});
return me;
}

Subscribe arrayChange on knockout computed accessor

I have a case where I need to listen for array changes of a computed that is simply returning a filtered value of an observable.
However, I do need to have the full list of changes, as .subscribe(function(changes){},null,'arrayChange') would do on an observableArray.
What I understand is that arrayChange does not work in the case of a computed value, because it probably remakes a new array and so there's no specific change to list.
See http://jsfiddle.net/darknessm0404/A6D8u/1/ for a complete example.
// The following does not work, but I'd like it
computedTest.subscribe(function(changesList){
console.log('COMPUTED subscription : arrayChange');
}, null, 'arrayChange');
The only way I seem to achieve what I want is to create another observable array which would have push/delete depending on the changes, so I would be able to get the 'arrayChange' method work.
Full example of my idea:
this.events.listFiltered = ko.observableArray().extend({ rateLimit: 0 });
this.events.listFiltered_Worker = ko.computed(function () {
var listFiltered = me.events.listFiltered();
ko.utils.arrayForEach(me.events.list(), function (item) {
index = listFiltered.indexOf(item);
if (FILTERING_CASE_HERE) {
if (index < 0) {
listFiltered.push(item);
}
} else if (index >= 0) { // Delete
listFiltered.splice(index, 1);
}
});
return ko.utils.arrayFilter(me.events.list(), function (item) {
return !(item.end().isBefore(filterStart) || item.start().isAfter(filterEnd));
});
return __rd++;
}).extend({ rateLimit: 0 });
this.events.listFiltered.subscribe(function () {
debug('inside subscribe');
debugger;
}, null, 'arrayChange');
However I was wondering if there's a easier solution to this problem?
Knockout supports arrayChange for any observable, which you have to enable specifically.
var computedTest = ko.computed(function() {
...
}).extend({trackArrayChanges: true});
http://jsfiddle.net/mbest/A6D8u/2/
If you look at the Knockout source code, this is what's done automatically for observable arrays.
If this a common scenario in your project you could create a wrapper function that does this:
function computedArray() {
return ko.computed.apply(ko, arguments).extend({trackArrayChanges: true});
}

Meteor.js Collection empty on Client

Why is it that myCollection.find().fetch() returns an empty array [] even though the call is made within if(data){...}? Doesn't the if statement ensure that the collection has been retrieved before executing the console.log()?
Template.chart.rendered = function() {
var data = myCollection.find().fetch();
if(data) {
console.log(data);
}
$('#chart').render();
}
This returns [] in the browser Javascript console.
You could use count() instead which returns the number of results. data itself would be an empty array, [] which isn't falsey ( [] == true ).
Also don't use fetch() unless you're going to use the raw data for it because its quite taxing. You can loop through it with .forEach if you need to.
var data = myCollection.find();
if(data.count())
console.log(data);
//If you need it for something/Not sure if this is right but just an example
$('#chart').render(data.fetch())
The problem is that you have to wait for data from the server. When you just use Template.name.rendered function it is immediately invoked. You have to use Template.name.helpers function to wait for data from the server. Everything is described in the documentation.
It seems when you "remove autopublish" you have to also subscribe on the client.
if(Meteor.isClient) {
Meteor.startup(function() {
Myvars = new Mongo.Collection("myvars");
Meteor.subscribe('myvars')
});
}
and enable allow and publish on the server
if(Meteor.isServer) {
Meteor.startup(function () {
Myvars = new Mongo.Collection("myvars");
Myvars.allow({
insert: function () {
return true;
},
update: function () {
return true;
},
remove: function () {
return true;
}
});
if (Myvars.find().count() == 0) {
Myvars.insert({myvalue:'annoyed'});
}
Meteor.publish("myvars", function() {
return Myvars.find();
});
});
}
I'm new to this as well. I was just looking to have a global value that all clients could share. Seems like a useful idea (from a beginner's perspective) and a complete oversight on the Meteor teams behalf, it was nowhere clearly documented in this way. I also still have no idea what allow fetch is, that too is completely unclear in the official documentation.
It does, but in javascript you have the following strange behaviour
if ([]){
console.log('Oops it goes inside the if')
} // and it will output this, nontheless it is counter-intuitive
This happens because JS engine casts Boolean([]) to true. You can how different types are casted to Boolean here.
Check if your array is not empty in the beginning.
a = [];
if (a.length){
//do your thing
}

Referencing a parent object in callback functions with jQuery

I've a page that is generated dynamically, and that includes certain number (user-dynamically-defined) of advanced scatter plot charts. I intend to create a JavaScript object which defines the scatter plot itself, i.e. which takes some parameters, some data, and some container ID, and which will create the various elements needed to obtain the visualisation: canvas elements, toolbar, etc.. To do so, I started with the following (simplified) class:
(function () {
if (!this.namespace) { this.namespace = {};}
this._instances = { index: 0 };
this.namespace.ScatterPlot = function (containerId, file, options) {
_instances.index ++;
this.id = this.containerId+"-"+_instances.index ;
this.containerId = containerId ;
_instances [this.id] = this;
// ... Do stuffs with file and options ...
// Initialize elements once the DOM is ready
$(this.updateDOM);
}
namespace.ScatterPlot.prototype = {
updateDOM: function() {
$("<canvas>")
.click(clickCallback)
.appendTo("#"+this.containerId);
//(...)
},
clickCallback: function() {
alert("Some click: "+this.id);
}
}
})();
Each object can be created with:
var v1 = new namespace.ScatterPlot("container1", "foo", "foo");
var v2 = new namespace.ScatterPlot("container2", "foo", "foo");
There are two problems here: (1) in updateDOM, 'this' does not make reference to my initial ScatterPlot object, which means that this example will never work, and (2) similarly, the clickCallback will not be able reference the scatterplot with 'this' either.
I'm new to javascript, and I'm still struggeling to understand the logic of OO programming in javascript, so the question is: I'm I taking the wrong direction here ? After some digging, I could roughly achieve what I wanted by passing this to updateDOM:
$(this.updateDOM(this)); // This blows my eyes but does the trick, at least partially
updateDOM: function(that) {
$("<canvas>")
.click(that.clickCallback)
.appendTo("#"+that.containerId);
//(...)
},
clickCallback: function() {
// Not working either... Should pass 'that' to the function too
alert("Some click: "+this.id);
}
But I don't feel this patters to be very elegant... And the problem is not fixed either regarding the click callback.
Thoughts ?
Have a look at MDN's introduction to the this keyword.
The standard ways of dealing with that issue are using a that variable - not as an argument, but in a separate function:
var that = this;
$(function() {
that.updateDOM();
});
// or
$(this.getClickCallback());
...
namespace.ScatterPlot.prototype.getClickCallback = function() {
var that = this;
return function clickCallback(e) {
alert("Some click: "+that.id);
};
};
Alternatively, you can always use .bind() (or $.proxy for older browsers) which do quite what the second example does in a more generic way:
$(this.clickCallback.bind(this));

Passing collection.fetch as a named function to collection.bind does not work

I have two Backbone collections. I want to bind to the reset event one one. When that event is fired, I want to call fetch on the second collection, like so:
App.collections.movies.bind("reset", App.collections.theaters.fetch);
The second fetch never fires though. However, if I pass an anonymous function that calls theaters.fetch, it works no problem:
App.collections.movies.bind("reset", function () { App.collections.theaters.fetch(); });
Any idea why this might be the case?
Heres my full code. I'm not showing any of the models or collections, because it's a lot of code, but let me know if you think that might be the source of the problem:
var App = {
init: function () {
App.collections.theaters = new App.Theaters();
App.collections.movies = new App.Movies();
App.events.bind();
App.events.fetch();
},
events: {
bind: function () {
App.collections.theaters.bind("reset", App.theaterManager.assign);
App.collections.movies.bind("reset", function () { App.collections.theaters.fetch(); });
},
fetch: function () {
App.collections.movies.fetch();
}
},
collections: {},
views: {},
theaterManager: {
// Provide each model that requires theaters with the right data
assign: function () {
// Get all theaters associated with each theater
App.theaterManager.addToCollection("theaters");
// Get all theaters associated with each movie
App.theaterManager.addToCollection("movies");
},
// Add theaters to a collection
addToCollection: function (collection) {
App.collections[collection].each(function (item) {
item.theaters = App.theaterManager.getTheaters(item.get(("theaters")));
});
},
// Returns a collection of Theaters models based on a list of ids
getTheaters: function () {
var args;
if (!arguments) {
return [];
}
if (_.isArray(arguments[0])) {
args = arguments[0];
} else {
args = Array.prototype.slice.call(arguments);
}
return new App.Theaters(_.map(args, function (id) {
return App.collections.theaters.get(id);
}));
}
}
};
$(function () {
App.init();
});
This all has to do with function context. It is a common confusion with the way functions are called in Javascript.
In your first way, you are handing a function to be called, but there is no context defined. This means that whoever calls it will become "this". It is likely that the equivalent will be of calling App.collections.movies.fetch() which is not what you want. At least, I am guessing that is what the context will be. It is difficult to know for sure... it might be jQuery, it might be Backbone.sync. The only way to tell is by putting a breakpoint in the Backbone.collections.fetch function and print out the this variable. Whatever the case, it won't be what you want it to be.
In the second case, you hand it a function again but internally, you specify the context in which the function is called. In this case, fetch gets called with App.collections.theaters as the context.
... was that clear?

Categories

Resources