I am unable to figure out how to get to a property of a bound item. The following does not work when the .name() is there.
<span data-bind='text: selectedMenuSection().name()'></span>
Seems you can't access a property of a bound object in the data-bind. The same syntax works in a model function, MenuViewModel.showSelection(). I've tried every conceivable syntax with no success. Below is an excerpt of my code.
Html
MenuSections: <select data-bind="options: leftMenu, optionsText: 'name', value: selectedMenuSection, optionsCaption: 'Pick...'"> </select>
Selected: <span data-bind='text: selectedMenuSection().name()'></span>
<button data-bind="click: showSelection">Show Selected Info</button>
javascript
function MenuViewModel (menu) {
var self = this;
self.leftMenu = ko.observableArray(menu);
//Bound to Select
self.selectedMenuSection = ko.observable();
self.showSelection = function(){
alert(self.selectedMenuSection().name());
};
};
Is it not possible to get to a databound object property or have I just not figured out how to do it?
Can you try the following:
<span data-bind="with: selectedMenuSection">
<span data-bind="text: name"></span>
</span>
The first line makes sure that the inner span only exists if selectedMenuSection is not null or undefined (when you create self.selectedMenuSection, its value is undefined which can cause an error while trying to access the name property). The second line is your original binding. You don't need the () at the end, because you bind against the observable (you only need () to access the current value of the observable, or if you bind against a complex statement instead of directly to an observable).
What does a MenuSection look like? I'm assuming that it has a name property that is not an observable. Hans's answer is the correct way to solve your problem. Understand the javascript object and understanding what 'context' you are in when you write your knockout html is very important.
The with binding changes the context from the root element ( the object you passed into your ko.applyBindings) to the context of the selectedMenuSection. So inside of that 'with' your context ( $data in all cases ) is a MenuSection. If you are every curious about what object is in the context you can do something like this:
<div data-bind="with: selectMenuSection">
<pre data-bind="text: ko.toJSON($data)></pre>
<span data-bind="text: name"></span>
</div>
This will print out the current object context that you are in. You will then be able to see that you have a 'name' property available to you and whatever else is attached to a MenuSection.
Related
What i got is:
var post = function(id, message, username, date) {
this.id = id;
this.message = message;
this.username = username;
this.date = date;
this.comments = ko.observableArray([]);
this.addComment = function(context) {
var comment = $('input[name="comment"]', context).val();
if (comment.length > 0) {
$.connection.boardHub.server.addComment(this.id, comment, vm.username())
.done(function() {
$('input[name="comment"]', context).val('');
});
}
};
And View:
<form class="add-comment" data-bind="submit: addComment">
<div class="row">
<div class="col-md-9">
<input type="text" class="form-control" name="comment" placeholder="" />
</div>
<div class="col-md-3">
<button class="btn btn-default" type="submit">Answer</button>
</div>
And the question is how can i pass to addComment extra value without missing context?
Like:
<form data-bind="submit: function(){ writePost(<context>, '#ViewBag.UserN'); }">
As I mentioned in my comment, you are not using knockout correctly. Therefore I'll provide you with a generic example of how you should handle form submits using knockout.
First, you should define a viewmodel which wraps your UI data and connects to services to do any actual logic. In simple cases, many people just write the logic, such as API calls right into the viewmodel instead of a service. For simplicity, I'll just do that, because extracting it into its own 'class' should be simple enough. You should give your viewmodel a more descriptive name than post. Here is an example:
var commentViewModel = function(id, userName) {
var self = this;
this.commentText = ko.observable('');
this.addComment = function() {
var txt = self.commentText();
if (txt) {
$.connection.boardHub.server.addComment(id, txt, userName)
.done(function() {
self.commentText('');
});
}
};
};
var commentVM = new commentViewModel('#ViewBag.Id', '#ViewBag.UserN');
$(function() { ko.applyBindings(commentVM); });
HTML:
<form class="add-comment" data-bind="submit: addComment">
<div class="row">
<div class="col-md-9">
<input type="text" class="form-control" data-bind="textInput: commentText" />
</div>
<div class="col-md-3">
<button class="btn btn-default" type="submit">Answer</button>
</div>
</div>
</form>
Explaination:
The commentVM object contains a commentText member. This is a KO observable, so by exposing it by assigning it to this, you can bind to it. Then you can read the value of it by utilizing the KO binding mechanism instead of mixing jQuery and KO. Bindings exist to simplify your life not to be mixed with plain jQuery stuff, so you should use them wherever you can.
You should also expose an observable array to hold the actual comments, but since that wasn't part of your question I didn't want to introduce it to exclude any additional noise which is not related to what you asked.
In your HTML, you used the submit binding correctly. By binding the submit event to the addComment method, this method will get invoked correctly. But what's different compared to your code is that you read the comment text from the observable you've just defined, and avoid using jQuery. Since the VM itself is the context, you don't have to care about how to pass it -- that is, anything that you intend do use from within the submit method should generally be included in the viewmodel.
If, for whatever reason, you need to pass any extra parameter - such as dynamic content generated by Razor, for example, you can use the bind method. You can read more about it here, it is written just above Note 3. I'd like to note that even though it is documented in the click binding page, you can use it with submit as well (in fact, with any binding that binds to a method). Just for clarity, I'll include the relevant part here as well:
<button data-bind="click: myFunction.bind($data, 'param1', 'param2')">
Click me
</button>
So all you have to do is to specify the function to which you want to bind, and append a .bind($data, parameter1, parameter2,....) expression to it. Note that here, $data is the context; you should always include it in the .bind call. However, this will not be passed to any method parameter, it is necessary for KO to do its magic, so if you had a function like this:
function a(x,y,z)
then x would be passed param1, y would be passed param2 and z wouldn't be passed anything. If you want to force passing $data, you should include it twice in the bind call as such: .bind($data, $data, ....more params).
I have a list of items in an array.
When i click the item in my view i am attempting to remove this item
View
<div class="lootItem" repeat.for="lootItem of stack">
<div class="noselect" click.delegate="$parent.takeItem(lootItem)">
<i class="fa fa-diamond"></i> ${lootItem.value}
</div>
</div>
ViewModel
takeItem(lootItem){
this.eventAggregator.publish(new ItemTaken(lootItem));
console.log(_.includes(this.stack, lootItem)); //true
_.pull(this.stack, lootItem); //removes item and fails to update the ui
_.remove(this.stack, lootItem); //removes item and fails to update the ui
this.stack.shift(); //removes first item and updates the ui
}
Both .pull() and .remove() (using lodash) will remove the item in the array but not update the ui.
.shift() manages to remove an item from the array and is updating the UI.
Why is Aurelia not updating the UI despite me removing the item when using lodash?
addendum: it might be worth noting if i click the same item twice then _.includes is true the first time and then false the second time.
Aurelia can provide you with the index of the current item when inside a repeater using the $index variable. Then, simply use the built in array methods that ES2015 provides:
View
<div class="lootItem" repeat.for="lootItem of stack">
<div class="noselect" click.delegate="$parent.takeItem($index, lootItem)">
<i class="fa fa-diamond"></i> ${lootItem.value}
</div>
</div>
ViewModel
takeItem($index, lootItem){
this.eventAggregator.publish(new ItemTaken(lootItem));
this.stack.splice($index, 1);
}
I believe your problem lies in the scope of the variables.
_.pull and _.remove are returning a new instance of an array.
Try
this.stack = _.remove(this.stack, lootItem);
Using ES2015 Sets Yet another approach is to use the ES2015 Set data
structure. It can be initialized with an array of values, and the Set
prototype even provides a delete method for removing a specific value
...
However, the semantics of sets are different from regular arrays. By
definition, a set is a collection of unique values. Therefore, it
never contains duplicates. If you need a collection that allows for
duplicate values, sets are the wrong data structure.
https://blog.mariusschulz.com/2016/07/16/removing-elements-from-javascript-arrays
I am now using a Set for this and it allows me to use .add() and .delete(). I just need to remember that everything has to be unique (okay in my case here)
I'd still like to understand why manipulating the array though lodash doesn't work but i'll investigate that some other time.
Addendum
You can also use a prototype:
interface Array<T> {
remove(itemToRemove: T): Array<T>;
}
(<any>Array.prototype).remove = function (itemToRemove) {
const index = this.indexOf(itemToRemove);
if (index !== -1) {
this.splice(index, 1);
}
return this;
}
I want to create a dynamic menu list, and apply class at selected item.
I have an array of menu entries, menuItems which is an observableArray.
The page binding contain two entries :
menuItems
selectedPage
The idea is simple : apply a different class when the selectedPage parameter equal the page name to indicate to user which page is currently displayed.
<Repeater items="{{menuItems}}" id="repeater">
<Repeater.itemTemplate>
<Label text="{{name}}" class="{{ $parents['Page'].selectedPage == name ? 'selected' : '' }}" tap="navigate" />
</Repeater.itemTemplate>
</Repeater>
This doesn't work, so I have made some tests, and a strange thing happened.
When I use a simple Label into my Repeater to test my bindings, I can acces the good datas.
<Label text="{{name}}"/>
Display the good menuItems entry name.
<Label text="{{$parents['Page'].selectedPage}}"/>
Display the good selectedPage entry name.
But, these code samples doesn't work together. Both works only independently.
So, I'm a bit lost, is using a $parents based selector change the context inside the Repeater ?
I too have encountered this issue and found the solution in NativeScript's documentation on data binding:
Note: Binding expression could be used without explicitly named source property ( TextField text="" ). In that case $value is used as a source property. However this could lead to problems when a nested property should be observed for changes (e.g. item.nestedProp). $value represents bindingContext and when any property of the bindingContext is changed expression will be evaluated. Since nestedProp is not a property of the bindingContext in item.nestedProp then there will be no propertyChange listener attached and changes to nestedProp will not be populated to UI. So it is a good practice to specify which property should be used as source property in order to eliminate such issues.
What this means is that when you're binding a variable that's set directly on your binding context, you can pass the expression into the curly brackets by itself:
<Label text="{{name}}" class="{{ mySelectedPage == name ? 'selected' : '' }}" tap="navigate" />
...but if you're binding a variable that's nested within an object which is set on the binding context, you must pass that nested property into the curly brackets as the first parameter, and the expression itself as the second parameter:
<Label text="{{name}}" class="{{ $parents['Page'].selectedPage, $parents['Page'].selectedPage == name ? 'selected' : '' }}" tap="navigate" />
I'm just beginning to experiment in Angular, and confused about how best to approach binding using ng-repeat. I basically get the point about ng-repeat creating a child scope. My problem is much more basic :) For html like this:
<div ng-controller="swatchCtrl" class="swatch-panel">
Swatches
<ul>
<li ng-repeat="swatch in swatchArray" class="swatch">
<input
type="radio"
name="swatches"
ng-model="$parent.currentSwatch"
value="{{swatch}}"
>
<label class="swatch-label">
<div class="swatch-color" style="background-color: #{{swatch.hexvalue}};"></div
><span class="swatch-name">{{swatch.colorName}}</span>
</label>
</li>
</ul>
currentSwatch is:
<pre>{{currentSwatch | json}}</pre>
currentSwatchObj is:
<pre>{{currentSwatchObj | json}}</pre>
how do I tell this to fire??
swatchArray is:
<pre>{{swatchArray | json}}</pre>
</div>
and javascript like this:
function swatchCtrl($scope) {
$scope.swatchArray = [
{colorName:'Red', hexvalue: 'ff0000', selected: 'false'},
{colorName:'Green', hexvalue: '00ff00', selected: 'false'},
{colorName:'Blue', hexvalue: '0000ff', selected: 'false'}
];
$scope.currentSwatch = {};
}
http://jsfiddle.net/8VWnm/
I want to:
a) When the user clicks on a radio button, I want it to set both the colorName and the hexvalue properties of the currentSwatch object. Right now the binding seems to be giving me a stringified object from the array. How do watch the return of currentSwatch so I can parse it back to an available object? Simple, I know, but what am I missing?
b) When the user clicks on a radio button, I think I want that to set the value of the corresponding "selected" key in the original array to "true". Vice versa for unchecking. Let's say that only one swatch can ever be selected at a time in the palette. (I would like in theory to be able to iterate through the array later on, on the supposition that the different keys and values are likely to sometimes not be unique.)
This kinda stuff is super easy with jquery methods, but I'd like to learn the idiomatic angular way. Thanks in advance for any help.
http://jsfiddle.net/8VWnm/54/
Instead of listening to the ng-click event I would set the index of the selected element to a variable called "currentSwatchIndex"
<li ng-repeat="swatch in swatchArray" class="swatch">
<input
type="radio"
ng-model="$parent.currentSwatchIndex"
value="{{$index}}"
>
</li>
The you can $watch value changes of the currentSwatchIndex in your controller and set the selected swatch-Object and selection states in this $watch function:
$scope.$watch('currentSwatchIndex', function(newValue, oldValue) {
$scope.currentSwatchObj = $scope.swatchArray[newValue];
$scope.swatchArray[newValue].selected = true;
$scope.swatchArray[oldValue].selected = false;
});
Only knowing the currentSwatchIndex should be enough to identify the selected swatchObject. So probably you can get rid of the currentSwatchObj and the selected property of your swatchArray.
You can always get the selected swatch programmatically through a array access.
For future users that can come here to do the same in a select, you don't need use any index, the select must be done like this:
http://docs.angularjs.org/api/ng.directive:select
I just read about KnockoutJS and when I try to bind to sub-properties on objects that can be null I get binding errors, e.g.:
<div data-bind="text: selectedAccount().DocumentList().length"></div>
So once you call ko.applyBindings it tries to evaluate the above expression and if selectedAccount is null (which it is by default) it throws an error. I know that I can create a dependentObservable like so:
viewModel.docLength = ko.dependentObservable(function () {
return selectedAccount() ? selectedAccount().DocumentList().length : null;
})
But I was wondering if there's a solution other than putting properties in the ViewModel simply because I get a binding error.
A couple thoughts:
If you don't want to bother with the dependentObservable, then you can place your statement directly in the text binding like:
<div data-bind="text: selectedAccount() ? selectedAccount().DocumentList().length : null"></div>
or even shorter:
<div data-bind="text: selectedAccount() && selectedAccount().DocumentList().length"></div>
Depending on your scenario, you can also use the template binding to your advantage when dealing with potentially null values. It would be like this:
<div data-bind="template: { name: 'accountTmpl', data: selectedAccount }"></div>
<script id="accountTmpl" type="text/html">
${DocumentList().length}
</script>
Additionally, in the 1.3 release of Knockout there will be some control flow bindings that could be helpful to you. Particularly the "if" or "with" bindings would work for this situation. They are described here: https://groups.google.com/d/topic/knockoutjs/pa0cPkckvE8/discussion