I extend a Control to create a new custom control in UI5 and this control renders a tree as UL items nicely. Now I need to implement a collapse/expand within that tree. Hence my renderer writes a tag like
<a class="json-toggle" onclick="_ontoggle"></a>
and within that _ontoggle function I will handle the collapse/expand logic.
No matter where I place the _ontoggle function in the control, I get the error "Uncaught ReferenceError: _ontoggle is not defined"
I am missing something obvious but I can't find what it is.
At the moment I have placed a function inside the
return Control.extend("mycontrol",
{_onToggle: function(event) {},
...
Please note that this event is not one the control should expose as new event. It is purely for the internals of how the control reacts to a click event.
I read things about bind and the such but nothing that made sense for this use case.
Took me a few days to crack that, hence would like to provide you with a few pointers.
There are obviously many ways to do that, but I wanted to make that as standard as possible.
The best suggestion I found was to use the ui5 Dialog control as sample. It consists of internal buttons and hence is similar to my requirement: Render something that does something on click.
https://github.com/SAP/openui5/blob/master/src/sap.ui.commons/src/sap/ui/commons/Dialog.js
In short, the solution is
1) The
<a class="json-toggle" href></a>
should not have an onclick. Neither in the tag nor by adding such via jQuery.
2) The control's javascript code should look like:
sap.ui.define(
[ 'sap/ui/core/Control' ],
function(Control) {
var control = Control.extend(
"com.controlname",
{
metadata : {
...
},
renderer : function(oRm, oControl) {
...
},
init : function() {
var libraryPath = jQuery.sap.getModulePath("mylib");
jQuery.sap.includeStyleSheet(libraryPath + "/MyControl.css");
},
onAfterRendering : function(arguments) {
if (sap.ui.core.Control.prototype.onAfterRendering) {
sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments);
}
},
});
control.prototype.onclick = function (oEvent) {
var target = oEvent.target;
return false;
};
return control;
});
Nothing in the init(), nothing in the onAfterRendering(), renderer() outputs the html. So far there is nothing special.
The only thing related with the onClick is the control.prototype.onclick. The variable "target" is the html tag that was clicked.
I have a parent Vue which enables or disables "edit" mode. In non-edit mode all components are read only.
I've implemented this via a data object and all works fine.
I've split out some of the components in child components.
From the parent an $emit message is sent with the new edit mode state:
methods: {
toggleMode () {
this.editMode = !this.editMode
this.$emit('edit-mode-change', this.editMode)
}
Using Vue DevTools I can see the message is emitted.
However, I can't seem to receive it in my child component!I've looked a the docs, but none of the examples match this case. This is what I have currently (in the child component):
methods: {
onEditModeChange: function (mode) {
console.log('mode is', mode)
this.editMode = mode
}
Also tried:
events: {
onEditModeChange: function (mode) {
console.log('mode is', mode)
this.editMode = mode
}
Plus I'm getting an browser console error as follows:
[Vue warn]: Invalid handler for event "edit-mode-change": got false
(found in <Dimensions> at /home/anthony/Projects/Towers-Vue/src/components/assets/Dimensions.vue)
I'm sure I'm doing something basic wrong, but the docs don't reference the events: {} block, yet I've seen it on other code. Nor does it show how to implement a listener.
Thanks for taking the time to view this post, if you can point me in the right direction, it's much appreciated.
In Vue 2, events only flow laterally or up, not down.
What you really want is to simply pass a prop into your components.
In the parent JS:
toggleMode () {
this.editMode = ! this.editMode;
}
In the parent template:
<child-component-1 :editMode="editMode"></child-component-1>
...same for others...
Then simply accept editMode as a prop in each of your child components:
{
props: ['editMode']
}
You can now use editMode within your child's template. It'll track the parent's editMode, so no need for manual events/watchers.
The way vue2 works is by having a one-direction flow of the data, from parent to child, so in your parent component you can have
<template>
<child-component :isEditing="editMode"></child-component>
</template>
<script>
export default {
methods: {
toggleMode () {
this.editMode = !this.editMode
this.$emit('edit-mode-change', this.editMode)
}
}
}
and in child component you use props to get the data
Vue.component('child-component', {
props: ['isEditing'],
template: '<span>edit mode: {{ isEditing }}</span>'
})
we have cover the edit mode for the child. now if you want to send data from child to parent, then child needs to "emit" a signal to the parent, as props are "read only"
in child component you do at any point
someMethod() {
this.$emit('editDone', dataEdited);
}
and in your parent component you "intercept" the message using on:
<template>
<child-component
:isEditing="editMode"
#editDone="someParentMethod"></child-component>
</template>
Greetings!
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.
I have really weird problem. I am trying to implement "root" view which also works as some namespace structure. Same principle introduced in codeschool.com course part II. In this root view, I want to catch event "click button", but that's the problem. When I click on button nothing happened.
window.App = new (Backbone.View.extend({
el: $("#app"),
Collections: {},
Models: {},
Views: {},
Routers: {},
events: {
'click button' : function(e) {
alert("Thank god!");
}
},
render: function(){
//for test purposes
console.log($("#app").find("button"));
console.log(this.$el.find('button'));
},
start: function(){
this.render();
new App.Routers.PostsRouter();
Backbone.history.start({pushState: true});
}
}))();
$(document).ready(function() { App.start() });
The HTML look like this
<body>
<div id="app">
<div id="posts"></div>
<button>Click me</button>
</div>
</body>
And what's really weird is output from console.log in render function. Both selectors are same, even the context is same, so where is problem?
console.log($("#app").find("button")); //this returns correct button
console.log(this.$el.find('button')); //this returns 0 occurences (WTF?)
EDIT:
After little change at el: "#app", still same problem. Problem was (thanks to #nemesv) in instantiating this class before DOM is loaded. But however, it's not possible to instantiating after DOM is loaded, because then it's not possible to use that namespace structure (eg. App.Model.Post = Backbone.Model.extend() ). But this "main view with namespace structure" is introduced in codeschool.com course as some sort of good practice. Solution can be found there http://jsfiddle.net/BckAe
You have specified your el as a jquery selector but because you are inside an object literal it evaluates immediately so before the DOM has been loaded.
So the el: $("#app"), won't select anything.
You can solve this by using one of the backbone features that you can initilaize the el as a string containing a selector.
So change your el declaration to:
el: "#app"
Your click event is not triggered because you instantiate your view before the DOM is loaded so backbone cannot do the event delegation your you.
So you need separate your view declaration and creation into two steps. And only instantiate your view when the DOM is loaded:
var AppView = Backbone.View.extend({
el: "#app",
//...
});
$(document).ready(function()
{
window.App = new AppView();
App.start()
});
Demo: JSFiddle.
Normally ng-model updates bound model each time user pushes the key:
<input type="text" ng-model="entity.value" />
This works great in almost every case.
But I need it to update when onchange event occurs instead when onkeyup/onkeydown event.
In older versions of angular there was a ng-model-instant directive which worked same as ng-model works now (at least for the user - i don't know anything about their implementations).
So in older version if I just gave ng-model it was updating the model onchange and when I specified ng-model-instant it was updating the model onkeypup.
Now I need ng-model to use on "change" event of the element. I don't want it to be instant. What's the simplest way of doing this?
EDIT
The input still has to reflect any other changes to the model - if the model will be updated in other place, value of the input should reflect this change.
What I need is to have ng-model directive to work just like it worked in the older versions of angularjs.
Here is an explanation of what I'm trying to do:
http://jsfiddle.net/selbh/EPNRd/
Here I created onChange directive for you. Demo: http://jsfiddle.net/sunnycpp/TZnj2/52/
app.directive('onChange', function() {
return {
restrict: 'A',
scope:{'onChange':'=' },
link: function(scope, elm, attrs) {
scope.$watch('onChange', function(nVal) { elm.val(nVal); });
elm.bind('blur', function() {
var currentValue = elm.val();
if( scope.onChange !== currentValue ) {
scope.$apply(function() {
scope.onChange = currentValue;
});
}
});
}
};
});
See also: the AngularJS ngChange directive.
When applied to an <input> the changes occurs after each key press not on the blur event.
http://docs.angularjs.org/api/ng.directive:ngChange
Angularjs: input[text] ngChange fires while the value is changing : This answer provides a much better solution that allows the custom directive to work with ngModel so you can still use all of the other directives that go along with ngModel.
Also, an even more flexible solution that allows for specifying the event to use (not just blur) and other properties should be built in to angular very soon: https://github.com/angular/angular.js/pull/2129
I'm not sure if there is a better way to do this, but you can achieve this using a custom directive (on any jquery event you want)
<input type="text" ng-model="foo" custom-event="bar" />
<p> {{ bar }} </p>
// create the custom directive
app.directive('customEvent', function() {
return function(scope, element, attrs) {
var dest = attrs.customEvent;
$(element[0]).on('any-jquery-event', function(e) {
e.preventDefault();
// on the event, copy the contents of model
// to the destination variable
scope[dest] = scope.foo;
if (!scope.$$phase)
scope.$apply();
});
}
});