Just discovering knockout so please point out stupidity if I missed something here. I currently have a multiple select bound to an observable in knockout.
<select class="form-control" id="SithSelect" data-bind="attr: { 'data-has-value': (Sith() ? true : false) }, selectedOptions: Sith() ? Sith().split('|') : '' " placeholder="The dark side" multiple>
<option disabled></option>
<option value="Darth Vader">Darth Vader</option>
<option value="Darth Maul">Darth Maul</option>
<option value="Darth Bane">Darth Bane</option>
</select>
Where "Sith" is an observable string value.
When I manually add the string value "Darth Vader|Darth Maul" in the database and the value eventually gets down to the client side, the multi select correctly shows two values selected.
However, if I change the values, how do I then combine the selected values back to a pipe delimited string stored in the observable?
From what I've tried I can sort this by having an observable array that the select is bound to and then re-construct the pipe delimited string on save to the database. This solution smells a bit funny when I look over the code tho because it feels tightly coupled to the individual select element (this is the only one on the page that uses a pipe delimited string from the DB).
Have I missed something in managing to do two-way binding between a multi select and a delimited string? Rather, is there a way to do the opposite of
selectedOptions: Sith() ? Sith().split('|') : ''
when assigning the multiple selected options back to the observable value in the binding?
UPDATE: The question has been narrowed down to:
Is there something inbuilt to knockout that I can place within a binding, that can handle the read/write functions of a computed value, so that I can write it inline, rather than having to create a separate computed observable value?
selectedOptions should be array or an observable array. http://knockoutjs.com/documentation/selectedOptions-binding.html
There's no way for Knockout to know how to save data back to Sith, when you're passing to it some data calculated from Sith.
You should use ko.observableArray or just ko.observable with array value. But you'll need to synchronize your string value and array value. Instead of manually supporting two subscriptions (str=>arr, arr=>str) you can use writable computed (http://knockoutjs.com/documentation/computed-writable.html):
<select data-bind="selectedOptions: sith">...</select>
vm.sithString = ko.observable('');
vm.sith = ko.computed({
read: function() { return vm.sithString().split('|'); },
write: function(arr) { vm.sithString(arr.join('|')); }
});
UPDATE
Is there something inbuilt to knockout that I can place within a binding, that can handle the read/write functions of a computed value, so that I can write it inline, rather than having to create a separate computed observable value?
This is the exact purpose of ViewModel in MVVM. You have model, which is you pure data (well, you have to store it in observables, but anyway). You have bindings, which are purely UI things. And you have mediator between them ViewModel to convert data and events between them. Writable computed in this example represents ViewModel-Model relations.
Writable computeds is much more general and flexible mechanism than write/read functions per binding.
UPDATE 2
Well, selectedOptions is not the easiest binding to sit on top of. The way is to attach exactly the same computed to element and replace valueAccessor in binding functions. Code is straightforward, but requires knowledge of how bingings work:
var selectedOptions = ko.bindingHandlers['selectedOptions'];
ko.bindingHandlers['pipedOptions'] = {
after: selectedOptions.after, /* ['options', 'foreach'] */
init: function(el, va) {
var obsv = va();
var mediator = ko.computed({
read: function() { return obsv().split('|'); },
write: function(arr) { obsv(arr.join('|')); }
});
ko.utils.domData.set(el, 'pipedOptions.mediator', mediator);
var args = [].slice.call(arguments);
args[1] = function() { return mediator; };
selectedOptions.init.apply(this, args);
},
update: function(el, va) {
var mediator = ko.utils.domData.get(el, 'pipedOptions.mediator');
var args = [].slice.call(arguments);
args[1] = function() { return mediator; };
selectedOptions.update.apply(this, args);
}
};
If you need more complex behavior take a look at components.
Related
I am trying to create a page that shows a list of products with filtering and multiple ways of sorting the list. Rather than repeating the filtering code for each sort, I wanted to start each of the sorting methods with a call to the filtering method. I can currently filter by name:
filterText: '',
filteredResults: function() {
var filterText = this.get('filterText');
var regexp = new RegExp(filterText,'gi');
var causes = this.get('model.products').filter(function(name){
return name.get('name').match(regexp);
});
return causes;
}.property('filterText'),
However I now want to use this function in the sort methods. I've tried using Ember.computed.sort :
priceAscSorting: ['price'],
sortPriceAsc: Ember.computed.sort('filteredResults', 'priceAscSorting'),
but it seems like it treats the first input as null if I stick it in a template. If I try calling the filter method inside the sorting method:
priceAscSorting: ['views'],
sortPriceAsc: function() {
var products = filteredResults();
}
Ember throws a compiler error saying 'filteredResults' is not defined. How can I access my filter methods to use in my sorting methods?
Your code is correct, it will take filteredResults computed property and return sorted result in ascending order by using price key.
priceAscSorting: ['price'],
sortPriceAsc: Ember.computed.sort('filteredResults', 'priceAscSorting'),
but it seems like it treats the first input as null if I stick it in a
template
that means you need to check filteredResults computed property
I have $scope.myArray, and it's binding with an input field by ngModel and the expression {{myArray}}
My issue is when I modified myArray by call changeMyArray(), the input's value did not change. But the expression {{myArray}} is display new value.
So, Why the expression work but input field does not?
I have a way to do, but I want to find a better approach
var newArr = $scope.myArray;
newArr.push("b");
$scope.myArray = angular.copy(newArr);;
Example fiddle
Basically, I think what you want to do is bind the input to a "new entry" scope variable, and then push the value of that variable to your array when the user clicks "Push To". Here's what I mean:
In controller:
$scope.changeMyArray = function() {
$scope.myArray.push($scope.newEntry);
$scope.newEntry = "";
}
In HTML:
<input ng-model="newEntry">
But actually:
Really what you want is a way to edit the contents of an array via text, and have updates to that array from elsewhere also update the text. This is actually pretty simple since browsers come with a JSON library.
I implemented it by starting with a known pair of objects:
$scope.myArray = [];
$scope.myArrayString = "[]";
That way you can update the string via ngModel:
<input ng-model="myArrayString">
Watch for changes on this model to update the actual array:
$scope.$watch("myArrayString", function() {
$scope.myArray = JSON.parse($scope.myArrayString);
});
Then update the string in the changeMyArray function:
$scope.changeMyArray = function() {
$scope.myArray.push("b"); // Or whatever you would like to add here
$scope.myArrayString = JSON.stringify($scope.myArray);
}
Experiment in my fork of the Fiddle.
What's going on?
The variable $scope.myArray is an object, and any object in Javascript can be converted to a string (most complex objects end up as the unhelpful "[object Object]"). Arrays will actually display their contents when converted to a string, so binding an array to HTML via {{myArray}} is pretty straightforward.
However, the reverse conversion is not as simple. In general, a text input can't be bound to an array in a two-way fashion as we'd like. The solution, then, is to use an intermediary variable to hold the string value, and use $scope.$watch to keep the two values in sync.
So you seem to be wondering why when pushing to the array, your $watch function doesn't do the increment. That's because the #watch function only checks object reference equality.
When pushing to the array, the reference stays the same. When you copy the array and set it again in the same variable, the reference changes.
That's why #watchCollection works as expected and increments when each item is pushed.
I have an explanation for my question. Please correct me if I wrong, very thank.
My Question:
Why "myArray" input field does not update when $scope.myArray is changed (Model doesn't update View)?
<input ng-model="myArray" id="myArray">
The answer is AngularJs ng-model doesn't know $scope.myArray is changed. Because ng-model does not perform a deep watch of object (rather than a string or number), it only looks for a change of identity or compares the reference of the objects.
In my case, $scope.myArray is collection. So, although $scope.myArray has changed by push new item (structure is changed), it's reference does not change.
As the result, $setViewValue() and $render() never invoked to update the view.
$render
$setViewValue
Solution:
Sol1: Add new item to $scope.myArray, make a copy of myArray object and then asign a copy to $scope.myArray again. By this way, the object reference is changed. AngularJs see that change and update the view.
var newArr = $scope.myArray;
newArr.push("b");
$scope.myArray = angular.copy(newArr);
Sol2: Create custome $watch('email', function(){...}, true). The last parameter is TRUE to let Angular perform a deep watch. Then, in watch's listener function, I manually set $viewValue = newValue and invoke $render() of ngModelController to update the view. In case we have Formatters, we should invokes them in this step.
$scope.$watch('myArray', function(newValue, oldValue) {
if (newValue !== oldValue) {
var ctrl = angular.element(document.querySelector('#myArray')).controller('ngModel');
// Invoke formatter
var formatters = ctrl.$formatters,
idx = formatters.length;
while(idx--) {
newValue = formatters[idx](newValue);
}
ctrl.$render();
}
}, true);
Please see my script
I am trying to append an additional value to a knockout computed observable with no luck. Hopefully someone can point me in the right direction.
Currently I am iterating through a collection of fields and building an array of dates
//extract Primary dates from entities
report.PrimaryDateRangeAttributes = ko.computed(function () {
return $.grep(entity.PrimaryAttributes(), function (item) { return item.DataType() === 'datetime' });
});
Once I get the array built I wanted to add one additional item
report.PrimaryDateRangeAttributes.push('DateEntered');
However push is not supported on a computed observable. Can anyone provide some suggestions on how to insert the additional value during the initialization of the object?
Thanks in advance,
Since the computation function is called to build the value whenever the value is required (that's the purpose of computed, after all), you simply add that within the function:
//extract Primary dates from entities
report.PrimaryDateRangeAttributes = ko.computed(function () {
var rv = $.grep(entity.PrimaryAttributes(), function (item) { return item.DataType() === 'datetime' });
rv.push('DateEntered');
return rv;
});
I have an object that represents a restaurant order:
function order () {
this.customer_name = ''
this.menu = // menu object
}
extended with some object methods for business logic, like:
order.prototype.value = function() {
var total = 0;
for (var i = 0; i < this.menu.length; i++) {
// calculate the order value
}
return total;
}
In the angular controller orders get pushed onto an array when submitted (via ng-click from a button in the view):
var ref = new Firebase('https://myfirebase.firebaseio.com');
$scope.orders = [];
angularFire(ref, $scope, 'orders');
$scope.currentOrder = orderService;
$scope.submitOrder = function() {
$scope.orders.push($scope.currentOrder);
};
Once orders are pushed into the array, properties like orders[0].customer_name work, but methods like orders[0].value() don't.
It seems reasonable that Firebase/Angularfire would only be syncing JSON, but is there an approach that would allow me to keep order-related logic included with the order object, i.e without having to write $scope.getOrderValue(orders[0])?
There isn't a great way to do exactly what you want, since according to the Firebase FAQ:
At a low-level, we support basically the same data types as JSON: Strings, Numbers, Booleans, and Objects (which in turn contain Strings, Numbers, Booleans, and more Objects).
Which means you can store data but not functions. It seems like a clean way to accomplish the same thing would be to store the latest order value as a property of your order object, and have a method as part of your orderService that updates it whenever menu items are added or removed. Alternatively, do what you suggested and have a getOrderValue somewhere, but it probably still makes sense to put that in a service.
I actually had the same issue.
I wanted to add a method to my firebase object.
After looking in the latest angularfire docs I found that $extend can do just that
I didn't test it yet, but I think this is the way to go about it.
What is the basic way to keep models any views in order using Backbone? I have some idea but it's not totally clear.
I want to keep them in order by a field called "created_at". I know there is the ability to provier a comparator function in the collection but I'm not sure how it works.
I also want this order in the collection to be reflected by the views at all times (in a list). I'm not exactly sure where I tie into the model though. I'm guessing i look for change in an index attribute and then update a to match?
Thanks very much for any help or explanation!
When you define your collection, you also define comparators.
I did it this way recently:
comparators: {
id: function(animal) {
return Number(animal.get("id"));
},
d_id: function(animal) {
return -Number(animal.get("id")); // descending
},
name: function(animal) {
return animal.get("name");
},
d_name: function(animal) {
return String.fromCharCode.apply(String, _.map(animal.get("name").split(""), function (c) {
return 0xffff - c.charCodeAt();
})
);
},
}
these I defined within my collection code.
Then, in rendering my collection views, I just did this
(this was within my view that renders the whole collection, in initialize():
this.collection = new MyCollection();
this.collection.comparator = Collection.comparators[// here I put 'id' or 'd_id' etc. ];
this.collection.sort();
Since this code is in your views's initialize, you can define your comparator
when you initialize your view, and pass it a name of a comparator like this:
var directory = new pageView("d_id");
and than thru initialize(comparator_id) you could pass this to your code in initialize:
this.collection = new MyCollection();
this.collection.comparator = Collection.comparators[comparator_id];
this.collection.sort();
And then I can use the collection in rendering and re-rendering the view/page
Edited:
Here is Backbone's collection.comparator documentation,
And right below it is an explanation of sort()
Basically, comparator can be a property of a model or a function that returns a property,
Or a negative property, if it's numeric, for descending order,
or a string or its reverse value for descending order
like in the example I gave you here.
So, comparator returns a property like "id" or "name", or "-id" , "-name" etc etc.
(for string you can't just make it "negative", you need to apply more complex function,
as I wrote.)