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});
}
Related
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.)
I have a 'select' element in a UI component from which I need to retrieve the selected option (if any). As a beginner in both JavaScript and protractor, I am having trouble figuring out how to accomplish this without a bunch of nested promises:
I have two locators -- one for the selector's current selection and one for all the options:
selector = element(by.model("something.someId"));
this.selectorOptions = element.all(by.repeater("repeat in someOptions | orderBy:'name'"));
getSelectedOption = function () {
return this.selector.getText().then( function (selectionText) {
return this.selectorOptions.filter(function (option) {
option.getText().then(function (optionText) {
if(optionText === selectionText) {
option.getAttribute("value").then(function (value) {
// Some logic here which uses the value to return an pojo representing the selection
})
}
})
})
})
};
The above is just awful and I am sure this can be done better. I have looked at a lot of examples, but I haven't found one that involves dealing with nested promises which need to take parameters and then do something conditional based on the value, so I am having difficultly applying them to my situation, mostly because I don't really feel comfortable with asynchronous programming yet. How can I take the mess above and refactor it into something that isn't a nested callback hell?
Maybe playing a little bit with promises, protractor, arguments and bind you could get it quite cleaner.
Then you are using the protractor filter method which needs a boolean to be returned, in order to filter your values. But, from the way you used it, maybe
you were looking for each():
http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.each
I didn't have any chance to test the following code, so it may most probably not work :D
selector = element(by.model("something.someId"));
this.selectorOptions = element.all(by.repeater("repeat in someOptions | orderBy:'name'"));
getSelectedOption = function () {
return this.selector.getText().then(firstText.bind(this));
};
function firstText(text) {
return this.selectorOptions.filter(filterSelector.bind(this, text));
}
function filterSelector(text, option) {
return option.getText().then(optionText.bind(this, text, option));
}
function optionText(text, option, optionText) {
if(optionText === text) {
return option.getAttribute("value").then(someLogic);
}
}
function someLogic(value) {
console.log(value);
// value should be your value
// Some logic here which uses the value to return an pojo representing the selection
// return true or false, filter is still waiting for a boolean...
}
Another version just using arguments without function parameters. Specially follow the arguments which got printed, to see if the order is correct:
selector = element(by.model("something.someId"));
this.selectorOptions = element.all(by.repeater("repeat in someOptions | orderBy:'name'"));
getSelectedOption = function () {
return this.selector.getText().then(firstText.bind(this));
};
function firstText() {
console.log(arguments);
// arguments[0] should be your selectionText
return this.selectorOptions.filter(filterSelector.bind(this, arguments[0]));
}
function filterSelector() {
console.log(arguments);
// arguments[0] should be your previous selectionText
// arguments[1] should be your option
return arguments[1].getText().then(optionText.bind(this, arguments[0], arguments[1]));
}
function optionText() {
console.log(arguments);
// arguments[0] should be your optionText
// arguments[1] should be your selectionText
// arguments[2] should be your option
if(arguments[0] === arguments[1]) {
return arguments[2].getAttribute("value").then(someLogic);
}
}
function someLogic(value) {
console.log(value);
// value should be your value
// Some logic here which uses the value to return an pojo representing the selection
// return true or false, filter is still waiting for a boolean...
}
I'm itching head with concept of promises and async procedures. I have ordered a list and want to call a function with every item, wait until first procedure with the first item is done, proceed to second, third and so on. And only after every item is processed I want continue the main process.
Below is the code that made it well with the main process. So returning Q.all(promises) resulted that first all promises were processed and then main process continued. But problem was, that items (navigation keys) were processed async while I need them in sync:
function processPages(that) {
var navs = [];
Object.keys(that.navigation).map(function(key) {
navs.push({key: key, order: parseInt(that.navigation[key].index)});
});
var promises = navs.sort(function(a, b) {
return a.order - b.order;
})
.map(function(item) {
return that.parsePage(item.key).then(function(page) {
return page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that);
});
});
});
return Q.all(promises);
}
Below is the code when I modified that items are processed in sync and right order, but now main process will be out of sync:
function processPages(that) {
var navs = [];
Object.keys(that.navigation).map(function(key) {
navs.push({key: key, order: parseInt(that.navigation[key].index)});
});
var promises = navs.sort(function(a, b) {
return a.order - b.order;
})
.reduce(function(previous, item) {
return previous.then(function () {
return that.parsePage(item.key).then(function(page) {
return page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that);
});
});
});
}, Q());
return Q.all(promises);
}
Does anyone know what is happening here and how to use promises right way in this case?
Additional information
processPages is called from init hook. If promise (Q.all) is not used, then page hook may fire before init hook is totally processed, which I cannot allow either. This is what I refer with the "main process".
module.exports =
{
hooks: {
"init": function() {
var options = this.options.pluginsConfig['regexplace'] || {};
options.substitutes = options.substitutes || {};
// collects text replacement queries from plugin configuration
options.substitutes.forEach(function (option) {
patterns.push({re: new RegExp(option.pattern, option.flags || ''),
sub: option.substitute,
decode: option.decode || false,
store: option.store || null,
unreset: option.unreset || false});
});
this.config.book.options.variables = this.config.book.options.variables || {};
processPages(this);
},
"page": function(page) {
var that = this;
// process all normal sections in page
page.sections.filter(function(section) {
return section.type == 'normal';
})
.map(function(section) {
collectStore(section, page, that, true);
});
return page;
}
}
};
Code is part of the GitBook plugin code.
Take a look at the runnable examples (from Chrome's dev console) from the Understanding JavaScript Promise with Examples, especially the "chaining" example.
Based on your description of "...I have ordered a list and want to call a function with every item, wait until first procedure with the first item is done, proceed to second, third and so on. And only after every item is processed I want continue the main process.",
From algorithm point of view, you should be "chaining" multiple promises together:
create a Promise for every item. When an item is done, call resolve() so that then() will execute (next item in chain).
put the "main process" as the last item in the chain.
Recommend you test/learn promises' execution flow with simple example before applying it in your problem - makes it easier to understand.
Hope this helps! :-)
Solution was as simple as changing:
processPages(this);
to
return processPages(this);
on init hook.
I am creating a form and I am trying to find a simple, elegant way of handling to see if all inputs exist.
Form = Ember.Object.extend({
// section 1
name: null,
age: null,
isABoolean: null,
// section 2
job: null,
numberOfSiblings: null,
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('name')) && isPresent(this.get('age')) && isPresent(this.get('isABoolean'));
}.property('name', 'age', 'isABoolean'),
_isSection2Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('job')) && isPresent(this.get('numberOfSiblings'));
}.property('job', 'numberOfSiblings')
});
However, this doesn't seem to scale. My actual application will have many sections (over 20 sections).
I am looking into trying to create a re-usable computed property that fits my needs. Take for example the code of what I am going for:
Form = Ember.Object.extend({
// properties...
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: Ember.computed.allPresent('name', 'age', 'isABoolean'),
_isSection2Complete: Ember.computed.allPresent('job', 'numberOfSiblings')
});
I feel that this is a common case, but I'm failing to find the correct computed properties on how to execute this, so I would like to make my own.
Two questions:
Where's the best place to define the custom computed property? Can I just attach a function to Ember.computed?
Is there an easier way to solve this? I feel like I'm overlooking something simple.
As for Question #1,
You can define a custom computed helper in the App namespace. In this example, I created a new computed helper called allPresent that checks each property passed in against Ember.isPresent.
App.computed = {
allPresent: function (propertyNames) {
// copy the array
var computedArgs = propertyNames.slice(0);
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
It can be used like this, per your example code:
_isSection2Complete: App.computed.allPresent(['job', 'numberOfSiblings'])
I adapted this from the approach here: http://robots.thoughtbot.com/custom-ember-computed-properties
As for Question #2, I can't think of a simpler solution.
I had to make a minor adjustment to Evan's solution, but this works perfectly for anyone else that needs it:
App.computed = {
allPresent: function () {
var propertyNames = Array.prototype.slice.call(arguments, 0);
var computedArgs = propertyNames.slice(0); // copy the array
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
This can now be used as such:
_isSection2Complete: App.computed.allPresent('job', 'numberOfSiblings')
I'm trying to make the {{#each}} helper to iterate over an object, like in vanilla handlebars. Unfortunately if I use #each on an object, Ember.js version gives me this error:
Assertion failed: The value that #each loops over must be an Array. You passed [object Object]
I wrote this helper in attempt to remedy this:
Ember.Handlebars.helper('every', function (context, options) {
var oArray = [];
for (var k in context) {
oArray.push({
key : k,
value : context[k]
})
}
return Ember.Handlebars.helpers.each(oArray, options);
});
Now, when I attempt to use {{#every}}, I get the following error:
Assertion failed: registerBoundHelper-generated helpers do not support use with Handlebars blocks.
This seems like a basic feature, and I know I'm probably missing something obvious. Can anyone help?
Edit:
Here's a fiddle: http://jsfiddle.net/CbV8X/
Use {{each-in}} helper. You can use it like like {{each}} helper.
Example:
{{#each-in modelWhichIsObject as |key value|}}
`{{key}}`:`{{value}}`
{{/each-in}}
JS Bin demo.
After fiddling with it for a few hours, I came up with this hacky way:
Ember.Handlebars.registerHelper('every', function(context, options) {
var oArray = [], actualData = this.get(context);
for (var k in actualData) {
oArray.push({
key: k,
value: actualData[k]
})
}
this.set(context, oArray);
return Ember.Handlebars.helpers.each.apply(this,
Array.prototype.slice.call(arguments));
});
I don't know what repercussions this.set has, but this seems to work!
Here's a fiddle: http://jsfiddle.net/CbV8X/1/
I've been after similar functionality, and since we're sharing our hacky ways, here's my fiddle for the impatient: http://jsfiddle.net/L6axcob8/1/
This fiddle is based on the one provided by #lxe, with updates by #Kingpin2k, and then myself.
Ember: 1.9.1, Handlebars: 2.0.0, jQuery 2.1.3
Here we are adding a helper called every which can iterate over objects and arrays.
For example this model:
model: function() {
return {
properties: {
foo: 'bar',
zoo: 'zar'
}
};
}
can be iterated with the following handlebars template:
<ul class="properties">
{{#every p in properties}}
<li>{{p.key}} : {{p.value}}</li>
{{/every}}
</ul>
every helper works by creating an array from the objects keys, and then coordinating changes to Ember by way of an ArrayController. Yeah, hacky. This does however, let us add/remove properties to/from an object provided that object supports observation of the [] property.
In my use case I have an Ember.Object derived class which notifies [] when properties are added/removed. I'd recommend looking at Ember.Set for this functionality, although I see that Set been recently deprecated. As this is slightly out of this questions scope I'll leave it as an exercise for the reader. Here's a tip: setUnknownProperty
To be notified of property changes we wrap non-object values in what I've called a DataValueObserver which sets up (currently one way) bindings. These bindings provide a bridge between the values held by our internal ArrayController and the object we are observing.
When dealing with objects; we wrap those in ObjectProxy's so that we can introduce a 'key' member without the need to modify the object itself. Why yes, this does imply that you could use #every recursively. Another exercise for the reader ;-)
I'd recommend having your model be based around Ember.Object to be consistent with the rest of Ember, allowing you to manipulate your model via its get & set handlers. Alternatively, as demonstrated in the fiddle, you can use Em.Get/Em.set to access models, as long as you are consistent in doing so. If you touch your model directly (no get/set), then every won't be notified of your change.
Em.set(model.properties, 'foo', 'asdfsdf');
For completeness here's my every helper:
var DataValueObserver = Ember.Object.extend({
init: function() {
this._super();
// one way binding (for now)
Em.addObserver(this.parent, this.key, this, 'valueChanged');
},
value: function() {
return Em.get(this.parent, this.key);
}.property(),
valueChanged: function() {
this.notifyPropertyChange('value');
}
});
Handlebars.registerHelper("every", function() {
var args = [].slice.call(arguments);
var options = args.pop();
var context = (options.contexts && options.contexts[0]) || this;
Ember.assert("Must be in the form #every foo in bar ", 3 == args.length && args[1] === "in");
options.hash.keyword = args[0];
var property = args[2];
// if we're dealing with an array we can just forward onto the collection helper directly
var p = this.get(property);
if (Ember.Array.detect(p)) {
options.hash.dataSource = p;
return Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
}
// create an array that we will manage with content
var array = Em.ArrayController.create();
options.hash.dataSource = array;
Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
//
var update_array = function(result) {
if (!result) {
array.clear();
return;
}
// check for proxy object
var result = (result.isProxy && result.content) ? result.content : result;
var items = result;
var keys = Ember.keys(items).sort();
// iterate through sorted array, inserting & removing any mismatches
var i = 0;
for ( ; i < keys.length; ++i) {
var key = keys[i];
var value = items[key];
while (true) {
var old_obj = array.objectAt(i);
if (old_obj) {
Ember.assert("Assume that all objects in our array have a key", undefined !== old_obj.key);
var c = key.localeCompare(old_obj.key);
if (0 === c) break; // already exists
if (c < 0) {
array.removeAt(i); // remove as no longer exists
continue;
}
}
// insert
if (typeof value === 'object') {
// wrap object so we can give it a key
value = Ember.ObjectProxy.create({
content: value,
isProxy: true,
key: key
});
array.insertAt(i, value);
} else {
// wrap raw value so we can give it a key and observe when it changes
value = DataValueObserver.create({
parent: result,
key: key,
});
array.insertAt(i, value);
}
break;
}
}
// remove any trailing items
while (array.objectAt(i)) array.removeAt(i);
};
var should_display = function() {
return true;
};
// use bind helper to call update_array if the contents of property changes
var child_properties = ["[]"];
var preserve_context = true;
return Ember.Handlebars.bind.call(context, property, options, preserve_context, should_display, update_array, child_properties);
});
Inspired by:
How can I make Ember.js handlebars #each iterate over objects?
http://mozmonkey.com/2014/03/ember-getting-the-index-in-each-loops/
https://github.com/emberjs/ember.js/issues/4365
https://gist.github.com/strathmeyer/1371586
Here's that fiddle again if you missed it:
http://jsfiddle.net/L6axcob8/1/