How to move functions to a separate file in MVVM? - javascript

I have following Kendo UI MVVM. Here the business functions are written inside the viewModel.How can we move these functions to a different file?
The challenge I am facing is due to the “this” keyword inside these functions which refers the model present inside the observable.
View Model
<script type="text/javascript">
var viewModel = kendo.observable({
// model definition
employees: [
{ name: "Lijo", age: "28" },
{ name: "Binu", age: "33" },
{ name: "Kiran", age: "29" }
],
personName: "",
personAge: "",
//Note: Business functions does not access any DOM elements using jquery.
//They are referring only the objects in the view model.
//business functions (uses "this" keyword - e.g. this.get("employees"))
addEmployee: function () {
this.get("employees").push({
name: this.get("personName"),
age: this.get("personAge")
});
this.set("personName", "");
this.set("personAge", "");
},
deleteEmployee: function (e) {
//person object is created using "e"
var person = e.data;
var employees = this.get("employees");
var index = employees.indexOf(person);
employees.splice(index, 1);
}
});
</script>
Head
<head>
<title>MVVM Test</title>
<script type="text/javascript" src="lib/kendo/js/jquery.min.js"></script>
<script type="text/javascript" src="lib/kendo/js/kendo.web.min.js"></script>
<!----Kendo Template-->
<script id="row-template" type="text/x-kendo-template">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: age"></td>
<td><button type="button" data-bind="click: deleteEmployee">Delete</button></td>
<td>
</tr>
</script>
<!--MVVM Wiring using Kendo Binding-->
<script type="text/javascript">
$(document).ready(function () {
kendo.bind($("body"), viewModel);
});
</script>
</head>
Body
<body>
<table>
<thead>
<tr>
<th>
Name
</th>
<th>
Age
</th>
</tr>
</thead>
<!--The data-template attribute tells Kendo UI that the employees objects should be formatted using a Kendo UI template. -->
<tbody data-template="row-template" data-bind="source: employees">
</tbody>
</table>
<br />
<br />
<form>
<input type="text" name="personName" placeholder="Name" data-bind="value: personName" /><br />
<input type="text" name="personAge" placeholder="Age" data-bind="value: personAge" /><br />
<button type="button" data-bind="click: addEmployee">
Add</button>
</form>
</body>
REFERENCES
The Kendo MVVM Framework - packtpub
MVVM / Remote binding -- demos.telerik
DropDownList / MVVM - demos.telerik
DataSource / Basic usage

Whether you define it in another javascript file or outside of your kendo.observable function, you just need to bind 'this' to the function.
This is untested but the first may work, while the second is sure to work!
function externalAddEmployee() {
this.get("employees").push({
name: this.get("personName"),
age: this.get("personAge")});
this.set("personName", "");
this.set("personAge", "");
}
kendo.observable({
// first solution: may not work because the 'this' you want is not properly defined
addEmployee: externalAddEmployee.bind(this);
// second solution: will work everytime
// the 'this' is properly defined, just proxy the call to the function
addEmployee: function() { return externalAddEmployee.apply(this, arguments); }
})
To improve the second solution, you can even make an helper function:
function proxy(fn) { return function() { return fn.apply(this, arguments); })
// and then
kendo.observable({
addEmployee: proxy(externalAddEmployee)
})

Related

Creating an alias in Knockout?

At times I have elements from a view model that I am accessing multiple times in the view and I would like to be able to assign an alias from the view context to shorten things up.
I'm looking for something that would be similar to the foreach as alias or with, but that I am able to use on a single arbitrary access.
As an example, in the following I might want to alias $root.form().budget.budgetEndDate to endDate
<input id="foo" type="text" data-bind="dateTimePicker: $root.form().budget.budgetEndDate" />
Then I could rewrite the binding as
<input id="foo" type="text" data-bind="dateTimePicker: endDate" />
Two options.
First, for demo purposes only but not quite recommended, you ould pollute the global namespace:
function Root() {
this.form = ko.observable(new Form());
}
function Form() {
this.budget = {
budgetEndDate: ko.observable("dummy budget end date")
};
window["endDate"] = this.budget.budgetEndDate;
}
ko.applyBindings(new Root());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div>
endDate outside form: <b data-bind="text: endDate"></b>
</div>
<div data-bind="with: form">
<div>inside form: <b data-bind="text: endDate"></b></div>
<div data-bind="with: budget">
<div>inside form.budget: <b data-bind="text: endDate"></b></div>
</div>
</div>
Second, you could place it on $root:
function Root() {
this.form = ko.observable(new Form());
this.endDate = this.form().budget.budgetEndDate;
}
function Form() {
this.budget = {
budgetEndDate: ko.observable("dummy budget end date")
};
}
ko.applyBindings(new Root());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div>
endDate outside form: <b data-bind="text: $root.endDate"></b>
</div>
<div data-bind="with: form">
<div>inside form: <b data-bind="text: $root.endDate"></b></div>
<div data-bind="with: budget">
<div>inside form.budget: <b data-bind="text: $root.endDate"></b></div>
</div>
</div>
However, on a subjective note, I'd consider this a design smell and/or an XY-problem. The place you bind to budgetEndDate should probably be the right context / view to access it directly. You should consider moving that observable to another part of your view model.

