In the file "KoComponents.js", I wrote the following component
ko.components.register('pager-navigator', {
viewModel: function (params) {
var self = this;
self.GoToPageNumber = function (pageNo) {
// Raise event and with the value of the parameter pageNo.
alert('Button clicked with parameter (pageNo) = ' + pageNo);
}
},
template: '<input type="button" value="Click me and raise event" data-bind="click: function(){GoToPageNumber(1);}"/>'
});
In the view, I wrote the following to consume the created component.
<div data-bind='component: {name: "pager-navigator", params: { TotalPagesCount: 20 }}'></div>
My question is: How can I fire event in the component with parameters and consume that event from the view?
A good way to structure your code to share an observable variable between your parent page, and the component. It would be passed in with the params.
So if your parent VM had an observable called "pageNum" then make sure your component does too, and then pass the observable in to the component to link them.
This way, if the component changes the pageNum value then the observable in the parent will also change. So you can subscribe to the variable in the parent, and if it changes, you can execute some code. Essentially you are left with a situation where if the component changes the pageNum, the parent will know and can act accordingly.
It might sound long-winded, but it's a really clean solution, and it really helps you break problems down into sections and cut down on strong couplings.
Hopefully that explanation makes sense, but if you need a fiddle to demonstrate then let me know.
This is the answer:
Create an observable parameter in the page named for example "CurrentPageNumber".
Pass the observable parameter to the component.
In the component, you can change the value of the passed parameter. (act as passing by reference).
In the view, you'll need to add a subscribe method and handle your custom action when the value of CurrentPageNumber changed.
Check the sample code below.
ko.components.register('pager-navigator', {
viewModel: function (params) {
var self = this;
self.PageNumber = params.pageNumber;// pageNumber is an observable passed parameter.
self.GoToPageNumber = function (pageNo) {
self.PageNumber(pageNo);// Act as passing by reference.
}
},
template: '<input type="button" value="Click me and raise event" data-bind="click: function(){GoToPageNumber(2);}"/>'
});
In the view, write:
<div data-bind='component: {name: "pager-navigator", params: { totalPagesCount: totalPagesCount(), pageNumber: CurrentPageNumber}}'></div>
In the View JavaScript, write the following:
var self = this;
// Declare an observable variable named CurrentPageNumber with the value 1.
self.CurrentPageNumber = ko.observable(1);
self.CurrentPageNumber.subscribe(function (newValue) {
// The value of CurrentPageNumber is changed inside the component.
var newPageNo = newValue;
alert('value changed = ' + newPageNo);
self.SearchEmployees(newPageNo);
});
I've read through the Ractive Documentation and I'm scratching my head a bit, because it seems like the default events initialization option allows me to do something - create new eventtypes - far more complex than what i need but conversely, there's no hook for the simpler, (more common?) task of defining default events
Could someone advise on how to provide global events that could be fired for traditional DOM events?
Example:
I have a 3 Component application page. I want to define a getOptions event, such that any <select on-click='getOptions'>...</select> will be handled by the same function. I don't want to have to define that function in each component.
My intuition would have been to do the following:
Ractive.events['getOptions'] = function(event){
//logic for getting the options for the value in event.keypath
}
or, if i wanted a true default that could be overridden...
Ractive.default.events['getOptions'] = function(event){
//logic for getting the options for the value in event.keypath
}
but my understanding of the documentation, is that Ractive.events and Ractive.default.events do not provide this, but rather provide a way to define new event plugins, that depend on a separate mechanism for getting fired:
Ractive.events.getoptions = function(node,fire){
//here goes logic for interacting with DOM event listeners, etc
}
//and then i would need to do this
ractive = Ractive.extend({...});
ractive.on('someOtherEventName',function(event){
//logic for getting the options for the value in event.keypath
});
//and then I could do this...
<select on-getoptions='someOtherEventName'>...</select>
but what would fire the getoptions in this case - from the template, rather than js ractive.fire()?
Would something like <select on-getoptions='someOtherFunction' on-click=getoptions>...</select> work? That seems very strange to me. Do I understand the concept correction? If not, what am i missing?
Is there a simple way to achieve the first example?
Ractive.events refers to custom events for mediating between the dom and the template:
Ractive.events.banana = function( node, fire ) { ... };
<div on-banana="doSomething()"/>
The handler for the event can either be the name of an event to fire, or a method on the component instance.
In your case, I think defining a method on the Ractive.prototype would be the best way to have a common handler:
Ractive.prototype.getOptions = function( /* pass in arguments */ ){
// and/or this.event will give you access
// to current event and thus context
// you can also override this method in components and
// call this base method using this._super(..)
}
// now any ractive instance can use:
<select on-click="getOptions(data)">...</select>
An event based approach usually entails letting the root instance or common parent in the view hierarchy handle same event across child components:
var app = new Ractive({
template: "<componentA/><componentB/>",
oninit(){
this.on( '*.getOptions', ( event, arg ) => {
// any child component (at any depth)
// that fires a "getOptions" event will
// end up here
});
}
});
// in component A or B:
<select on-click="getOptions">...</select>
UPDATE: If you wanted to assign an event handler to the prototype, so in essence every component is pre-wired to handle an event of a set name, you could do:
Ractive.prototype.oninit = function(){
this.on( 'getOptions', ( event ) => {
// handle any "getOptions" event that happens in the instance
});
}
Just be aware that you must call this._super(); in any component in which you also implement oninit:
var Component = Ractive.extend({
oninit() {
// make sure we call the base or event listener won't happen!
this._super();
// do this component instances init work...
}
}
I'm working on a module in a CMS' backend. I'm trying to 'hook in' to their knockout bindings and run code when they are finished rendering. So far I've had no luck.
I have however, attached to the different data-components and obtained knockout data.
I've had many failed attempts, but so far, I have this which is returning a binding context.
var bindingContext = ko.contextFor(jQuery('div[data-component="customer_form.areas"]').get(0));
Does anyone know of a way I can use this to somehow attach an observer to watch for the rendering to finish? I'll admin, I'm new to knockout. I'm not creating the view models, nor the templates. I can't add the afterRender to the template like I think should be done.
Like you said, this should be done using afterRender. All other methods feel hacky, because you'll never know when knockout will re-render (parts of) the ui.
I can't think of a reason why you'd need such a work around, but who am I to judge..
The only approach I can think off, is to use the MutationObserver. Here's an example:
var bindingContextElement = document.querySelector("ul");
var renderThrottle = 300;
var renderCycle = null;
var onRenderComplete = function() {
var pre = document.createElement("pre");
var msg = new Date().toLocaleString() + ": finished rendering";
pre.appendChild(document.createTextNode(msg));
document.body.appendChild(pre);
}
// Observe mutations to element, call onRenderComplete after 300ms of no mutations
var observer = new MutationObserver(function(mutations) {
clearTimeout(renderCycle);
renderCycle = setTimeout(onRenderComplete, renderThrottle);
});
var config = {
childList: true
};
observer.observe(bindingContextElement, config);
ko.applyBindings({
items: ko.observableArray([1, 2, 3])
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: items">
<li data-bind="text: $data"></li>
</ul>
<button data-bind="click: function() { items.push(items().length + 1)}">add</button>
This code listens to any mutations in the <ul> element, which is controlled by knockout. Once changes start happening to the element or its children, it attempts to log a "Rendered" message. It's only allowed to log this message if there are no further changes for 300ms.
Have a look at the docs to determine your config object and which elements to watch... And please keep in mind that things might get out of hand if stuff gets more complicated than this example...
i'm rather new to Meteor and have a problem, where can't figure out how to solve it.
I want to store dates in a collection. I can pickup the place of the meeting using google maps, which gives me a String with the coordinates.
I reverse geocode the coordinates with jaymc:google-reverse-geocode which is basically working (i can console.log the results).
When using Session variables i can output the result, but they keep changing itself. The entrys get there result, then first and second entry change their result, then they change again and so on.
I tried to use ReactiveVar and ReactiveDict but with no result. I can't get any results returned from the reverseGeocode function.
Here's the code:
{{#each termine}}
<div class="listTermine">
<p class="title">{{title}}</p>
<p class="desc">{{desc}}</p>
<p class="location">{{getAddress}}</p>
<p class="dates">
<span class="glyphicon glyphicon-time" aria-hidden="true"></span>
{{formatDate startDate}} bis {{formatDate endDate}}
</p>
</div>
{{/each}}
Template.showTermine.helpers({
getAddress: function() {
var locArray = Termine.findOne({
"title": this.title
}, {
fields: {
locations: 1
}
});
latlngArray = locArray.locations.toString();
var latlong = latlngArray.split(",");
var lat = latlong[0];
var lng = latlong[1];
reverseGeocode.getLocation(lat, lng, function(location) {
Session.set('location', reverseGeocode.getAddrStr());
})
// commented out to prevent infinite loop
//return Session.get('location');
}
});
this is because a Reactive variable (like a Session variable) will cause the whole function it is included in to re-run each time it is changed.
So here you have the Session.set() in the same function as the Session.get(), so it will re-run the getAddress function each time Session.set() is called, which will re-run the thing in a loop.
Since you're returning the result in the same function, you really don't need a Session variable at all here:
you can simply:
reverseGeocode.getLocation(lat, lng, function(location) {
return reverseGeocode.getAddrStr();
})
if this doesn't work (because you're doing an asynchronous call to .getLocation), then you should do this call somewhere else
The best place to do this would be in the Template.mytemplate.onCreated() event
Template.showTermine.onCreated(function() {
var locArray = Termine.findOne({
"title": this.title
}, {
fields: {
locations: 1
}
});
latlngArray = locArray.locations.toString();
var latlong = latlngArray.split(",");
var lat = latlong[0];
var lng = latlong[1];
reverseGeocode.getLocation(lat, lng, function(location) {
Session.set('location', reverseGeocode.getAddrStr());
})});
Template.showTermine.helpers({
"getAddress": function() {
return Session.get("location");
}
});
The set is called inside the callback which will execute after the helper has returned the get already, this mean when it sets the variable the helper gets invalidated because of the change in the session value and reruns.
Couple of possible fixes:
Move code to set the var into the onCreated or onRendered methods and remove everything bar the return from your helper
Or
Ensure the session var is created as a null value initially and then add an if check to look at the value of the var before attempting to call the location service or set the var.
Reactive vars are defo the right way to go over sessions for repeated templates here to avoid namespace collisions in session and the same advice should work for reactive vars or sessions vars. Just avoid setting and getting anything reactive in the same helper without some checks to see if it is needed
What I am trying to implement is basically a "on ng repeat finished rendering" handler. I am able to detect when it is done but I can't figure out how to trigger a function from it.
Check the fiddle:http://jsfiddle.net/paulocoelho/BsMqq/3/
JS
var module = angular.module('testApp', [])
.directive('onFinishRender', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
element.ready(function () {
console.log("calling:"+attr.onFinishRender);
// CALL TEST HERE!
});
}
}
}
});
function myC($scope) {
$scope.ta = [1, 2, 3, 4, 5, 6];
function test() {
console.log("test executed");
}
}
HTML
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" on-finish-render="test()">{{t}}</p>
</div>
Answer:
Working fiddle from finishingmove: http://jsfiddle.net/paulocoelho/BsMqq/4/
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.onFinishRender);
});
}
}
}
});
Notice that I didn't use .ready() but rather wrapped it in a $timeout. $timeout makes sure it's executed when the ng-repeated elements have REALLY finished rendering (because the $timeout will execute at the end of the current digest cycle -- and it will also call $apply internally, unlike setTimeout). So after the ng-repeat has finished, we use $emit to emit an event to outer scopes (sibling and parent scopes).
And then in your controller, you can catch it with $on:
$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
//you also get the actual event object
//do stuff, execute functions -- whatever...
});
With html that looks something like this:
<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
<div>{{item.name}}}<div>
</div>
Use $evalAsync if you want your callback (i.e., test()) to be executed after the DOM is constructed, but before the browser renders. This will prevent flicker -- ref.
if (scope.$last) {
scope.$evalAsync(attr.onFinishRender);
}
Fiddle.
If you really want to call your callback after rendering, use $timeout:
if (scope.$last) {
$timeout(function() {
scope.$eval(attr.onFinishRender);
});
}
I prefer $eval instead of an event. With an event, we need to know the name of the event and add code to our controller for that event. With $eval, there is less coupling between the controller and the directive.
The answers that have been given so far will only work the first time that the ng-repeat gets rendered, but if you have a dynamic ng-repeat, meaning that you are going to be adding/deleting/filtering items, and you need to be notified every time that the ng-repeat gets rendered, those solutions won't work for you.
So, if you need to be notified EVERY TIME that the ng-repeat gets re-rendered and not just the first time, I've found a way to do that, it's quite 'hacky', but it will work fine if you know what you are doing. Use this $filter in your ng-repeat before you use any other $filter:
.filter('ngRepeatFinish', function($timeout){
return function(data){
var me = this;
var flagProperty = '__finishedRendering__';
if(!data[flagProperty]){
Object.defineProperty(
data,
flagProperty,
{enumerable:false, configurable:true, writable: false, value:{}});
$timeout(function(){
delete data[flagProperty];
me.$emit('ngRepeatFinished');
},0,false);
}
return data;
};
})
This will $emit an event called ngRepeatFinished every time that the ng-repeat gets rendered.
How to use it:
<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >
The ngRepeatFinish filter needs to be applied directly to an Array or an Object defined in your $scope, you can apply other filters after.
How NOT to use it:
<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >
Do not apply other filters first and then apply the ngRepeatFinish filter.
When should I use this?
If you want to apply certain css styles into the DOM after the list has finished rendering, because you need to have into account the new dimensions of the DOM elements that have been re-rendered by the ng-repeat. (BTW: those kind of operations should be done inside a directive)
What NOT TO DO in the function that handles the ngRepeatFinished event:
Do not perform a $scope.$apply in that function or you will put Angular in an endless loop that Angular won't be able to detect.
Do not use it for making changes in the $scope properties, because those changes won't be reflected in your view until the next $digest loop, and since you can't perform an $scope.$apply they won't be of any use.
"But filters are not meant to be used like that!!"
No, they are not, this is a hack, if you don't like it don't use it. If you know a better way to accomplish the same thing please let me know it.
Summarizing
This is a hack, and using it in the wrong way is dangerous, use it only for applying styles after the ng-repeat has finished rendering and you shouldn't have any issues.
If you need to call different functions for different ng-repeats on the same controller you can try something like this:
The directive:
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.broadcasteventname ? attr.broadcasteventname : 'ngRepeatFinished');
});
}
}
}
});
In your controller, catch events with $on:
$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});
$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});
In your template with multiple ng-repeat
<div ng-repeat="item in collection1" on-finish-render broadcasteventname="ngRepeatBroadcast1">
<div>{{item.name}}}<div>
</div>
<div ng-repeat="item in collection2" on-finish-render broadcasteventname="ngRepeatBroadcast2">
<div>{{item.name}}}<div>
</div>
The other solutions will work fine on initial page load, but calling $timeout from the controller is the only way to ensure that your function is called when the model changes. Here is a working fiddle that uses $timeout. For your example it would be:
.controller('myC', function ($scope, $timeout) {
$scope.$watch("ta", function (newValue, oldValue) {
$timeout(function () {
test();
});
});
ngRepeat will only evaluate a directive when the row content is new, so if you remove items from your list, onFinishRender will not fire. For example, try entering filter values in these fiddles emit.
If you’re not averse to using double-dollar scope props and you’re writing a directive whose only content is a repeat, there is a pretty simple solution (assuming you only care about the initial render). In the link function:
const dereg = scope.$watch('$$childTail.$last', last => {
if (last) {
dereg();
// do yr stuff -- you may still need a $timeout here
}
});
This is useful for cases where you have a directive that needs to do DOM manip based on the widths or heights of the members of a rendered list (which I think is the most likely reason one would ask this question), but it’s not as generic as the other solutions that have been proposed.
I'm very surprised not to see the most simple solution among the answers to this question.
What you want to do is add an ngInit directive on your repeated element (the element with the ngRepeat directive) checking for $last (a special variable set in scope by ngRepeat which indicates that the repeated element is the last in the list). If $last is true, we're rendering the last element and we can call the function we want.
ng-init="$last && test()"
The complete code for your HTML markup would be:
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>
You don't need any extra JS code in your app besides the scope function you want to call (in this case, test) since ngInit is provided by Angular.js. Just make sure to have your test function in the scope so that it can be accessed from the template:
$scope.test = function test() {
console.log("test executed");
}
A solution for this problem with a filtered ngRepeat could have been with Mutation events, but they are deprecated (without immediate replacement).
Then I thought of another easy one:
app.directive('filtered',function($timeout) {
return {
restrict: 'A',link: function (scope,element,attr) {
var elm = element[0]
,nodePrototype = Node.prototype
,timeout
,slice = Array.prototype.slice
;
elm.insertBefore = alt.bind(null,nodePrototype.insertBefore);
elm.removeChild = alt.bind(null,nodePrototype.removeChild);
function alt(fn){
fn.apply(elm,slice.call(arguments,1));
timeout&&$timeout.cancel(timeout);
timeout = $timeout(altDone);
}
function altDone(){
timeout = null;
console.log('Filtered! ...fire an event or something');
}
}
};
});
This hooks into the Node.prototype methods of the parent element with a one-tick $timeout to watch for successive modifications.
It works mostly correct but I did get some cases where the altDone would be called twice.
Again... add this directive to the parent of the ngRepeat.
Very easy, this is how I did it.
.directive('blockOnRender', function ($blockUI) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (scope.$first) {
$blockUI.blockElement($(element).parent());
}
if (scope.$last) {
$blockUI.unblockElement($(element).parent());
}
}
};
})
Please have a look at the fiddle, http://jsfiddle.net/yNXS2/. Since the directive you created didn't created a new scope i continued in the way.
$scope.test = function(){... made that happen.