I am trying to create some event triggers to change the position of an Angular Carousel (Github repo).
See the plunker for a working version
The expected behavior is that you click the text that says "Go to position: 0" and the Carousel will change index position to 0.
This is accomplished using two-way binding on the Carousel: rn-carousel-index="carouselIndex"
This scope value is passed to a directive by setting: carousel-index="carouselIndex"
A controller method to modify carouselIndex is also passed to the directive with: index-model="indexModel(value)"
The directive takes the scopes and has a function bound to the text that should change the Carousel position:
app.directive('timeline', function () {
return {
replace:true,
scope:{
indexModel:'&',
carouselIndex:'='
},
link:function (scope, element, attrs) {
var valueto = 0;
var textElement = d3.select(".timeLine").text('Go to position: ' + valueto)
textElement
.on('click',clickMe)
function clickMe(d){
console.log('click');
console.log("but index is: " + scope.carouselIndex);
scope.carouselIndex = valueto;
// scope.$apply(carouselIndex) = valueto;
scope.indexModel({value: valueto});
//alert(d);
}
}
}
})
As you can see, when you click, the watcher on $carouselIndex does not always change. Moreover, and more importantly, the Carousel behavior to change position is not working.
$scope.$watch('carouselIndex', function(a,b) {
console.log('change detected');
console.log('Index really is: ' + $scope.carouselIndex);
})
Adding a digest() $apply() function solved the problem.
$scope.indexModel = function(slide) {
$scope.$apply(function() {
console.log('change slide ' + slide);
var changeSlide = parseInt(slide);
$scope.carouselIndex = changeSlide;
console.log('Index should be: ' + $scope.carouselIndex);
)}
};
For the problem with the watcher on $scope.carouselIndex, try what the Docs say
Note: If the indicators don't seem to update with the slides, try binding to an object param i.e. carousel.index, set in the controller like $scope.carousel.index = 0
https://github.com/revolunet/angular-carousel
Related
I am newer to AngularJS and having an issue that I hope someone can point me in the right direction to figuring out. I have created a directive called sizeWatcher which is placed as an attribute into the HTML which essentially just gets the height of the element it's placed on and echos that height into a scope variable named style that I set onto another element through the use of ng-style="style".
I'm finding that whenever I open my accordion, the $watch fires on the directive but it's firing multiple times. I have a console.log in my $watch and am seeing 3 log entries, the first 2 are the same (guessing this happens on click before the accordion opens, and then the accordion opens and the 3rd log entry is the final height after the accordion is opened). The main issue is that the style variable is only getting set to the smaller heights before the accordion is expanded even though the log is registering the greater height as the last time the directive is hit -- How can I ignore the first $watch event firings and only act accordingly on the last and final run-through of the directive? Any insight into this would be greatly appreciated. Relevant code attached below:
TEMPLATE:
<div class="content-wrap" id="height-block" ng-style="style">
<!-- Other HTML etc... -->
<uib-accordion size-watcher close-others="oneAtATime">
<!-- Accordion Directive HTML.. -->
</uib-accordion>
</div>
JavaScript:
.directive("sizeWatcher", function () { //add size-watcher attribute to element on the page to have it echo its' height to the {{style}} scope
function link(scope, element, attrs) {
scope.$watch(function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
});
}
return {
restrict: "AE", //attribute & element declarations
link: link
};
})
How can I ignore the first $watch event firings and only act
accordingly on the last and final run-through of the directive?
You can ignore watcher when new or old values are undefined and not equal to each other:
$scope.$watch(function () {
return element.height(); // or something else
},
function (newVal, oldVal) {
if (newVal !== undefined && oldVal !== undefined && newVal !== oldVal) {
// your stuff
if (newVal > 150) {
scope.style = {
height: newVal + 'px'
};
}
}
});
Anyways you can play with if statement regards to your needs
FYI, to improve performance $watch returns cancel callback so you can stop watcher whenever you want:
var cancelWatch = $scope.$watch(function () {
return element.height();
},
function (newVal, oldVal) {
if (<some condition>) {
cancelWatch();
}
});
Obviously an answer to this needs a link to the Angular-Documentation for $watch ;)
it states the following:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
which probably explains your first call.
I'm guessing the second call happens because the accordion is rerendered after initialization (with a title/ or label or anything) which triggers the $digest and thus the $watch expression on the height.
Finally the third call happens when you open the accordion and the height actually changes.
To fix this you can compare the newValue and oldValue of the watched expression like Maxim Shoustin said in his answer. Here is an example (again taken from the Angular-docs)
scope.$watch(
// This function returns the value being watched. It is called for each turn of the $digest loop
function() { return food; },
// This is the change listener, called when the value returned from the above function changes
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1;
}
}
);
However if you actually want to change the style of the element you might want to take a look into ng-class instead of manually registering any watchers!
This is happening because you are not using $watch correct way,
The first parameter to $watch is a variable which you want to watch(this can be a callback).
The second parameter to $watch is a callback which performs the desired action on change
So in your case it would be something like this
scope.$watch(
function () {
return element[0].offsetHeight;
},
function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
}
)
Please notice the first function, so whenever the value it is returning changes, the second callback will execute
Hope this helps you
Content edited
The goal is to create a directive that can be attached to a textbox that, when the textbox has focus, an image/button will appear after the textbox and the image/button click event will fire a function contained within the directive. The goal is for this functionality to be entirely self-contained in the directive so it can be easily deployed in many pages or apps.
The image/button appears after the textbox with no problem but the click event of the button does not fire the function. I have created a plunkr with the example code.
In the plunk, line 15 defines a function called 'search,' which does nothing more than fire an alert. When the textbox has focus, the button appears as expected and line 34 calls the search function successfully, which means the function itself is working. However, the button's click event doesn't fire the search function.
Original post
I'm trying to recreate some functionality in our apps that is currently being accomplished with jQuery. The functionality involves attaching a pseudo-class to a textbox which is then picked up by jQuery and an image of a magnifying glass is injected into the DOM immediately after the textbox. Clicking on the image causes a dialog box to pop open.
What I've accomplished so far is a simple html page, a simple controller, and a simple directive. When the textbox has focus, the image appears as expected. However, the ng-click directive does not fire.
Here's the html:
<input
id="txtAlias"
type="text"
ng-model="pc.results"
user-search />
</div>
Here is the controller:
angular
.module('app')
.controller('PeopleController', PeopleController);
PeopleController.$inject = ['$http'];
function PeopleController() {
var pc = this;
pc.results = '';
pc.search = function () {
alert('test');
};
}
And this is the directive:
angular
.module('app')
.directive('userSearch', userSearch);
function userSearch($compile) {
return {
restrict: 'EAC',
require: 'ngModel',
//transclude: true,
scope: {
//search : function(callerid){
// alert(callerid);
//}
},
template: "The user's alias is: <b><span ng-bind='pc.results'></span>.",
//controller: UserSearchController,
link: function (scope, element, attrs) {
element.bind('focus', function () {
//alert(attrs.id + ' || ' + attrs.userSearch);
var nextElement = element.parent().find('.openuserdialog').length;
if (nextElement == 0) {
var magnifyingglass = $compile('<img src="' + homePath + 'Images/zoomHS.png" ' +
'alt="User Search" ' +
'ng-click="pc.search("' + attrs.id + '")" ' +
'class="openuserdialog">')(scope);
element.after(magnifyingglass);
}
});
}
};
};
For the time being, I'd be happy to get an alert to fire by either hitting pc.search in the controller or by search in the isolated scope. So far, neither has worked. I'm sure it's something simple that's missing but I can't figure out what.
Solution
Thanks to a user over at the Google forum for showing me the controllerAs property for directives. This version now works perfectly:
angular
.module('app')
.directive('userSearch', userSearch);
function userSearch($compile){
return {
controller: function ()
{
this.search = function () {
alert('Test');
};
},
link: function (scope, element, attrs) {
element.bind('focus', function () {
var nextElement = element.parent().find('.openuserdialog').length;
if (nextElement === 0) {
var btn = '<img src="' + homePath + 'Images/zoomHS.png" ' +
'ng-click="userSearch.search()" ' +
'class="openuserdialog" />';
element.after($compile(btn)(scope));
}
});
},
controllerAs: 'userSearch'
};
};
You are using isolated scope in your directive which means it don't have access to its parent scope. So in this case you need to pass your method reference explicitly to directive. Passed method reference to your directive scope by new variable inside a isolated scope of directive.
Markup
<input id="txtAlias"
type="text" ng-model="pc.results"
user-search search="search(id)" />
scope: {
search: '&'
}
As you don't have access to parent scope, you can't use controller alias over there like you are using pc.. Simply do use following without alias. So directive will bind those variables from directive scope directly.
template: "The user's alias is: <b><span ng-bind='results'></span>.",
Also change compile template to
if (nextElement == 0) {
var magnifyingglass = $compile('<img src="' + homePath + 'Images/zoomHS.png" ' +
'alt="User Search" ' +
'ng-click="search({id: ' + attrs.id + '})' +
'class="openuserdialog">')(scope);
element.after(magnifyingglass);
}
Rather I'd love to have the compiled template as part of template of directive function only. And I'll show and hide it based on ng-if="expression" directive.
Relative answer
Rather than trying to inject into the DOM, and then trying to hook up to that thing you just injected, wrap both the input and the search button/icon in a directive. You can use an isolated scope and two-way binding to hook up both the input and the button:
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<search-box data="name" search-function="search"></search-box>
</body>
Here's both a controller and a directive that demonstrate this. Note the "=" in the isolated scope, creating a two-way binding to the corresponding attributes when the directive is used in a template.
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.search= function() { alert('search clicked'); }
});
app.directive('searchBox', function() {
return {
restrict: 'E',
scope: {
searchFunction: '=',
data: '=',
},
template: '<input ng-model="data" /><button ng-click="searchFunction()">Search</button>'
}
})
You should be able to easily replace the button element with an img or whatever else your heart desires.
Here's a plunk with an alert() for the search box, and where typing in the text box in the directive affects the corresponding property of the controller scope:
http://plnkr.co/edit/0lj4AmjOgwNZ2DJMSHDj
i am using the wysiwyg redactor in my angular project with the following directive i extended.
why does the element not display anything at focusIn nor does it update after focusOut, it's probably because of ngModel.$render().
how can i register the last event ngModel.$render() if it is only valid after focusIn was executed?
do the events have to be re-registered on focusIn/Out because i replace elements?
// focusin/out event for replacing div
function focusIn(e) {
//wysi redactor template
var tmpl = '<div ng-model=model validation="{{::scope.column.validation}}" placeholder="{{::scope.column.placeholder}}" class="wysi f12 form-control" contenteditable redactor';
if (element.hasClass('one-row'))
tmpl += ' ="{deniedTags: [\'br\'],enterKey: false,pastePlainText: true,linebreaks: true}" ng-class="one-row"></div>';
else
tmpl += '></div>';
var tmp = $compile(tmpl)(scope);
//var tmp = angular.element(tmpl);
// Resume the compilation phase after setting ngModel
element.replaceWith(tmp);
// put in timeout to avoid $digest collision. call render() to
// set the initial value.
$timeout(function() {
editor = element.redactor(options);
element.on('focusout', focusOut);
ngModel.$render();
//element.on('remove', function () {
// //console.log('redactor remove ' + scope.column);
// element.off('remove');
// element.redactor('core.destroy');
// //todo aow: maybe save value?
//});
}, 150);
}
//destroy redactor when losing focus
function focusOut(e) {
//for html render in read-only div
scope.readonlyContent = scope.column.content;
//destroy redactor
element.redactor('core.destroy');
//replace redactor with read-only div
element.replaceWith(this.template);
//$compile(element)(scope);
element.on('click', focusIn);
console.log('after settemplate');
}
ngModel.$render = function() {
if(angular.isDefined(editor)) {
$timeout(function() {
console.log('redactor render ' + scope.column);
//var ed = element.find('.wysi');
element.redactor('code.set', ngModel.$viewValue || '');
element.redactor('placeholder.toggle');
scope.redactorLoaded = true;
});
}
};
i am doing this so complicated because ng-model doesn't suppurt the proper rendering of html, it has to be in ng-bind-html to render properly so i have to use 2 different divs
please check out my plunker version.
i have solved it by putting the ngModel.$render() function in a separate directive as it originally was. my mistake
I am trying to use
https://github.com/zumba/angular-waypoints in one of my ionic projects.
I have the code setup as mentioned in the readme.
Here is the template snippet
<div zum-waypoint="waypoints"
down="flags.on"
up="flags.off"
ng-show="shown()" style="opacity:{{opacity}}; ... </div>
The documentation does not say anything about how to use the waypoints.flag.on value in the controller.
The template above is part of a custom directive - and in my directive's controller, I have tried doing:
$scope.waypoints = {}
$scope.waypoints.flags = {}
$scope.$watch('waypoints.flags', function() {
console.log("Flag on changed to "+JSON.stringify($scope.waypoints) + " for card = "+$scope.id)
if ($scope.waypoints.flags.on){
alert("Mark notification as Read for "+$scope.id)
console.log("up changed" + $scope.waypoints.flags.on + " for id = "+$scope.id)
}
if ($scope.waypoints.flags.off){
alert("2 Mark notification as Read for "+$scope.id)
console.log("down changed " + $scope.waypoints.flags.off + " for id = "+$scope.id)
}
})
But this is never called back.
However, in my template if I use:
{{waypoints.flags.on}} // {{waypoints.flags.off}}
They show up as true // false , though after a digest cycle.
Please let me know what is the correct way of using the waypoint flags in the controller.
Thank you.
That's what I did: a custom directive. It doesn't require zumba's angular-waypoints, only jQuery waypoints.
app.directive('waypoint',function(){
return {
link: function(scope, $this, attrs) {
$this.waypoint({
handler: function() {
scope.$parent.last = scope.post.id;
scope.$parent.$apply();
}
})
}
}
});
I'm tring to create a directive that will center a div.
So far, I have this code:
app.directive("setcenter", function () {
return {
scope:{
setcenter: '='
},
link: function (scope, element, attrs) {
scope.$watch('setcenter', function (newValue) {
if (newValue == true) {
var width = element.width();
element.css('position', 'absolute');
element.css('top', '80px');
element.css('left', '50%');
element.css('z-index', '200');
element.css('margin-left', '-' + width / 2 + 'px');
}
});
}
}
});
The problem is the width of the element. The whole point for this directive is that the div that uses this directive, don't have a width set. I want this directive to figure out the width and center the div.
The problem I encounter is that when the directive is invoked, the actual width of the div is not yet known. When I use this in my situation, the div is 800px, but when the page is finished loading, the div is 221px.
So, what can I do to wait till the actual width is known of the div?
First, I only have used this logic when I defined a controller for a directive rather than a link function. So defining it in a link function instead may cause different behavior, but I suggest you try it there first and if you can't get it to work then switch to using a controller.
As far as I can tell, the only change you would need to make this work would be to change $scope to scope calls and $element to element since the dependency injected objects become standard link function parameters.
$scope.getElementDimensions = function () {
return { 'h': $element.height(), 'w': $element.width() };
};
$scope.$watch($scope.getElementDimensions, function (newValue, oldValue) {
//<<perform your logic here using newValue.w and set your variables on the scope>>
}, true);
$element.bind('resize', function () {
$scope.$apply();
});
The idea for this usage came to me after reading a very similar type of usage about watching the $window rather than the current element, the original work can be found here.
Angular.js: set element height on page load
James' answer led me to:
app.directive('measureInto', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function() {
return element[0].clientWidth;
}, function(value){
scope[attrs.measureInto] = element[0].clientWidth + 10;
});
}
};
});
So, at runtime, I add this and assign into whatever scope variable I want the width of the element I'm looking for
I had a similar issue and found that the dimensions were reliably correct when all the ng-ifs (or anything else using ngAnimate) on the page had been resolved - it's possible something similar is happening here. If so, this would do the trick without adding any new listeners:
$scope.tryGetElementDimensions = function () {
if (!angular.element("your-element") || ((angular.element("your-element")[0].classList).contains("ng-animate")
$timeout(function() {
$scope.tryGetElementDimensions()
})
}
else {
$scope.getElementDimensions()
}
$scope.getElementDimensions = function (){
//whatever you actually wanted to do
}
link: function (scope, element, attrs) {
$scope.tryGetElementDimensions()
}
Angular adds ng-animate and ng-enter, ng-leave classes while it's animating and you can be confident it's finished when these classes have all been removed. $timeout without a second argument just waits for the next digest.
Can't comment yet, therefore this answer.
Found a similar solution like the one strom2357 is suggesting. $timeout works really well to let you know when the dom is ready, and it is super simple. I am using this solution to get the ui-view element width. Found it in a fiddle.
var app = angular.module('app', []);
app.controller('MyController', function($timeout, $scope){
$timeout(function(){
//This is where you would get width or height of an element
alert('DOM ready');
});
alert('DOM not ready');
});