One form and validation for many objects with different data

Is it possible to check that form is valid in js file, but for many objects? I want to have only one form in html and don't use any of ng-repeats or other loops in html, then check form is valid for all objects.
Exmaple
<!DOCTYPE html>
<html ng-app="AngularApp">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"> </script>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body ng-controller="mainController">
Click on table row if u want to change data in form
<table>
<thead>
<tr>
<td>
Index
</td>
<td>
Name
</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="element in testCollection"
ng-click="changeActiveElement(element)">
<td>{{ $index }}</td>
<td>{{ element.name }}</td>
</tr>
</tbody>
</table>
<br />
<br />
<br />
<form name="exampleForm">
<div class="row">
Name [required]: <input type="text" ng-model="activeObject.name" required>
</div>
<div class="row">
Phone [required]: <input type="text" ng-model="activeObject.phone" required>
</div>
<div class="row">
Active: <input type="checkbox" ng-model="activeObject.active">
</div>
</form>
<br />
<button ng-disabled="">
This button should be enable if all objects from table will pass form validation
</button>
but how to do this? Button should be know that every form is good or not, even if won't change object by clicking on table row.
</body>
</html>
Js:
var app = angular.module("AngularApp", []);
app.controller('mainController', function($scope) {
$scope.testCollection = [
{
name: 'Mike',
phone: 12345678,
active: true
},
{
name: 'Martin',
phone: '',
active: false
},
{
name: 'Anna',
phone: '',
active: ''
}
];
$scope.activeObject = $scope.testCollection[0];
$scope.changeActiveElement = function(element) {
$scope.activeObject = element;
};
});
Yes its possible, so lets say that 'many objects' are like this :
$scope.fianlObject = { manyObj1 : {}, manyObj2 :{}}
then do something like this with HTML :
<form id="frm1" name="frm1" ng-submit="submit()">
<div class="form-group required" ng-class="isInvalid('manyObj1', form1)">
....
</div>
<div class="form-group required" ng-class="isInvalid('manyObj2', form1)">
....
</div>
.
.
</form>
and something like this with script :
$scope.isInvalid = function (manyObj, form) {
if (form&& form.$submitted) {
return ( form[manyObj] && form[manyObj].$invalid) ? 'has-error'
: '';
}
return '';
}
Angular have FormController, that have property $invalid
$invalid
boolean
True if at least one containing control or form is invalid.
and you can use it like
<button ng-disabled="formName.$invalid" ... >
Yes, you can do that.
Just specify you validator function as a pure function which just accepts a plain object with the data and returns an array of errors (empty if everything is ok). This function should know about context it's used in.
Simple example:
function validateForm(data) {
var errors = [];
if (data.name === 'Joe') errors.push("You can't be Joe!");
return errors;
}
Then, every time you want to validate your form, convert your form data to JS object and use the function. Also, you can use such function in any other context, no matter who is initiator of the validation.

Displaying different <tr> depending on radio button (in Knockout)

