I'm trying to wrap my head around the directive concept in Angular.
I want to show a modal box when clicking on a link. The contents of the modal box is dynamic. In jQuery it would be an easy $("body").append(myModal) and then simply remove() it from the DOM when closed.
Now I'd like to do the same in pure Angular. This is what I have so far:
A controller function:
$scope.userLogout = function() {
notification.show();
};
A service:
.service('notification', ['$rootScope',
function($rootScope) {
var notification = {
open: false,
show : function() {
this.open = true;
},
hide: function() {
this.open = false;
}
};
return notification;
}
])
A directive:
.directive('notification', ['notification',
function(notification){
return {
restrict: 'E',
replace: true,
template: (notification.open) ? '<div class="myModal"></div>' : ''
}
}])
How do I update the directive when the value in my service changes? Or is this the right approach at all?
For what it's worth, with something like Angular, it's possible to simply use data-ng-show and data-ng-hide on an element styled like a modal. Depending on your use case, you may not need to create a directive to achieve what you want. Consider the following:
HTML:
...
<div data-ng-show="notification.open" class="modalPopup">
...
{{notification.my_modal_message}}
...
<button data-ng-click="closeModal()">Close</button>
</div>
JS (simplified):
function myCtrl ($scope) {
$scope.notification = {
my_modal_message: "Bender's back, baby!",
open: false
}
$scope.logout = function () {
// logout stuff
logout().success(function () {
// open the modal
$scope.notification.open = true;
}
}
$scope.close = function () {
$scope.notification.open = false;
}
}
At times, it's much better to make a full directive to do something like this for you. However, again - depending on your use case - this may be all you need. Just something to keep in mind.
Related
I'd like to add a class to the element, when it's in view a.k.a scrolled to, but I have no clue where to start.
Let's say I want to add it to <div></div> block and when it's scrolled in to it, the class will be added and when it's scrolled out of it, the class will be removed.
I would provide some html mark up to apply it on, but I think demonstration on <div></div> tags is satisfactionaly to work with.
ui-scroll module adds a class when you scroll past an item. You could fork it and adjust to your needs.
You might already know; there are also many jQuery plugins for this and it's pretty easy to wrap this kind of plugin with a directive:
angular.module('testApp', [])
.directive('onScrollAddClass', function() {
return {
link: function(scope, elm, attr) {
init(elm, attr.onScrollAddClass);
function init(elm, cls) {
elm.viewportChecker({
classToAdd: cls,
repeat: true
});
}
}
}
});
Here's the demo for above code.
I solved it this way on one of my websites.
angular.module("Directives")
.directive("rjOnScrollTo", [
"scroll", "$window", "$rootScope",
function(scroll, $window, $rootScope) {
function link($scope, $element) {
var offset = 100;
var pageLoaded = false;
var uniqueId = "rjOnScrollTo_" + $element.get(0).id;
var onScroll = function() {
if (pageLoaded && $window.scrollTop() + $window.height() - offset >= $element.offset().top) {
$element.addClass($scope.className);
scroll.removeCallback($window, uniqueId);
}
};
scroll.addCallback($window, uniqueId, onScroll);
$scope.$on("$destroy", function() {
scroll.removeCallback($window, uniqueId);
});
$scope.$on("$routeChangeSuccess", function() {
pageLoaded = false;
});
$scope.$on("pageLoaded", function() {
pageLoaded = true;
onScroll();
});
}
return {
link: link,
restrict: "A",
scope: {
"className": "#rjOnScrollTo"
}
};
}]);
Use like so:
<div data-rj-on-scroll-to="my-class"></div>
Edit:
You might want to pass the offset parameter in rather than hard-code it.
The service that loads the data from the back-end via AJAX fires the "pageLoaded" event. It was important in my case to wait for the page content to be fully loaded otherwise the div would be immediately in view and the class would be added straight away.
I am trying to trigger the click of a button when a key is pressed. I'm doing this using the triggerHandler function, but this is causing the error above. I'm thinking I must have created some kind of circular reference/loop, but I can't see where.
This is my HTML:
<button id="demoBtn1" hot-key-button hot-key="hotKeys.primaryTest" class="po-btn primary-btn" type="submit" ng-click="btnFunction()"></button>
Here's my controller:
.controller('BtnCtrl', function ($scope) {
$scope.checkKeyPress = function ($event, hotKeyObj) {
for(var key in hotKeyObj) {
if($event.keyCode == hotKeyObj[key].keyCode) {
var id = hotKeyObj[key].btnId;
hotKeyObj[key].funcTriggered(id);
}
}
}
$scope.clickFunction = function(id) {
var currentButton = angular.element(document.getElementById(id));
currentButton.triggerHandler("click");
}
$scope.btnFunction = function() {
console.log("clickFunction1 has been triggered");
}
$scope.hotKeys = {
'primaryTest': {
keyCode: 49,
keyShortcut: "1",
label: "Button",
btnId: 'demoBtn1',
funcTriggered: $scope.clickFunction
},
// more objects here
}
}
})
And my directive is here:
.directive("hotKeyButton", function() {
return {
controller: 'BtnCtrl',
scope: {
hotKey: '='
},
transclude: true,
template: "<div class='key-shortcut'>{{hotKey.keyShortcut}}</div><div class='hotkey-label'>{{hotKey.label}}</div>"
};
})
It's a bit of a work in progress, so I suspect there might be small errors in places, but I'm primarily interested in the logic running from the keypress to btnFunction being triggered. The error fires on the currentButton.triggerHandler("click") line.
Can anyone see what I've done? Thanks.
Since you have a problem with $apply in progress - you can just wrap your triggerHandler call into $timeout wrapper - just to make everything you need in another $digest-cycle, like this:
$scope.clickFunction = function(id) {
var currentButton = angular.element(document.getElementById(id));
$timeout(function () {
currentButton.triggerHandler("click");
});
}
After this everything will work OK.
Also don't forget to $inject $timeout service into your BtnCtrl.
Also i'm not sure you need to define controller property for your directive, but it's not a main case.
Demo: http://plnkr.co/edit/vO8MKAQ4397uqqcAFN1D?p=preview
So I have a directive that Will be acting as a side panel in my app. When a user clicks a button the side panel will open. In said side panel I need the controller and view for this area to be dynamic based on which button the users clicks. I have found a way to load up the template dynamically but I am running into issues with loading the controller dynamically.
Enough talking here is the code.
Directive Code
app.directive('itemForm', function($compile) {
var item1Template = '<div ng-include="view"></div>';
var item2Template = '<h1> Hello item2 Form </h1>';
var getTemplate = function(contentType) {
if(contentType === 'item1') {
return item1Template;
} else if(contentType === 'item2') {
return item2Template;
}
};
return {
restrict: 'E',
replace: 'true',
scope: {
formType: '#formType'
},
//templateUrl: scope.template,
link: function(scope, element) {
if(scope.formType === 'item1') {
scope.view = '/views/item1.html';
}
element.html(getTemplate(scope.formType)).show();
$compile(element.contents())(scope);
}
};
});
Html
<item-form form-type='{{form.type}}'> </item-form>
Controller for view that directive lives in
$scope.form = {};
$scope.openItemOneDlg = function() {
$scope.isFormOpen = !$scope.isFormOpen; // this opens the side panel
$scope.form.type = 'item1';
};
$scope.openItemTwoDlg = function() {
$scope.isFormOpen = !$scope.isFormOpen; // this opens the side panel
$scope.form.type = 'item2';
};
You can broadcast (using $broadcast) an event on click of the button. And have a listener (using $on) in the directive. This way, whenever the event is fired, directive logic will get executed.
You can refer the answer on this link for the usage of $broadcast and $on:
On-and-broadcast-in-AngularJS
I want to be able to load the directive's template from a promise. e.g.
template: templateRepo.get('myTemplate')
templateRepo.get returns a promise, that when resolved has the content of the template in a string.
Any ideas?
You could load your html inside your directive apply it to your element and compile.
.directive('myDirective', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
//Some arbitrary promise.
fetchHtml()
.then(function(result){
element.html(result);
$compile(element.contents())(scope);
}, function(error){
});
}
}
});
This is really interesting question with several answers of different complexity. As others have already suggested, you can put loading image inside directive and when template is loaded it'll be replaced.
Seeing as you want more generic loading indicator solution that should be suitable for other things, I propose to:
Create generic service to control indicator with.
Manually load template inside link function, show indicator on request send and hide on response.
Here's very simplified example you can start with:
<button ng-click="more()">more</button>
<div test="item" ng-repeat="item in items"></div>
.throbber {
position: absolute;
top: calc(50% - 16px);
left: calc(50% - 16px);
}
angular
.module("app", [])
.run(function ($rootScope) {
$rootScope.items = ["One", "Two"];
$rootScope.more = function () {
$rootScope.items.push(Math.random());
};
})
.factory("throbber", function () {
var visible = false;
var throbber = document.createElement("img");
throbber.src = "http://upload.wikimedia.org/wikipedia/en/2/29/Throbber-Loadinfo-292929-ffffff.gif";
throbber.classList.add("throbber");
function show () {
document.body.appendChild(throbber);
}
function hide () {
document.body.removeChild(throbber);
}
return {
show: show,
hide: hide
};
})
.directive("test", function ($templateCache, $timeout, $compile, $q, throbber) {
var template = "<div>{{text}}</div>";
var templateUrl = "templateUrl";
return {
link: function (scope, el, attr) {
var tmpl = $templateCache.get(templateUrl);
if (!tmpl) {
throbber.show();
tmpl = $timeout(function () {
return template;
}, 1000);
}
$q.when(tmpl).then(function (value) {
$templateCache.put(templateUrl, value);
el.html(value);
$compile(el.contents())(scope);
throbber.hide();
});
},
scope: {
text: "=test"
}
};
});
JSBin example.
In live code you'll have to replace $timeout with $http.get(templateUrl), I've used the former to illustrate async loading.
How template loading works in my example:
Check if there's our template in $templateCache.
If no, fetch it from URL and show indicator.
Manually put template inside element and [$compile][2] it.
Hide indicator.
If you wonder what $templateCache is, read the docs. AngularJS uses it with templateUrl by default, so I did the same.
Template loading can probably be moved to decorator, but I lack relevant experience here. This would separate concerns even further, since directives don't need to know about indicator, and get rid of boilerplate code.
I've also added ng-repeat and run stuff to demonstrate that template doesn't trigger indicator if it was already loaded.
What I would do is to add an ng-include in my directive to selectively load what I need
Check this demo from angular page. It may help:
http://docs.angularjs.org/api/ng.directive:ngInclude
````
/**
* async load template
* eg :
* <div class="ui-header">
* {{data.name}}
* <ng-transclude></ng-transclude>
* </div>
*/
Spa.Service.factory("RequireTpl", [
'$q',
'$templateCache',
'DataRequest',
'TplConfig',
function(
$q,
$templateCache,
DataRequest,
TplConfig
) {
function getTemplate(tplName) {
var name = TplConfig[tplName];
var tpl = "";
if(!name) {
return $q.reject(tpl);
} else {
tpl = $templateCache.get(name) || "";
}
if(!!tpl) {
return $q.resolve(tpl);
}
//加载还未获得的模板
return new $q(function(resolve, reject) {
DataRequest.get({
url : "/template/",
action : "components",
responseType : "text",
components : name
}).success(function(tpl) {
$templateCache.put(name, tpl);
resolve(tpl);
}).error(function() {
reject(null);
});
});
}
return getTemplate;
}]);
/**
* usage:
* <component template="table" data="info">
* <span>{{info.name}}{{name}}</span>
* </component>
*/
Spa.Directive.directive("component", [
"$compile",
"RequireTpl",
function(
$compile,
RequireTpl
) {
var directive = {
restrict : 'E',
scope : {
data : '='
},
transclude : true,
link: function ($scope, element, attrs, $controller, $transclude) {
var linkFn = $compile(element.contents());
element.empty();
var tpl = attrs.template || "";
RequireTpl(tpl)
.then(function(rs) {
var tplElem = angular.element(rs);
element.replaceWith(tplElem);
$transclude(function(clone, transcludedScope) {
if(clone.length) {
tplElem.find("ng-transclude").replaceWith(clone);
linkFn($scope);
} else {
transcludedScope.$destroy()
}
$compile(tplElem.contents())($scope);
}, null, "");
})
.catch(function() {
element.remove();
console.log("%c component tpl isn't exist : " + tpl, "color:red")
});
}
};
return directive;
}]);
````
I have the following code for creating a popover in my template file:
<span class="icon-globe visibility"
id="visibilityFor{{post.metaData.assetId}}"
popover="{{post.visibilityListStr}}"
popover-placement="right"
popover-trigger="mouseenter"
popover-popup-delay="50"
visibility>
</span>
I have a few clickable links on the popover. But the problem is I'm not able to hover on the popover created. I referred to the link http://jsfiddle.net/xZxkq/
and tried to create a directive viz. 'visibility' for this purpose.
Here is the code:
myAppModule.directive("visibility", function ($timeout,$rootScope) {
return {
controller: function ($scope, $element) {
$scope.attachEvents = function (element) {
$('.popover').on('mouseenter', function () {
$rootScope.insidePopover = true;
});
$('.popover').on('mouseleave', function () {
$rootScope.insidePopover = false;
$(element).popover('hide');
});
}
},
link: function (scope, element, attrs) {
$rootScope.insidePopover = false;
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
attachEvents(element);
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
attachEvents(element);
}
}, 200);
});
}
}
});
But I get an exception for 'element.popover' since it is undefined. Please point as to what I'm doing wrong and how can I show/hide the angular ui popover from the directive. I am using angular ui bootstrap JS file.
I have solved it in a very cleaned way and thought to share it:
.popover is being created not as a child of the uib-popover
so the idea is to wrap uib-popover with a parent and to control show&hide on hovering the parent.
.popover and uib-popover are children of this parent
so just left to set popover-trigger=none and you have what you are wishing for.
I created a plunk example:
<span ng-init="popoverOpened=false" ng-mouseover="popoverOpened=true" ng-mouseleave="popoverOpened=false">
<button class="btn btn-default" uib-popover-html="htmlPopover"
popover-trigger="none" popover-placement="bottom-left" popover-is-open="popoverOpened" >
<span>hover me</span>
</button>
</span>
enjoy.
I don't know if this is relevant to the OP anymore, but I've had the same problem and fortunately I managed to solve it.
Undefined error
First thing first, the undefined error you are getting might be (at least in my case) because you are using the development version of ui-bootstrap. In my case I got this error when trying to bind element.popover. After adding the minified version of the library the error went away.
Keep the popover open when hovering over it
To do this I have created a custom directive that makes use of the popover from the ui-bootstrap library.
Directive
app.directive('hoverPopover', function ($compile, $templateCache, $timeout, $rootScope) {
var getTemplate = function (contentType) {
return $templateCache.get('popoverTemplate.html');
};
return {
restrict: 'A',
link: function (scope, element, attrs) {
var content = getTemplate();
$rootScope.insidePopover = false;
$(element).popover({
content: content,
placement: 'top',
html: true
});
$(element).bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
$(element).popover('show');
scope.attachEvents(element);
}
}, 200);
});
$(element).bind('mouseleave', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover)
$(element).popover('hide');
}, 400);
});
},
controller: function ($scope, $element) {
$scope.attachEvents = function (element) {
$('.popover').on('mouseenter', function () {
$rootScope.insidePopover = true;
});
$('.popover').on('mouseleave', function () {
$rootScope.insidePopover = false;
$(element).popover('hide');
});
}
}
};
});
This directive also accepts a custom template for the popover, so you are not limited to just title and some text in it. You can create your own html template and feed it to the control.
Usage
<a href="#" hover-popover>Click here</a>
Hopes this helps someone else in the future :)
Edit
As requested, here is a Fiddle link. It lacks the styling, but it should demonstrate the way it works.
There I spend 1 day and finally get solution.
<button uib-popover="{{dynamicPopover.content}}"
popover-trigger="outsideClick" popover-is-open="popoverIsOpen"
ng-mouseenter="popoverIsOpen = !popoverIsOpen"
popover-title="{{dynamicPopover.title}}" type="button" class="btn btn-default">Dynamic Popover</button>
Please check
Plunkeer Link
Check only Dynamic Popover button code
Thanks,
I think Cosmin has the hoverable popover right, but it does seem to be using the Twitter Bootstrap popover method. The idea is to have this hoverable popover implemented only with AngularJS and one of the Bootstrap wrappers for AngularJS, which are UI Bootstrap or AngularStrap.
So I have put together an implementation which uses only AngularStrap:
myApp.directive('hoverablePopover', function ($rootScope, $timeout, $popover) {
return {
restrict: "A",
link: function (scope, element, attrs) {
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!scope.insidePopover) {
scope.popover.show();
scope.attachEventsToPopoverContent();
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!scope.insidePopover) {
scope.popover.hide();
}
}, 400);
});
},
controller: function ($scope, $element, $attrs) {
//The $attrs will server as the options to the $popover.
//We also need to pass the scope so that scope expressions are supported in the popover attributes
//like title and content.
$attrs.scope = $scope;
var popover = $popover($element, $attrs);
$scope.popover = popover;
$scope.insidePopover = false;
$scope.attachEventsToPopoverContent = function () {
$($scope.popover.$element).on('mouseenter', function () {
$scope.insidePopover = true;
});
$($scope.popover.$element).on('mouseleave', function () {
$scope.insidePopover = false;
$scope.popover.hide();
});
};
}
};
});
When you have a popover element, you need to take into account that you have the element that triggers the popover and you also have the element with the actual popover content.
The idea is to keep the popover open when you mouse over the element with the actual popover content. In the case of my directive, the link function takes care of the element that triggers the popover and attaches the mouseenter/mouseout event handlers.
The controller takes care of setting the scope and the popover itself via the AngularStrap $popover service. The controller adds the popover object returned by the AngularStrap service on the scope so that it is available in the link function. It also adds a method attachEventsToPopoverContent, which attaches the mouseenter/mouseout events to the element with the popover content.
The usage of this directive is like this:
<a title="Popover Title" data-placement="left" data-trigger="manual" data-content="{{someScopeObject}}" content-template="idOfTemplateInTemplateCache" hoverablePopover="">
You have to put the trigger in single quotes, because, reasons:
<button uib-popover="I appeared on mouse enter!" popover-trigger="'mouseenter'" type="button" class="btn btn-default">Mouseenter</button>
demo:
https://jsbin.com/fuwarekeza/1/edit?html,output
directive:
myAppModule.directive('popoverHoverable', ['$timeout', '$document', function ($timeout, $document) {
return {
restrict: 'A',
scope: {
popoverHoverable: '=',
popoverIsOpen: '='
},
link: function(scope, element, attrs) {
scope.insidePopover = false;
scope.$watch('insidePopover', function (insidePopover) {
togglePopover(insidePopover);
})
scope.$watch('popoverIsOpen', function (popoverIsOpen) {
scope.insidePopover = popoverIsOpen;
})
function togglePopover (isInsidePopover) {
$timeout.cancel(togglePopover.$timer);
togglePopover.$timer = $timeout(function () {
if (isInsidePopover) {
showPopover();
} else {
hidePopover();
}
}, 100)
}
function showPopover () {
if (scope.popoverIsOpen) {
return;
}
$(element[0]).click();
}
function hidePopover () {
scope.popoverIsOpen = false;
}
$(document).bind('mouseover', function (e) {
var target = e.target;
if (inside(target)) {
scope.insidePopover = true;
scope.$digest();
}
})
$(document).bind('mouseout', function (e) {
var target = e.target;
if (inside(target)) {
scope.insidePopover = false;
scope.$digest();
}
})
scope.$on('$destroy', function () {
$(document).unbind('mouseenter');
$(document).unbind('mouseout');
})
function inside (target) {
return insideTrigger(target) || insidePopover(target);
}
function insideTrigger (target) {
return element[0].contains(target);
}
function insidePopover (target) {
var isIn = false;
var popovers = $('.popover-inner');
for (var i = 0, len = popovers.length; i < len; i++) {
if (popovers[i].contains(target)) {
isIn = true;
break;
}
}
return isIn;
}
}
}
}]);
html:
<span class="icon-globe visibility"
id="visibilityFor{{post.metaData.assetId}}"
popover="{{post.visibilityListStr}}"
popover-is-open="{{post.$open}}"
popover-trigger="click"
popover-hoverable="true"
visibility>
</span>
html
<span class="icon-globe" id="visibilityFor" popover="hello how are you"
popover-placement="right" popover-trigger="mouseenter"
popover-popup-delay="50" viz>
</span>
directive
myAppModule.directive('viz', function ($rootScope,$timeout){
return{
restrict:"A",
link: function (scope, element, attrs) {
$rootScope.insidePopover = false;
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
// attachEvents(element);
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
// attachEvents(element);
}
}, 200);
});
}
}
});
Note : - Don't forget to include angular-strap after jQuery.js & angular.js
This feature was added in Angular UI Bootstrap 0.14.0 and is documented here. Disable the triggers and use the popover-is-open property to manually dictate the opened/closed state.
What I did that gets my by in 0.13.X is to set the element to be hoverable to a <button> and then set the popover-trigger="focus". Then style the button how you wish, and focus the button by clicking it. You can hover in the popover and click a link, all I need to do.
Easiest way to have a mouse-event using uib-popover
Look at the below working example !
You need not have a uib-tabset, I faced an issue with uib-tabset and so added that example.
<uib-tabset>
<uib-tab>
<uib-tab-heading>
Tab 1
</uib-tab-heading>
<div>
<span ng-mouseover="popoverIsOpen = true"
ng-mouseleave="popoverIsOpen = false">
<button uib-popover-template="'includeFile.html'"
popover-trigger="outsideClick"
popover-is-open="popoverIsOpen"
popover-placement="right"
type="button" class="btn btn-default">
Dynamic Popover
</button>
</span>
</div>
<p> tab 1</p>
</uib-tab>
<uib-tab>
<uib-tab-heading>
Tab 2
</uib-tab-heading>
<p> tab 2</p>
</uib-tab>
</uib-tabset>
Template: includeFile.html
<div>
<span>This is for tesitng</span>
<strong> www.google.com</strong>
</div>
I needed to do this as well. I have a checkbox in a table cell that can have 3 possible states: Enabled, Disabled, or Special case. The UI spec I have asked for a popover over the box that shows either of those statuses, or for the special case a sentence with a link.
I tried several of these solutions and one of them worked for me, and they all added extra code. After some playing around, I determined I could just add the "popover-popup-close-delay" attribute with a dynamic value. So this works for me:
<td uib-popover-html="getPopoverTxt()" popover-popup-close-delay="{{ele.isspecial ? 2000 : 300}}" popover-popup-delay="300" popover-append-to-body="true" popover-placement="top" popover-trigger="mouseenter">
<input id="issynced{{ele.id}}" name="isChecked" type="checkbox" data-ng-checked="ele.ischecked" data-ng-model="ele.ischecked">
<label for="issynced{{ele.id}}"></label>
</td>
Some context: My table is looping over an array of data objects, so ele is a single object. The getPopoverTxt() is just a simple method in my controller that returns one of the 3 labels I want to show ("Enabled", "Disabled", or "Special Text with HTML"). Its not necessary here, but the takeaway is to get the HTML to work, you have to wrap the string value in $sce.trustAsHtml(), like:
var specialText = $sce.trustAsHtml('Text with a link to contact support');
The rest is all the usual popover and form input settings we normally use. The "popover-popup-close-delay" is the key.