Calling a function defined within knockout js ViewModel - javascript

I have a html 5 view and want to perform Drag and Drop. In my html I have a knockout foreach as given below. The li element is being dragged on.
<ul class="games-list" data-bind="foreach: games">
<li data-bind="text: name, attr: { 'data-dragdrop': id }"
ondragstart='setTransferProperties(event)'
draggable="true">
</li>
</ul>
and here is the javascript with knockout ViewModel
<script type="text/javascript">
var AppScope = function () {
...//plain js object here
//knockout js View Model
function PlayersViewModel() {
var self = this
self.games = ko.observableArray([
new Game({ id: 0, name: "Cricket" }),
new Game({ id: 1, name: "Football" }),
new Game({ id: 2, name: "Hockey" })
]);
...
//Drag and Drop
self.setTransferProperties = function (event) { //Not invoked
event.dataTransfer.setData("Text", event.target.getAttribute('data-dragdrop'));
};
}
}
With the above the setTransferProperties(event) is looked for in AppScope, instead of inside the knockout ViewModel, and hence not found.
What would be the way to invoke the setTransferProperties(event) defined in the knockout ViewModel when performing the Drag.

You need to do the following
<ul class="games-list" data-bind="foreach: games">
<li data-bind="text: name, attr: {
'data-dragdrop': id ,
'ondragstart':$parent.setTransferProperties
}"
draggable="true">
</li>
</ul>

Related

knockout observable array binding to view when there is a delay in assigning value not happening

I have a knockout observable array whose value assignment changes after a set value of time but do not see this reflecting in the view. Could someone tell where I am doing it wrong? I would expect the output to show
• GRE 1111
• TOEFL 111
but it shows
• GRE2 222
• TOEFL2 22
jsFiddle link: https://jsfiddle.net/4r37x9y5/
HTML:
console.clear();
function viewModel() {
this.plans = ko.observableArray([]);
var plans1 = [
{ id: 'GRE', count: '1111' },
{ id: 'TOEFL', count: '111' },
];
var plans2 = [
{ id: 'GRE2', count: '222' },
{ id: 'TOEFL2', count: '22' },
];
this.plans = plans2;
//this.plans = plans1;
setTimeout(function(){
console.log("In timeout before assigning plans");
this.plans = plans1;
console.log(this.plans);
}, 2000);
}
ko.applyBindings(viewModel());
// The above line equals:
// viewModel(); // updates window object and returns null!
// ko.applyBindings(); // binds window object to body!
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="panel panel-default">
<ul data-bind="foreach: plans" class="list-group">
<li class="list-group-item">
<span data-bind="text: id"></span>
<span data-bind="text: count"></span>
</li>
</ul>
</div>
There are couple of issues here. As mentioned by you in the comments, you are not binding an object with observables. You are simply adding a global variable plans. If knockout can't find a property in the viewModel, it will use the window object's property. That's why it works the first time
You need to change viewModel as a constructor function and use new viewModel() to create an object or an instance.
observables should be read and updated by calling them as functions. So, this.plans(plans1). If you set this.plans = plans2, it will simply overwrite the observable with a simple array without the subscribers to update the UI when the property changes
You need to use correct this inside setTimeout. Either by creating a self = this variable outside or using an arrow function as a callback
function viewModel() {
this.plans = ko.observableArray([]);
var plans1 = [{ id: "GRE", count: "1" }, { id: "TOEFL", count: "1" }];
var plans2 = [{ id: "GRE2", count: "2" }, { id: "TOEFL2", count: "2" }];
this.plans(plans2) // call it like a function
setTimeout(() => {
console.log("In timeout before assigning plans");
this.plans(plans1)
}, 2000);
}
ko.applyBindings(new viewModel()); // new keyword to create an object
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<ul data-bind="foreach: plans">
<li>
<span data-bind="text: id"></span>
<span data-bind="text: count"></span>
</li>
</ul>

Edit not working for newly added data - knockout

