How to splice ko.computeds - javascript

I got these two ko.computeds lists that I want to drag and drop among, but also moved with pushbuttons to remove an add to lists.
However I cannot make them work with both the dragndrop and the pushbutton functionality.
For the drag and drop I use Ryan Niemeyers excellent sortable library.
I guess I need to make my computeds writable, but this is where I get stuck and canĀ“t seem to get rid of the error "splice is not a function" for the computed.
Hence the sorting by drag and drops fails.
This is one of the computers:
this.availableTexts = ko.computed({
read: function(){
return ko.utils.arrayFilter(self.texts(), function(text) {
return text.sceneID() === null;
})
},
write: function(value){
return value;
},
owner: this
});
Full fiddle: http://jsfiddle.net/AsleG/yLvrp7zz/

Niemeyer's knockout-sortable library works with splice internally to sort an array of items (source). I believe it simply won't work on a computed, even if it returns an array and has a correct write method...
I'd suggest to use the visible binding to hide individual items. You'll have to expose projectID or map your items to include a computed like so:
var projectID = 1;
self.allScenes = ko.observableArray(scenes.map(function(scene) {
return Object.assign({}, scene, {
isVisible: ko.computed(function() {
return scene.projectID === projectID;
});
});
Alternatively, in viewmodel:
self.projectId = 1;
in HTML:
<li data-bind="visible: projectID === $parent.projectId"> ... </li>

See: https://stackoverflow.com/a/16464935/4024558
You can use observableArray.filter instead of computed in your case.
Suddenly I can't fork your fiddle. So you can replace your js with this http://pastebin.com/jjNQ39nJ and it will work.

Related

How do I sort the existing stream in rxjs?

I'm new to RxJs. I have a response stream which is getting data from ajax. Also, I have another button to sort by. I can sort without any problem. My question is if I do the sorting and updating properly? What I'm doing is essentially just empty the child nodes and append new result.
(function($, _) {
var fetchRepoButton = $('.fetch');
var sortByButton = $('.sort-by');
var fetchRepoClickStream = Rx.Observable.fromEvent(fetchRepoButton, 'click');
var sortByClickStream = Rx.Observable.fromEvent(sortByButton, 'click');
var requestStream = fetchRepoClickStream.map(function() {
return '/api';
});
var responseStream = requestStream.flatMap(function (requestUrl) {
return Rx.Observable.fromPromise($.getJSON(requestUrl));
});
responseStream.subscribe(function (es) {
var repositories = $('.container');
repositories.empty();
var names = es.map(function (e) {
return {name: e.name};
}).forEach(function (e) {
var rep = $('<div>');
rep.html(e.name);
repositories.append(rep);
});
});
var sortByStream = sortByClickStream.combineLatest(responseStream, function (click, es) {
return _.sortBy(es, function(e) {
return e.count;
}).reverse().map(function (e) {
return {name: e.name, count: e.count};
});
});
sortByStream.subscribe(function(es) {
var repositories = $('.container');
repositories.empty();
var names = es.map(function (e) {
return {name: e.name};
}).forEach(function (e) {
var rep = $('<div>');
rep.html(e.name);
repositories.append(e);
});
});
})($, _);
I'm playing with the code right now. So there might be duplication.
There is nothing incorrect with your code, and your RxJS usage looks fine, though your DOM usage is not as optimized as it could be. Creating/deleting all those DOM elements is a relatively expensive process, so ideally you want to resume elements where possible. Your sorting code seems ripe for optimizing in this respect.
When you sort your list, you know that DOM elements already exist for each. Instead of deleting all of them, then recreating them in the right order, I would instead use detach() to remove the element from the page and return it, then later use container.append(element) to add them in the right order.
If I was implementing it, I'd do something like rep.data('listCount', e.count) when I originally create the element, so we can sort the jQuery elements directly, then sort the list with:
sortByClickStream.subscribe(function() {
var container = $('.container');
// `.children()` returns raw DOM elements, so wrap each in jQuery
_.map(container.children(), function(el) { return $(el); })
.sortBy(function(item) { return item.data('listCount'); })
.reverse()
.forEach(function(item) {
item.detach();
container.append(item);
});
});
Doing something similar with the response stream list is possible, but a lot more work, since you can't guarantee that each element in the latest list already has an element.
Overall, what you have will work fine, and should be fast enough for small/medium-sized lists. If it appears to get sluggish with your expected list size, then I'd start optimizing DOM code. Frameworks like Angular have entire libraries dedicated to 'DOM diffing' to figure out the minimal number of changes needed to modify the DOM for updated content. If you are doing a lot of this sort of content updates, I'd look into using a library/framework that has this built-in.

Angular, filter objects in repeat, replace with new object

I am dynamically bringing in objects to an repeat, and when I get a certain type of object, I want to make a service call and replace it. My best guess was a filter on the repeat would be a good idea. So here is what I tried.
.filter('filterByType', [
vitModel.componentName,
function(myVitModel) {
return function(items) {
var newObj = _.map(items, function(item) {
if (!item.data) {
return item;
} else {
var newModel = new myVitModel(item.data);
newModel.getFullVitData().then(function() {
return newModel.data;
});
}
});
return newObj;
};
}
]);
This seems to have some problems in the logic, but the basic idea is if an item comes in that has .data it needs to call a new myVitModel on the data and replace it.
Everything seems sound until I drop an object in that does have the .data, then I am getting a
Error: $rootScope:infdig
Infinite $digest Loop
So I'm not sure where in this problem is being caused, possibly looping in on itself? Could use some help. Thanks!

Ember - Custom Computed Property to check if all dependent fields exists

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')

.filter jquery error

I'm having a problem, here is the javascript/jquery
getTextForDisplay: function() {
var displayText = "Select...";
var options = this.dataSource._data;
var selectedOptions = $.filter(options, function(index){
return this.selected;
});
if (selectedOptions.length == 1) {
displayText = "length1";
}
else if (selectedOptions.length > 1) {
displayText = "Multiple...";
}
return displayText;
}
});
so this is in regards to a multi-select dropdown box that has checkboxes, the options variable is an observable array pulling its data from a viewmodel, so what I am trying to do is to display "length1" if only one of the checkboxes is selected and to display "Multiple..." if more than one checkbox is selected, this seems pretty straightforward but I keep getting a error in when I run it. the error is c.replace is not a function and the error is in the jquery.min.js file. If I remove index from the .filter then it still doesn't work but it doesn't error out either.
jQuery doesn't define a jQuery.filter() function (at least, not in the public API). The .filter() it does define is a method for jQuery collections.
Perhaps jQuery.grep() is what you're looking for?
var selectedOptions = $.grep(options, function (option, index) {
return option.selected;
});
There is no such thing as $.filter(), unless you wrote it yourself or are using a plugin.
The correct syntax is
options.filter(function (index) {
...
});
Here's the documentation: http://api.jquery.com/filter/
You aren't properly using the jQuery.filter method, however, there is a documented method that does what you need. You should use documented methods rather than undocumented methods.
$.grep(options, function(){
return this.selected;
});
For this to work, options must be an array-like structure.

Creating methods on the fly

Hi I'm trying to author a jQuery plugin and I need to have methods accessible to elements after they are initialized as that kind of object, e.g.:
$('.list').list({some options}); //This initializes .list as a list
//now I want it to have certain methods like:
$('.list').find('List item'); //does some logic that I need
I tried with
$.fn.list = function (options) {
return this.each(function() {
// some code here
this.find = function(test) {
//function logic
}
}
}
and several other different attempts, I just can't figure out how to do it.
EDIT:
I'll try to explain this better.
I'm trying to turn a table into a list, basically like a list on a computer with column headers and sortable items and everything inbetween. You initiate the table with a command like
$(this).list({
data: [{id: 1, name:'My First List Item', date:'2010/06/26'}, {id:2, name:'Second', date:'2010/05/20'}]
});
.list will make the <tbody> sortable and do a few other initial tasks, then add the following methods to the element:
.findItem(condition) will allow you to find a certain item by a condition (like findItem('name == "Second"')
.list(condition) will list all items that match a given condition
.sort(key) will sort all items by a given key
etc.
What's the best way to go about doing this?
If you want these methods to be available on any jQuery object, you will have to add each one of them to jQuery's prototype. The reason is every time you call $(".list") a fresh new object is created, and any methods you attached to a previous such object will get lost.
Assign each method to jQuery's prototype as:
jQuery.fn.extend({
list: function() { .. },
findItem: function() { .. },
sort: function() { .. }
});
The list method here is special as it can be invoked on two occasions. First, when initializing the list, and second when finding particular items by a condition. You would have to differentiate between these two cases somehow - either by argument type, or some other parameter.
You can also use the data API to throw an exception if these methods are called for an object that has not been initialized with the list plugin. When ('xyz').list({ .. }) is first called, store some state variable in the data cache for that object. When any of the other methods - "list", "findItem", or "sort" are later invoked, check if the object contains that state variable in its data cache.
A better approach would be to namespace your plugin so that list() will return the extended object. The three extended methods can be called on its return value. The interface would be like:
$('selector').list({ ... });
$('selector').list().findOne(..);
$('selector').list().findAll(..);
$('selector').list().sort();
Or save a reference to the returned object the first time, and call methods on it directly.
var myList = $('selector').list({ ... });
myList.findOne(..);
myList.findAll(..);
myList.sort();
I found this solution here:
http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html
This seems to do exactly what I need.
(function($) {
var TaskList = function(element, options)
{
var $elem = $(element);
var options = $.extend({
tasks: [],
folders: []
}, options || {});
this.changed = false;
this.selected = {};
$elem.sortable({
revert: true,
opacity: 0.5
});
this.findTask = function(test, look) {
var results = [];
for (var i = 0,l = options.tasks.length; i < l; i++)
{
var t = options['tasks'][i];
if (eval(test))
{
results.push(options.tasks[i]);
}
}
return results;
}
var debug = function(msg) {
if (window.console) {
console.log(msg);
}
}
}
$.fn.taskList = function(options)
{
return this.each(function() {
var element = $(this);
if (element.data('taskList')) { return; }
var taskList = new TaskList(this, options);
element.data('taskList', taskList);
});
}
})(jQuery);
Then I have
$('.task-list-table').taskList({
tasks: eval('(<?php echo mysql_real_escape_string(json_encode($tasks)); ?>)'),
folders: eval('(<?php echo mysql_real_escape_string(json_encode($folders)); ?>)')
});
var taskList = $('.task-list-table').data('taskList');
and I can use taskList.findTask(condition);
And since the constructor has $elem I can also edit the jQuery instance for methods like list(condition) etc. This works perfectly.
this.each isn't needed. This should do:
$.fn.list = function (options) {
this.find = function(test) {
//function logic
};
return this;
};
Note that you'd be overwriting jQuery's native find method, and doing so isn't recommended.
Also, for what it's worth, I don't think this is a good idea. jQuery instances are assumed to only have methods inherited from jQuery's prototype object, and as such I feel what you want to do would not be consistent with the generally accepted jQuery-plugin behaviour -- i.e. return the this object (the jQuery instance) unchanged.

Categories

Resources