I need to compare values of variable name by id. But I do not know ho to do it.
It must be something like:
if (this.name == self.copy().indexOf(this.id).name) {
alert('equals');
} else {
alert('not equals');
}
function MyViewModel() {
var self = this;
self.items = ko.observableArray();
self.copy = self.items;
self.items.push({ id: 1, name: 'Jhon' });
self.items.push({ id: 2, name: 'Smith' });
self.alarm = function () {
alert(this.name);
}
}
ko.applyBindings(new MyViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.0.0/knockout-min.js"></script>
<table>
<thead>
<tr>
<th>Passenger name</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td><input class="target" data-bind="value: name, event: { change: $root.alarm}" /></td>
</tr>
</tbody>
</table>
You're looking for the newer Array functions, e.g. filter. Use it like this:
self.alarm = function(data) {
var itemsWithSameName = self.copy().filter(function(item) {
return item.name() === data.name();
});
if (itemsWithSameName.length > 0) {
alert('At least one item with same name exists.');
} else {
alert('New name is unique, for now.');
}
}
However, for this to work you need to change name on your items observable (which is needed anyways for the value binding to work).
Also note that I've replaced this with data as passed to the custom event handler you bound in the view (the "current" item inside the foreach is passed as the first arg to alarm).
As a final note, beware that copy is not actually a copy, but a reference to the original array.
PS. If you want to filter by id you can do this:
self.alarm = function(data) {
var itemsWithSameId = self.copy().filter(function(item) {
return item.id === data.id;
});
// If `id` is guaranteed to be unique, you can probably get away
// with just assuming/grabbing the only item found:
var theItem = itemsWithSameId[0];
// Note that you've just essentially retrieved `data`. That is:
// data === theItem
if (!!theItem) {
alert('At least one item with same id exists.');
} else {
alert('Id of edited item cuold not be found.');
}
}
As I've put in comments, this makes less sense, because data passed to the event handler already is a reference to the item from your observableArray.
Related
I have a table of users that is populated dynamically from a Firebase database:
<table id="users">
<thead>
...
</thead>
<tbody>
<tr ng-repeat="user in users">
<td data-user-id="{{user.$id}}">{{user.$id}}</td>
</tr>
</tbody>
</table>
function loadUsers($scope, DatabaseFactory) {
DatabaseFactory.users.on('value', function(snapshot) {
var users = snapshot.val();
for(var id in users) {
var user = users[id];
// Find cell with user ID
var element = angular.element('#users tr td[data-user-id="' + user.id + '"]'); // 1
console.log(element); // 2
}
});
};
In loadUsers(), I'm trying to find a cell which contains a particular user ID. The log statement above returns an array with length 0. Why does this happen? When I try the statements 1 and 2 in the Chrome console, it works fine.
Put your loop inside viewContentLoaded event.Its look like things are asycnhronous.
$scope.$on('$viewContentLoaded', function () {
for (var id in users) {
var user = users[id];
// Find cell with user ID
var element = angular.element('#users tr td[data-user-id="' + user.id + '"]'); // 1
console.log(element); // 2
}
});
What worked for me was turning loadUsers() into a directive. It is called after generating the DOM elements with ngRepeat.
HTML:
<tr ng-repeat="user in users" load-users>
Angular:
var app = angular
.module('app', ['firebase'])
.directive('loadUsers', loadUsers);
function loadUsers($scope, DatabaseFactory) {
return function(scope, element, attrs) {
if (scope.$last) {
...
}
}
};
Really strange knockout error I am having. It's a pretty complex scenario so please see this fiddle:
http://jsfiddle.net/yx8dkLnc/
Essentially I have a double nested collection, the first collection FishMeasurements contains a collection of objects which have the species information associated with it, and a collection of Measurements which hold all measurements associated with that species.
Now when I try and remove items from the nested collection in this HTML:
<!-- ko foreach: FishMeasurements() -->
<h3><span data-bind="text: SpeciesName"></span><span data-bind="text: SpeciesId" style="display: none;"></span></h3>
<table class="table table-striped">
<thead>
<tr>
<th>Length</th>
<th>Count</th>
<th>Weight</th>
<th>Fish Id</th>
<th> </th>
</tr>
</thead>
<tbody data-bind="foreach: Measurements()">
<tr>
<td><span data-bind="text: LengthInMillimeters"></span></td>
<td><span data-bind="text: Count"></span></td>
<td><span data-bind="text: WeightInPounds"></span></td>
<td><span data-bind="text: FishCode"></span></td>
<td>Remove</td>
</tr>
</tbody>
</table>
<!-- /ko -->
The remove measurement function doesn't work when the Measurements collection has more than one object. I click the remove link, and it throws an error that says:
VM617:163 Uncaught TypeError: Cannot read property 'Measurements' of null(…)
The strange thing about this is, if I only add one item to the Measurements collection, the delete button work fine, but as soon as I add multiple measurements, if I click remove on any item in the table but the first row, this error is generated. However, if I click the first item in the table per species, there is no error and all records are removed!
Something tells me its treating Measurements like one object instead of a collection, because it only works on index 0. But I'm not sure because in console I am able to type:
mappedModel.FishMeasurements()[0].Measurements()[1]
And get a full ko_mapping object returned, so it's not null. But for some reason when I click the remove, it is null. As long as there is only one measurement per species, clicking remove works fine, as soon as there are more it breaks.
What am I doing wrong?
When you addMeasurement for the first time speciesId, speciesName are getting defined because fishMeasurementBySpecies === undefined and therefore when you remove the first item you have a valid measurement.SpeciesId() as a parameter inside removeMeasurement function but for the second time and more since fishMeasurementBySpecies is not undefined anymore then speciesId, speciesName never get set and then whenremoveMeasurementis called,measurement.SpeciesId() is null.
In order to make your model works, you need to apply below changes.
define var speciesId = mappedModel.SelectedSpecies(); before your if statment
var speciesId = mappedModel.SelectedSpecies();
if (fishMeasurementBySpecies === undefined || fishMeasurementBySpecies === null) {
Put () for Measurements inside removeMeasurement function where you want to get the length
if(fishMeasurementBySpecies.Measurements().length === 0)
Below I provide you an example of what you want to do by using manual view model instead of using mapping plugin.
Example: https://jsfiddle.net/kyr6w2x3/118/
Your example :http://jsfiddle.net/yx8dkLnc/1/
VM:
var data = {
"AvailableSpecies":
[
{"Id":"f57830b8-0766-4374-b481-82c04087415e","Name":"Alabama Shad"},
{"Id":"3787ce10-e61c-4f03-88a5-ff648bb55480","Name":"Alewife"},{"Id":"e923214f-4974-4663-9158-d6979ce637f1","Name":"All Sunfish Spp Ex Bass And Crappie"} ],
"SelectedSpecies": null, "CountToAdd":0,"LengthToAdd":0,"WeightToAdd":0,"GenerateFishCode":false,"FishMeasurements":[]
};
function AppViewModel(){
var self = this;
self.AvailableSpecies = ko.observableArray(data.AvailableSpecies);
self.SelectedSpecies = ko.observable();
self.CountToAdd = ko.observable();
self.LengthToAdd = ko.observable();
self.WeightToAdd = ko.observable();
self.FishCode = ko.observable();
self.FishMeasurements = ko.observableArray([]);
self.addMeasurement = function(item) {
var SpeciesExists = false;
ko.utils.arrayForEach(self.FishMeasurements(), function (item) {
if(item.SpeciesId() == self.SelectedSpecies().Id) {
var len = item.Measurements().length;
// you may have a better way to generate a unique Id if an item is removed
while(item.Measurements().findIndex(x => x.Id() === len) > 0){
len++;
}
item.Measurements.push(new MeasurementsViewModel({LengthInMillimeters:self.LengthToAdd(),
Count:self.CountToAdd(),
WeightInPounds:self.WeightToAdd(),
FishCode:self.FishCode(),
Id:len ++,
ParentId:self.SelectedSpecies().Id
})
);
SpeciesExists = true;
}
});
if(!SpeciesExists){
self.FishMeasurements.push(new FishMeasurementsViewModel({SpeciesName:self.SelectedSpecies().Name,
SpeciesId:self.SelectedSpecies().Id,
Measurements:[{LengthInMillimeters:self.LengthToAdd(),
Count:self.CountToAdd(),
WeightInPounds:self.WeightToAdd(),
FishCode:self.FishCode(),
Id:1}]
})
);
}
}
self.removeMeasurement = function(data){
ko.utils.arrayForEach(self.FishMeasurements(), function (item) {
if(item && item.SpeciesId() == data.ParentId()) {
ko.utils.arrayForEach(item.Measurements(), function (subItem) {
if(subItem && subItem.Id() == data.Id()) {
item.Measurements.remove(subItem);
}
});
}
if(item && item.Measurements().length == 0){
self.FishMeasurements.remove(item);
}
});
}
}
var FishMeasurementsViewModel = function(data){
var self = this;
self.SpeciesName = ko.observable(data.SpeciesName);
self.SpeciesId = ko.observable(data.SpeciesId);
self.Measurements = ko.observableArray($.map(data.Measurements, function (item) {
return new MeasurementsViewModel(item,self.SpeciesId());
}));
}
var MeasurementsViewModel = function(data,parentId){
var self = this;
self.LengthInMillimeters = ko.observable(data.LengthInMillimeters);
self.Count = ko.observable(data.Count);
self.WeightInPounds = ko.observable(data.WeightInPounds);
self.FishCode = ko.observable(data.FishCode);
self.Id = ko.observable(data.Id);
self.ParentId = ko.observable(parentId ? parentId : data.ParentId);
}
var viewModel = new AppViewModel();
ko.applyBindings(viewModel);
I have 2 observableArray connected to each other. When a "feature" is clicked, I try to show "tasks" of it. However KO, does not update UI when I clicked a feature. On the console, I can track my viewModel, and I can see tasks successfully loaded on selectedFeature. However, UI does not update, even all arrays are defined as observable.
Here is a live demo on fiddle.
Please tell me where I am missing?
function GetFeatures() {
var url = "/Project/GetFeatures";
$.get(url, "", function (data) {
$.each(JSON.parse(data), function (i, item) {
projectVM.features.push(new featureViewModelCreator(item, projectVM.selectedFeature));
});
});
};
function GetTasks(selectedFeature) {
var url = "/Task/GetTaskList";
$.get(url, { "FeatureId": selectedFeature.FeatureId }, function (data) {
$.each(JSON.parse(data), function (i, item) {
selectedFeature.tasks.push(new taskViewModelCreator(item, selectedFeature.selectedTask));
});
});
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
this.IsSelected = ko.computed(function () {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function () {
return selected() === self;
});
self.selectedTask = ko.observable();
self.taskClicked = function (clickedTask) {
var selection = ko.utils.arrayFilter(self.model.tasks(), function (item) {
return clickedTask === item;
})[0];
self.selectedTask(selection);
}
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function (clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(clickedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
On the UI
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<table class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature.tasks">
<tr>
<td><span data-bind="text:Title"></span></td>
</tr>
</tbody>
</table>
</div>
Here is the correct version with key notes: here. KO documentation is quite a detailed one.
You have mentioned an interesting note about UI code style: "As I know, we don't use () on UI". I did not put attention to this fact before.
We can really omit brackets for an observable: ko observable;
View contains an observable with no brackets:
<label>
<input type="checkbox" data-bind="checked: displayMessage" /> Display message
</label>
Source code:
ko.applyBindings({
displayMessage: ko.observable(false)
});
We can omit brackets for an observable array on UI: ko observable array
View contains: <ul data-bind="foreach: people">, while
View model has:
self.people = ko.observableArray([
{ name: 'Bert' },
{ name: 'Charles' },
{ name: 'Denise' }
]);
We can omit brackets on UI for 'leaf' observables or observables arrays. Here is your modified code sample. data-bind="if: selectedFeature" and data-bind="foreach: selectedFeature().tasks"> only leaf observable braces are omitted.
Finally, can we omit brackets for 'parent' observables? We can do it by adding another ko UI-statement (with instead of if, example 2).
The with binding will dynamically add or remove descendant elements
depending on whether the associated value is null/undefined or not
But, I believe, we can not omit brackets for parent nodes outside UI statement, because it is equal to a javascript statement: projectVM.selectedfeature().tasks. Othervise projectVM.selectedfeature.tasks will not work, because observables does not have such property tasks. Instead an observable contains an object with that property, which is retrieved by calling it via brackets (). There is, actually, an example on knockoutjs introduction page. <button data-bind="enable: myItems().length < 5">Add</button>
The code below uses the following fact (which can be found here, example 2):
It’s important to understand that the if binding really is vital to
make this code work properly. Without it, there would be an error when
trying to evaluate capital.cityName in the context of “Mercury” where
capital is null. In JavaScript, you’re not allowed to evaluate
subproperties of null or undefined values.
function GetFeatures() {
var data = {
Name: "Test Feature",
FeatureId: 1
}
projectVM.features.push(new featureViewModelCreator(data, projectVM.selectedFeature));
};
function GetTasks(selectedFeature) {
var data = {
Title: "Test Feature",
TaskId: 1
}
selectedFeature().tasks.push(new taskViewModelCreator(data, selectedFeature().selectedTask));
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Title;
// Step 3: you can omit $root declaration, I have removed it
// just to show that the example will work without $root as well.
// But you can define the root prefix explicitly (declaring explicit
// scope may help you when you models become more complicated).
// Step 4: data-bind="if: selectedFeature() statement was added
// to hide the table when it is not defined, this statement also
// helps us to avoid 'undefined' error.
// Step 5: if the object is defined, we should referense
// the observable array via -> () as well. This is the KnockoutJS
// style we have to make several bugs of that kind in order
// to use such syntax automatically.
this.IsSelected = ko.computed(function() {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function() {
return selected() === self;
});
self.selectedTask = ko.observable();
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function(clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(self.selectedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<hr/>
<table data-bind="if: selectedFeature()" class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature().tasks()"><!-- $root -->
<tr>
<td><span data-bind="text: Title"></span></td>
</tr>
</tbody>
</table>
</div>
I am using this plugin http://www.joshbuckley.co.uk/2011/07/knockout-js-datatable-bindings/
to handle datatables / ko bindings.
Here's JS code:
function ProductViewModel() {
// Init.
var self = this;
self.products = ko.observableArray();
self.singleProduct = ko.observable();
var mappedProducts;
// At first load i'm loading data.
$.getJSON("/admin/test", function(allData) {
mappedProducts = $.map(allData, function(item) {
var p = new Product(item);
// I'm adding a new property to my model, to handle row level actions.
// I'm not sure this is a good practice.
p.edit = "<button data-bind='click: $root.edit'><i class='icon-pencil'></i></button>";
return p;
});
self.products(mappedProducts);
});
// Here i'm using the basic switch pattern, as from KO tutorials.
self.edit = function(product) {
console.log(product); // <--- Prints the whole self.products() array
self.singleProduct(product);
self.products(null);
}
self.list = function() {
self.products(mappedProducts);
self.singleProduct(null);
}
}
// My model.
function Product(item) {
this.name = ko.observable(item.name);
this.dealer = ko.observable(item.dealer);
this.cost = ko.observable(item.cost);
this.price = ko.observable(item.price);
this.picture = ko.observable();
}
Here's my markup:
<table id="products-table" class="table table-striped table-bordered table-hover"
data-bind="dataTable: {data: $parent.products, options: {aoColumns: [
{ bSortable: false, mDataProp: null, sDefaultContent: '' },
{mData: 'name'},
{mData: 'dealer'},
{mData: 'cost'},
{mData: 'price'},
{ bSortable: false, mData: 'edit' }
]}}">
<thead>
<tr>
<th>Pic</th>
<th>Name</th>
<th>Dealer</th>
<th>Cost</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
I am able to virtually switch between list and edit views, bindings seems to be properly handled.
Problem: when in the edit handler, i would expect to receive a single model as an argument; i am receiving the whole collection though, so i am not able to identify the model to edit.
One more thing: I am not sure at all this is a good practice of binding events on the rows, so any suggestion would be appreciated!
Well i think i got it myself and reason is quite clear after peeking the plugin source code.
From plugin source:
(function($){
ko.bindingHandlers.dataTable = {
init: function(element, valueAccessor){
var binding = ko.utils.unwrapObservable(valueAccessor());
// If the binding is an object with an options field,
// initialise the dataTable with those options.
if(binding.options){
$(element).dataTable(binding.options);
}
},
update: function(element, valueAccessor){
var binding = ko.utils.unwrapObservable(valueAccessor());
// If the binding isn't an object, turn it into one.
if(!binding.data){
binding = { data: valueAccessor() }
}
// Clear table
$(element).dataTable().fnClearTable();
// Rebuild table from data source specified in binding
$(element).dataTable().fnAddData(binding.data());
}
};
})(jQuery);
Basically, for each update operation table is cleaned up and built again with the observable array, which should provide binding features.
What KO is trying to do, in each native click: binding, is to pass in the contextual data, which is the whole array, to the proper handler.
First of all, i'm new to Knockout.js and underscore.js, and this is my first day of learning those libraries. The task is to sort table by clicking column header in ascending order on first click, and in descending order on second click.
I have this kind of HTML markup:
<table>
<thead>
<tr data-bind="foreach: columnNames">
<td data-bind="text: $data, click: $root.sortColumn, css: { 'active': $root.currentItem() == $data }"></td>
</tr>
</thead>
<tbody data-bind="foreach: persons">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: formattedAge"></td>
<td data-bind="text: sex"></td>
<td data-bind="text: married"></td>
</tr>
</tbody>
</table>
And this js code for knockout.js:
function personViewModel()
{
var self = this;
self.currentItem = ko.observable('');
self.columnNames = ko.observableArray([
'Name',
'Age',
'Sex',
'Married'
]);
self.persons = ko.observableArray([...]);
self.sortColumn = function(item)
{
self.currentItem(item);
var sorted = _(self.persons()).sortBy(item.toLowerCase());
self.persons(sorted);
};
};
ko.applyBindings(new personViewModel());
Now the question is:
Is it possible to get element descriptor, while one of td's clicked, so i could use something like '$(this)' in self.sortColumn?
Because now i can sort a table by clicking appropriate column header, but i don't know how to mark a column, that it already was clicked (and check it), to use _(self.persons()).sortBy(item.toLowerCase()).reverse(), to sort it in descending order of columns.
Thanks:)
Answer is simple. Right now you saving only column name, you need also have variable for sort direction, and your logic in sortColumn would be:
function personViewModel()
{
var self = this;
self.currentItem = ko.observable('');
self.sortDirection = ko.observable(true);
self.columnNames = ko.observableArray([
'Name','Age','Sex','Married'
]);
self.persons = ko.observableArray([
{ name : "John", formattedAge:27, sex:"Male", married:"No"},
{ name : "Bob", formattedAge:30, sex:"Male", married:"Yes"}
]);
self.sortColumn = function(item)
{
if (item == self.currentItem()) {
self.sortDirection(!self.sortDirection())
} else{
self.currentItem(item);
self.sortDirection(true)
}
if( self.sortDirection() ) {
var sorted = _(self.persons()).sortBy(item.toLowerCase());
self.persons(sorted);
} else {
var sorted = _(self.persons()).sortBy(item.toLowerCase()).reverse();
self.persons(sorted);
}
};
};
ko.applyBindings(new personViewModel());
See jsfiddle with working example.
Also notice that you don't really need underscore.js here, as ko.js provides you with
myObservableArray.reverse()
myObservableArray.sort()
Everything you need.
var sortDirection = true
self.sortColumn = function(item) {
if (item == self.currentItem()) {
sortDirection = !sortDirection
self.persons.reverse()
} else {
sortDirection = true
var field = self.currentItem()
self.persons.sort(function(left, right) {
return left[field] == right[field] ? 0
: ( left[field]< right[field] ? -1 : 1 )
})
}
}