I have been following some tutorials and trying to follow knockout.js. I am not able to edit any data that is newly added.
My JS Code:
var data = [
new ListData("Programmer", 1),
new ListData("Testing", 2),
new ListData("DBA", 3),
new ListData("Lead", 4),
new ListData("Manager", 5)
];
function ListData(desig, id) {
return {
Desig: ko.observable(desig),
Id: ko.observable(id)
};
}
var viewModel = {
list: ko.observableArray(data),
dataToAdd: ko.observable(""),
selectedData: ko.observable(null),
addTag: function () {
this.list.push({ Desig: this.dataToAdd() });
this.tagToAdd("");
},
selecData: function () {
viewModel.selectedData(this);
}
};
$(document).on("click", ".editData", function () {
});
ko.applyBindings(viewModel);
My View Code:
<input type="text" data-bind="value: dataToAdd" />
<button data-bind="click: addData">
+ Add
</button>
<ul data-bind="foreach: list">
<li data-bind="click: $parent.selecData">
<span data-bind="text: Desig"></span>
<div>
Edit
</div>
</li>
</ul>
<div id="EditData" data-bind="with: selectedData">
Designation:
<input type="text" data-bind="value: Desig" />
</div>
I am able to edit the data which already exists like - Programmer, Testing, DBA...but if I add a new data..I am not able to edit. Please assist.
Your addData (you named it addTag in the code, but call addData in the HTML) function doesn't construct new elements the same way your initial data creation does. Note: since your ListData constructor explicitly returns an object, new is superfluous.
addData: function () {
this.list.push(ListData(this.dataToAdd(), ""));
},

Nested lists in Angular JS

I am trying to create JSON to use for a nested list.
I assume I am using incorrect syntax on the second ng-repeat:
<ul>
<li ng-repeat="n in navData">Label = {{n.label}}</li>
<ul>
<li ng-repeat="n.children in n">Child label = {{p.label}}</li>
</ul>
</ul>
When my data model is
var nav = [{
label: 'Pages',
value: 'pages',
children: [{
label: 'Home',
value: 'home'
}, {
label: 'Left Nav',
value: 'left-nav'
}]
}, {
label: 'Components',
value: 'components'
}];
All I am getting in the html is the top level:
Label = Pages
Label = Components
Outer li tag should close after inner li tag with ng-repeat. And you should use different var name and slightly different expression (actually you have name your var as p {{p.label}} somewhere but not in nested loop):
<ul>
<li ng-repeat="n in navData">Label = {{n.label}}<!-- closing li removed //-->
<ul>
<li ng-repeat="p in n.children">Child label = {{p.label}}</li>
</ul>
</li> <!-- closing li tag added //-->
</ul>

Dynamic knockout template with transition

I'm working on a list which is rendered with a template binding. The items have a collapsed and expanded view which is decided by an observable property on the individual items. This is done by providing a function to the template name (just like in the knockout docs). So far so good, everything is well so far.
Now.. to the problem. I want to animate the transition when changing templates. So far I have manage to animate the "In-transition" (with the afterRender event) i.e when the new template is loaded. But I also want to make an "Out-transition" for the old template before it is removed.
This is how far I am now.
http://jsbin.com/UvEraGO/15/edit?html,js,output
Any idea of how I can implement this "out-transition" ?
Here is the code:
[viewmodel.js]
var vm = {
items: [{name: 'John', age:'34', expanded: ko.observable(false)},
{name: 'David', age:'24', expanded: ko.observable(false)},
{name: 'Graham', age:'14', expanded: ko.observable(false)},
{name: 'Elly', age:'31', expanded: ko.observable(true)},
{name: 'Sue', age:'53', expanded: ko.observable(false)},
{name: 'Peter', age:'19', expanded: ko.observable(false)}]
};
vm.myTransition = function(el){
$(el[1]).hide().slideDown(1000);
};
vm.templateSelector = function(item){
return item.expanded() ? 'expanded_template' : 'collapsed_template';
}.bind(vm);
vm.toggleTemplate = function(item){
item.expanded(!item.expanded());
};
ko.applyBindings(vm);
And the html:
<div data-bind="template: { name: templateSelector, foreach: items, afterRender: myTransition }"></div>
<script type="text/html" id="collapsed_template">
<div style="min-height: 30px">
<strong>Name: <span data-bind="text: name"></span></strong>
<button data-bind="click: $parent.toggleTemplate">Expand</button>
<div>
</script>
<script type="text/html" id="expanded_template">
<fieldset style="height: 100px; min-height: 8px">
<legend>
<strong>Name: <span data-bind="text: name"></span></strong>
</legend>
<div>
Age: <span data-bind="text: age"></span>
<button data-bind="click: $parent.toggleTemplate">collapse</button>
</div>
</fieldset>
</script>
A thought would be to create something like a slideTemplate binding and use that inside of your template. It would look something like:
ko.bindingHandlers.slideTemplate = {
init: ko.bindingHandlers.template.init,
update: function(element, valueAccessor, allBindings, data, context) {
//ensure that we have a dependency on the name
var options = ko.unwrap(valueAccessor()),
name = options && typeof options === "object" ? ko.unwrap(options.name) : name,
$el = $(element);
if ($el.html()) {
$el.slideUp(250, function() {
ko.bindingHandlers.template.update(element, valueAccessor, allBindings, data, context);
$el.slideDown(1000);
});
}
else {
ko.bindingHandlers.template.update(element, valueAccessor, allBindings, data, context);
}
}
};
Then, you would bind something like:
<ul data-bind="foreach: items">
<li data-bind="slideTemplate: type">
</li>
</ul>
Sample: http://jsfiddle.net/rniemeyer/6J67k/