OK, here goes.
Problem 1:
I want to, based on the radio button checked, display a tr-element or not. Threre will be 3 buttons, displaying 'unlocked achievements', 'locked', and 'all' (both locked + unlocked).
The code below shows how it looks when I try to call three different functions, each setting the tr:s visibility to true/false depending on radio button checked. (D.R.Y, I know, but right now I'm just looking for the functionality).
Problem 2:
Making the for-loop run. itemsListForFilter is declared globally, outside the filter function. itemsListForFilter is a copy of an object arrayMap which is initiallized and filled elsewhere in the code. The array contains items with - amongst other things - the boolean "radioCheck", with the default value "true", which I want to check.
When I access itemsListForFilter in the function where the copying takes place it's filled with items but...
When I try to access itemsListForFilter in the filter function it displays as having the value of null. So the copy is "lost" somewhere :)
View / HTML:
<div class="widget-header-container">
<h3 class="widget-header">Achievements</h3>
<div class="wrapper">
<input type="radio" name="appliedFilter" value="all" data-bind="checked: filterAll"/><label for="all">Show all</label>
<input type="radio" name="appliedFilter" value="unlocked" data-bind="checked: filterUnlocked"/><label for="unlocked">Unlocked</label>
<input type="radio" name="appliedFilter" value="locked" data-bind="checked: filterLocked"/><label for="locked">Locked</label>
</div>
<div><div class="widget-header-line-game1"></div><div class="widget-header-line-game2"></div><div class="widget-header-line-shadow"></div></div>
</div>
<div>
<div class="rounded-box" style="padding:15px;padding-top: 0;background-color:#fff;overflow:hidden;">
<table id="game-achievements" class="table table-condensed" style="margin-top:10px;">
<tbody data-bind="foreach: viewGame.achievements()">
<tr data-bind="visible: radioCheck" style="display: none">
Viewmodel / JS:
filterUnlocked: function(){
return filter('unlocked');
},
filterLocked: function(){
return filter('locked');
},
filterAll: function(){
return filter('all');
},
filter: function(x){
for (var item in itemsListForFilter){
if (x === 'locked'){
item.radioCheck = '!achieved';
}
if (x === 'unlocked') {
item.radioCheck = 'achieved';
}
else {item.radioCheck = true;}
}
Observe that the viewmodel is an object and not a function:
var gamesViewModel = {
self: this,
settings: null,
gameId: null,
authorized: false,
(...)
Right now the functions named filterUnlocked, etc (except filter) displays as "unused properties" in the JS file. What should i do to call them from the HTML? Or is there a better way to accomplish what I'm looking for?
Thank you.
You can get the effect you want with less complexity.
First, the radio group. The checked binding in Knockout kind of replaces (or acts like) the name attribute for a radio group: all the radio buttons should share one. The bound variable will be updated to the value of the selected radio button, which will cause others bound to the same variable to de-select. Now you have one variable to look at instead of three.
Instead of doing filtering by hiding rows, it is more usual to have a computed filter the data, and the table just displays it. The computed can get the data from "outside"; it doesn't have to be an observable or part of the viewmodel (although if it's not an observable, you'll need to tell the computed when to update, if the source changes).
Here's a little working example:
var achievements = [{
name: 'The locked one',
locked: true
}, {
name: 'The unlocked one',
locked: false
}];
var vm = {
appliedFilter: ko.observable('all'),
viewGame: {}
};
vm.viewGame.achievements = ko.computed(function() {
var filter = vm.appliedFilter();
if (filter === 'all') {
return achievements;
}
return ko.utils.arrayFilter(achievements, function(item) {
return item.locked === (filter === 'locked');
});
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="widget-header-container">
<h3 class="widget-header">Achievements</h3>
<div class="wrapper">
<input type="radio" value="all" data-bind="checked: appliedFilter" />
<label for="all">Show all</label>
<input type="radio" value="unlocked" data-bind="checked: appliedFilter" />
<label for="unlocked">Unlocked</label>
<input type="radio" value="locked" data-bind="checked: appliedFilter" />
<label for="locked">Locked</label>
</div>
</div>
<div>
<div class="rounded-box" style="padding:15px;padding-top: 0;background-color:#fff;overflow:hidden;">
<table id="game-achievements" class="table table-condensed" style="margin-top:10px;">
<tbody data-bind="foreach: viewGame.achievements()">
<tr><td data-bind="text: name"></td></tr>
</tbody>
</table>
</div>
</div>

Why is my AngularJs 'add' button only working once?

I'm just starting out with Angular.
I've written some code that downloads a JSON array configuredAPIs and displays each object within it, <div ng-repeat="capi in configuredAPIs">. For each of these, there's another directive to list the items from an array of strings, <tr ng-repeat="eurl in capi.externalURLs">
Underneath there's a text box to add a new string to this array, which I've bound to a $scope variable called url.
When I click the 'add' button, everything works - the new string is added to the array, a new row appears in the table.. ..but it only works once. Subsequent clicks on the 'add' button add empty strings to the array (and thus empty text boxes).
What have I done wrong?
index.html
<div ng-app="testApp" ng-controller="testCtrl">
<div ng-repeat="capi in configuredAPIs">
<h1>Configured API</h1>
<p>
Name:
{{ capi.name }}
</p>
<h2>External URLs</h2>
<form ng-submit="addExternalURL(capi)">
<table>
<!-- A row in the table for each string in the array -->
<tr ng-repeat="eurl in capi.externalURLs">
<td>
<input type="text" ng-model="eurl" />
</td>
</tr>
<!-- Final table row to add a new string to the array -->
<tr>
<td>
<input type="text" ng-model="url" placeholder="Enter a new external URL">
<input class="btn-primary" type="submit" value="add">
</td>
</tr>
</table>
</form>
</div>
</div>
controller.js
var app = angular.module('testApp', []);
app.controller('testCtrl', function ($scope, $http) {
$scope.url = 'new url';
$http.get("/api/configuredapis?orgid=2")
.success(function (response) { $scope.configuredAPIs = response; });
$scope.addExternalURL = function ($capi) {
$capi.externalURLs.push($scope.url);
$scope.url = '';
};
});
It is because AngularJS does not watch and update primitives (e.g. strings, numbers, booleans) the way one obviously thinks it does.
So instead you bind objects with values to the scope or use a function which returns the primitive value.
See:
https://github.com/angular/angular.js/wiki/Understanding-Scopes
http://www.codelord.net/2014/05/10/understanding-angulars-magic-dont-bind-to-primitives/
Example for using an object (at controller):
$scope.primitives = {
url : 'foo://'
}
And within the template:
<input type="text" ng-model="primitives.url">
So what happens in your example is that once you set it to '' the changes to the model within the template are not recognized anymore.

Update ViewModel using Custom Bindings KnockoutJS

So I have this SPA developed using this sample.
The sample shows the list of Todo in a table something like this
<section id="lists" data-bind="foreach: todoLists, visible: todoLists().length > 0">
<table width="100%" style="margin-top: 20px;" class="table-main">
<thead>
<tr class="b-table-line">
<th>Select</th>
<th>Title</th>
<th>Artist</th>
</tr>
</thead>
<tbody data-bind="foreach: todos">
<tr>
<td>
<input type="checkbox" data-bind="checked: isDone" /></td>
<td>
<input class="todoItemInput" type="text"
data-bind="value: title,
disable: isDone,
blurOnEnter: true,
updateOnTitle:true,
click: $root.clearErrorMessage" />
</td>
<td>
<input class="todoItemInput" type="text"
data-bind="value: artist,
click: $root.clearErrorMessage" />
</td>
</tr>
</tbody>
</table>
Now what I am trying to do here is as soon as I change the Title text, I try to change Artist text as well, for that I have created a custom binding updateOnTitle and associated it with the textbox as shown in the table. Its definition looks something like this:
ko.bindingHandlers.updateOnTitle = {
init:function(element,valueAccessor,allBindings,viewModel,bindingContext)
{
$(element).blur(function (evt) {
//Here I am trying to update the artist property based on title
bindingContext.$data.title("Title goes here");
bindingContext.$data.artist("New Artist Name Here");
}
}
The changes are not reflected in the table above. Both these properties are observable.
I would like to know what exactly am I missing here?
I may as well turn my comment into an answer. I think binding handlers are better suited as a general approach to solve problems, and it pays off to avoid having a binding rely on a specific ViewModel (your binding refers to "artist" and "title"). A writeable computed observable may be more suited for the task.
Suppose the following view:
<h3>Inputs</h3>
Title: <input type="text" data-bind="value: title, valueUpdate: 'afterkeydown'" /><br />
Artist: <input type="text" data-bind="value: artist, valueUpdate: 'afterkeydown'" />
<hr />
<h3>Read only version</h3>
Title: <span data-bind="text: title"></span><br />
Artist: <span data-bind="text: artist"></span>
Notice the first input is bound to titleEditing, the computed observable. The ViewModel could be defined with these properties:
function ViewModel() {
var self = this;
var _title = ko.observable('my title');
self.title = ko.computed({
read: _title,
write: function(newval) {
_title(newval);
self.artist('New Artist Name Here');
}
});
self.artist = ko.observable('john doe');
};
Now, if you update the first input, title will change and artist will reset.
See this fiddle for a demo.

Categories

Resources