I'm trying to specify an entrance effect on elements being inserted using a knockoutjs foreach binding. Very simple setup:
myViewModel.myObservableArray.push({enter:function() { ... });
and in the markup:
foreach:{data:myObservableArray, afterRender:enter}
seems like it should work... right? But it doesn't find the enter function on the item. What I've found does work is:
myViewModel.enter = function(something, item) { item.enter(); };
foreach:{data:myObservableArray, afterRender:$root.enter}
adding an enter function to the root view model and binding afterRender to $root.enter. Enter is then passed the item as its second param so can in turn call the item's enter function, but it feels like a hack.
Can anyone explain what's going on here?
Thanks.
EDIT:
To clarify I've created a fiddle.
What this does is very simple, and is covered in more depth in the animated transitions example. It's running a function in the root view model for each dom element that's inserted using the foreach binding.
So the question is: what if I want item specific afterRender, afterAdd or beforeRemove functions? I could see this being useful. Especially if using the template binding to dynamically select a template (note 4). Is there a clean way of doing this? Right now I've got an enter function in the view model's root that simply calls the enter function on the item, but like I said above this feels like a hack.
Nope, this is the way it was designed.
From the Documenation:
Note 3: Using “afterRender”, “afterAdd”, and “beforeRemove”
Sometimes you might want to run custom post-processing logic on the DOM elements generated by your templates. For example, if you’re using a JavaScript widgets library such as jQuery UI, you might want to intercept your templates’ output so that you can run jQuery UI commands on it to transform some of the rendered elements into date pickers, sliders, or anything else.
Generally, the best way to perform such post-processing on DOM elements is to write a custom binding, but if you really just want to access the raw DOM elements emitted by a template, you can use afterRender.
Pass a function reference (either a function literal, or give the name of a function on your view model), and Knockout will invoke it immediately after rendering or re-rendering your template.
(Emphasis mine)
As it says, a custom binding is another way to do it, and may be better depending on what that enter() function does.
underscore debounce (_.debounce) is a great solution in such case.
template
data-bind=" template: {foreach:myObservableArray, afterRender: runWhenAllRenderDone }
debounce function will be executed if afterRender is not fired in last 100 milisecond.
var runWhenAllRenderDone = _.debounce(myFunction, 100);
function myFunction(){
//do some task but do it for once only
}
is't it awesome?
Found another workaround without timeout, this technique is based on virtual element <!-- ko if: $parent.afterRender($index()) --><!-- /ko -->
function ReservationsViewModel() {
// Editable data
this.seats = ko.observableArray([
{ name: "Steve", meal: "Standard (sandwich)", price: 343},
{ name: "Steve", meal: "Premium (lobster)", price: 10},
{ name: "Steve", meal: "Ultimate (whole zebra)", price: 290}
]);
this.afterRender = (i) => {
// checking element rendered is last
if (i === this.seats().length - 1) {
console.log('rendered');
// our after rendered logic goes here...
}
};
}
And it's template is
<tbody data-bind="foreach: seats">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: meal"></td>
<td data-bind="text: price"></td>
</tr>
<!-- ko if: $parent.afterRender($index()) --><!-- /ko -->
</tbody>
This extra logic i === this.seats().length - 1, will check last row is rendered.. then we can execute our afterRender logic inside.
Related
How is it possible to show changes of existing items in a dom-repeat template in polymer?
i tried really all i could think of and i could find in the polymer documentation or in the web. but nothing works. below you find a html-page that uses a small list and tries to change one entry in the list to another value when you click the change button. But the only thing that would change the item in the list next to the change-button is the line that is commented out. all the other lines try it, but fail.
i understand that re-rendering a template is a time-consuming task and that it only should take place when it is necessary and that polymer tries to avoid it as much as possible. but why is it not possible for me (from the view of the code the god of their world ^^) to force a render on purpose?
the method of creating a complete new object, deleting the old item from the list and inserting the new object (thats what the commented line does) is a workaround, but it is a really huge effort, when the items are more complex and have properties or arrays that are not even displayed.
What am i missing? What did i not try? I would be very glad if anybody could tell me what i could do to achieve such a (from my point of view) simple and very common task.
EDIT (solved):
the solution of Tomasz Pluskiewicz was partly successful. but i updated the code to show my current problem. the name of the item is bound using the method format(...). when a button is clicked the item will be removed from the list. this works good. but if you remove the last item of the list, then the new last item in the list should get the name "Last". this also works, when the name is bound directly to the property name. but if i want to do some formatting of the name (surround it with # for example) then the display of this item is not updated.
EDIT2 (partially solved):
The next example that doesn't work, occurs when a value inside the method that is called for displaying a value changes. This can be seen in the example if a change-button is clicked multiple times. It increases an internal counter and the corresponding text will display this value. But this is only true for the first change of the counter. Subsequent clicks won't change the display again. The display shows the value of the counter after the first click. But if another change button is clicked, then the text in front of this button shows the increased counter value. But also only once. It also doesn't display changes on subsequent clicks. The notifyPath-method seems to check if the value changed, but doesn't consider that inside the method that is used for displaying the value, something could have been changed to show the data in another way.
i included a partial solution in this example. If the method that gets called has a parameter that changes when something in the method is changed, then the update will be executed. This can be seen in the second variable that is bound with the parameter displayEnforcer - format(item.name,displayEnforcer). This variable is set to a random value everytime the counter is changed. And this triggers the display update.
But this is a really strange solution and should not be necessary. it would be great if someone has a better solution to this problem.
<link rel="import" href="components/bower_components/polymer/polymer.html">
<link rel="import" href="components/bower_components/paper-button/paper-button.html">
<dom-module id="polymer-test">
<template>
<table>
<template id="tpl" is="dom-repeat" items="{{list}}">
<tr>
<td>{{item.id}} - {{format(item.name)}}- {{format(item.name,displayEnforcer)}}</td>
<td><paper-button raised on-tap="tapDelete">delete</paper-button></td>
<td><paper-button raised on-tap="tapChange">change</paper-button></td>
</tr>
</template>
</table>
</template>
</dom-module>
<script>
Polymer(
{
is: "polymer-test",
properties:
{
count: {type: Number, value:0}
,list: {type: Array, value: [{id:0,name:"First"}
,{id:1,name:"Second"}
,{id:2,name:"Third"}
,{id:3,name:"Fourth"}
,{id:4,name:"Fifth"}
,{id:5,name:"Last"}
]}
,displayEnforcer: {type:Number,value:Math.random()}
},
format: function(name,dummy)
{
return "#" + name + " - " + this.count + "#";
},
tapChange: function(e)
{
this.count++;
this.displayEnforcer = Math.random();
this.notifyPath("list." + (e.model.index) + ".name","changed");
},
tapDelete: function(e)
{
if(this.list.length == 1)
return;
this.splice("list",e.model.index,1);
if(this.list.length > 0)
this.list[this.list.length-1].name = "Last";
this.notifyPath("list." + (this.list.length-1) + ".name",this.list[this.list.length-1].name);
}
});
</script>
You can use notifyPath to refresh binding of single list element's property:
tapChange: function(e) {
this.notifyPath('list.' + e.model.index + '.name', 'changed');
}
See: https://github.com/Polymer/polymer/issues/2068#issuecomment-120767748
This way does not re-render the entire repeater but simply updates the necessary data-bound nodes.
EDIT
Your format function is not getting updated because it doesn't use the notified path which is an item's name. When you remove an element, the index of that element within the rendered repeater doesn't change, even though the actual item changes. In other words, when you remove fifth element, the index 4 is still 4.
As a workaround you can add item.name to format call so that it is refreshed when you notify that path:
<td>{{item.id}} - {{format(index, item.name)}}</td>
You don't even have to use that value in the example. It's enough that the binding is reevaluated.
I am using Angularjs 1.5.3 I have 2 services one service calls Area names, the other calls the details for the Area.
So in my code, I call the first service to get the Area, then I set the ng-init to call the details. This works fine, however angular keeps only the first value for all the rows.
Here is the code;
<tbody data-ng-repeat="area in vm.Areas" ng-init='vm.getDetails(area)'>
<tr>
<td class="text-bold">{{area}}</td>
<td>{{vm.AreaDetails.Employees}}</td>
<td>{{vm.AreaDetails.Hours}}</td>
<td>{{vm.AreaDetails.Sales}}</td>
</tr>
</tbody>
Any ideas on fixing this?
Thanks
You should avoid using ng-init for this. It's an abuse of ng-init and decrease your performance drastically. See: ngInit. Try to get your details before you start rendering eg (pseydo):
vm.areas = vm.areas.map(function(area) {
return area.details = service.getDetails(area);
}
#TJ answer is right on the technical part however i think you have a design problem in your code.
If you want to load area and their details you should load all of them in one go.
Instead you'll load them one by one there.
So let's say you have 10 Area and you're Detail service load data from (i suppose) the server : that makes 11 requests : 1 for all area, 10 for details of each area.
So just load all the whole thing in one call to your service (and presumably the server) and perform a simple ng-repeat.
You can simply have the controller iterate over the areas and call getDetails for each of them and append the detail to the respective area when they arrive.
The bindings will be along:
<tbody data-ng-repeat="area in vm.Areas">
<tr>
<td class="text-bold">{{area}}</td>
<td>{{area.details.Employees}}</td>
<td>{{area.details.Hours}}</td>
<td>{{area.details.Sales}}</td>
</tr>
</tbody>
The bindings will be updated when the data arrive.
Or you can use a directive with isolated scope, something like the following:
angular.module('yourModule').directive('areaInfo', function() {
return {
scope: {
area: '=areaInfo'
},
require: "yourController", // conroller where getDetails is defined
templateUrl: "area-info.html",
link: function(scope, element, attrs, ctrl) {
scope.areaDetails = ctrl.getDetails(scope.area);
}
}
});
<script type="text/ng-template" id="area-info.html">
<tr>
<td class="text-bold">{{area}}</td>
<td>{{areaDetails.Employees}}</td>
<td>{{areaDetails.Hours}}</td>
<td>{{areaDetails.Sales}}</td>
</tr>
</script>
<tbody data-ng-repeat="area in vm.Areas" area-info="area"></tbody>
You can even move the getDetails method to the directive itself.
Im new to knockoutJS and really loving it. I'm trying to build something very similar to this http://jsfiddle.net/mac2000/N2zNk/light/. I tried copying the code and adapting it to my need. The problem with that is that I get my data from the server using $.getJSON it seems that the jsfiddle example was made for a different format of data which just confuses me.
So instead of asking for help to find the issue with my code I rather take a different approach. Hopefully you guys wont mind. Im starting from scratch and trying to learn each steps so I know what im doing.
Here is my code so far, this works great to simply display my data on my table.
<script type="text/javascript">
function EmployeeModal() {
var self = this;
self.Employees = ko.observableArray([]);
$.getJSON("../../../../_vti_bin/listData.svc/GDI_PROD_Incidents?$filter=ÉtatValue%20ne%20%27Fermé%27&$orderby=PrioritéValue desc",
function (data) {
if (data.d.results) {
self.Employees(ko.toJS(data.d.results));
}
}
);
}
$(document).ready(function () {
ko.applyBindings(new EmployeeModal());
});
</script>
I made a template where each row has an edit button similar to the example but no fucntion of binding are done yet. Now what I would like to do is simply onlick pass the selected data to my modal and show my modal like so:
$('#myModal').modal('show');
This is the step im struggling the most with. Would any have any clear documentations for a noob or example, hints or any type of help I would take to get me in the right direction from here.
Assume you have them in a list like this:
<ul data-bind="foreach: Employees">
<li data-bind="text: fullName, click: showEmployee"/>
</ul>
What I'd recommend is to update your view model to look like this:
function EmployeeModal() {
var self = this;
self.Employees = ko.observableArray([]);
self.currentEmployee = ko.observable(null);
self.showEmployee = function(vm){
self.currentEmployee(vm);
$('#myModal').modal('show');
};
.... // rest of your view model here
}
The last piece will be using KO's with binding to wrap your modal
<div class="modal" id="myModal" data-bind="with: currentEmployee">
<h1 data-bind="text: fullName"></h1>
</div>
What this does is listen for the click event on an individual element and automatically pass the view model bound to that element to the click handler you defined. We're then taking that view model, storing it in its own observable and then showing the modal like normal. The key here is the with binding which only executes when there's data stored in the observable, and also sets the context of all nested code to be the employee stored in currentEmployee.
There's a lot there if you're new, but using a current style observable to track individual data from a list is a great paradigm to use with Knockout = )
P.S.. the http://learn.knockoutjs.com/ tutorials are really great to work through if you've yet to do so.
I've created a reusable grid component via knockout and I'm finding that my html is becoming very bloating with data-bind="..." strings particularly for <td> elements.
I could have a grid with 8 columns and a mere 20 rows which would yield 160 cells. The issue is all my cells look like this:
<td data-bind="text: typeof rowText == 'function' ? rowText($parent) : $parent[rowText],
event: { dblclick: function() { $root.rowDoubleClicked($parent); } },
css: $data.columnClass">Yale University</td>
I may even add future bindings.
It'd be nice if there was a way to perhaps apply a binding to <tbody> that would automatically apply a binding to it's child <td> elements. Or perhaps there is a way to apply bindings via javascript instead of using the "data-bind" attribute?
Normally the only one looking at the code is you (developer), so what it looks like is unimportant in the grand scheme of things. You could just format your markup to make it easier to follow:
<td data-bind="
text: typeof rowText == 'function' ? rowText($parent) : $parent[rowText],
event: { dblclick: function() { $root.rowDoubleClicked($parent); } },
css: $data.columnClass">Yale University</td>
your text binding could be cleaned up by pushing back that check to the viewmodel, perhaps using a computed that does that check for you.
Knockout templates may help "pretty up" your markup, but it's really just moving it around anyways.
I have an observable array that I add or remove elements to. The elements are displayed as DIVs.
I want to make each DIV draggable, however because the DIV is created on the fly I am not sure how to do this. I was thinking of using the JQuery live() function, but I need to pass and action, so I dont think this would be the right approach.
This is my code:
Knockout:
function AssetViewModel() {
var self = this;
self.assets = ko.observableArray([]);
self.addAsset = function(){
self.assets.push(
{
id: "1",
content: "Hello World",
type: "Asset"
}
);
}
self.removeAsset = function(asset){
self.assets.remove(asset);
};
};
HTML:
<div id="layer1" data-bind="foreach: assets">
<div data-bind="text: content" class="asset"></div>
</div>
Any suggestions would be greatly appreciated!
This is a job for custom bindings. The gist is you create your own binding so you markup looks like this:
<div data-bind="foreach: assets">
<div data-bind="draggable: $data">
<p>More markup</p>
</div>
</div>
The custom binding would be:
ko.bindingHandlers.draggable = {
init: function (element, valueAccessor, allBindingsAccessor, vieModel, bindingContext){
$(element).draggable();
return ko.bindingHandlers.with.init.apply(this, arguments);
}
};
You don't need to return anything but by calling the with binding init function, you create a wrapper binding that performs your logic and and acts like the binding you're returning. It's generally a good place to start when learning custom bindings.
Try to read next article - Revisiting Dragging, Dropping, and Sorting observableArrays
It contains detailed explanation and number of samples.
I'm unsure whether you just want to add sorting within your layer1 div (or dragging and dropping amongst other dom elements.) Regardless of what you want I'd use the Knockout-Sortable plugin (https://github.com/rniemeyer/knockout-sortable).
It worked for me on a complex sort, drag and drop solution with Knockout (named templates nested multiple layers.)
Check the JSFiddles at the bottom of that page to get started.
Hope that helps.