How do I trigger the manipulation of the DOM after a partial view is loaded in AngularJS?

How do I trigger the manipulation of the DOM after a partial view is loaded in AngularJS?
If I were using jQuery, I could do
$(document).ready(function(){
// do stuff here
}
But in Angular, in particular with partial views, how would I do such? As a more concrete example, I have the following basic non-interactive Angular app (html and js on the same page source):
http://cssquirrel.com/testcases/ang-demo/
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Angular Question</title>
</head>
<body data-ng-app="demoApp">
<div data-ng-view=""></div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
<script>
var app = angular.module('demoApp', []);
var controller = {
demoController: function ($scope, demoFactory) {
$scope.fruits = demoFactory.getFruits();
}
};
var factory = {
demoFactory: function () {
var fruits = ['apples', 'bananas', 'cherries'];
var factory = {
getFruits: function () {
return fruits;
}
};
return factory;
}
}
function appRoute($routeProvider) {
$routeProvider
.when('/step-1',
{
controller: 'demoController',
templateUrl: 'partial.html'
})
.otherwise({ redirectTo: '/step-1' });
};
app.config(appRoute);
app.factory(factory);
app.controller(controller);
</script>
</body>
</html>
Which has the following partial:
http://cssquirrel.com/testcases/ang-demo/partial.html
<ul>
<li data-ng-repeat="fruit in fruits">{{fruit}}</li>
</ul>
So, if in this basic app, I wanted to add a class "active" to the first list item in the list after the partial view has finished loading, how would I go about it?
You have to stop thinking in terms of DOM manipulation. It's not the first LI that's active, rather it's the first fruit that has been selected.
First up, support the concept of a fruit being selected
var fruits = [
{ name: 'apples', active: true },
{ name: 'bananas', active: false },
{ name: 'cherries', active: false }
]
Then, support that attribute with an ng-class in your angular template:
<ul>
<li data-ng-repeat="fruit in fruits" ng-class="{ active: fruit.active }">{{fruit.name}}</li>
</ul>
Now you can manipulate your fruits array and change which one is selected, for example:
$scope.fruits[2].active = true;
AngularJS is model driven. If you want to change DOM, then change data instead.
You can use $first property to activate the first item of the repeater.
<ul>
<li ng-class="{active : $first}" data-ng-repeat="fruit in fruits">{{fruit}}</li>
</ul>
Or if you want to manually activate any item of the repeater by clicking on it, you can change the activate field of the model object.
<ul>
<li ng-class="{true: 'active', false: ''}[fruit.active]" ng-repeat="fruit in fruits" ng-click="activate(fruit)">{{fruit.name}}</li>
</ul>
Use this data structure
var factory = {
demoFactory: function () {
var fruits = [{
name: 'apples',
active: true
}, {
name: 'bananas',
active: false
}, {
name: 'cherries',
active: false
}]
var factory = {
getFruits: function () {
return fruits;
}
};
return factory;
}
}
And add this in the controller.
$scope.activate = function (fruit) {
console.log(fruit)
fruit.active = true;
}
DEMO
You would set an active property on that model and check for adding it to every element.
var controller = {
demoController: function ($scope, demoFactory) {
$scope.fruits = demoFactory.getFruits();
$scope.fruits[0].isActive = true; // kinda hacky
}
};
<ul>
<li data-ng-repeat="fruit in fruits" ng-class="{ active: fruit.isActive }}">{{fruit}}</li>
</ul>
This isn't exactly how I would do it, but with angular it is always best to edit the model so that you maintain 1 single definitive representation of the state of your application.

Categories