Angular-UI-Bootstrap custom tooltip/popover with 2-way data-binding - angularjs

I am using angular-ui-bootstrap in my current project, and I have a requirement for a popover that will allow the user to take some action on a given element (rename/edit/delete/etc...). Since angular-ui's bootstrap popover doesn't allow for custom html or data-binding by default, I have copied their tooltip/popover .provider and .directive in an effort to customize it to my needs.
Main Problem: The ng-click bindings are being lost after the popup is closed and re-opened.
Secondary Problem: Can two-way data-binding be setup so that I don't have to manually set scope.$parent.$parent.item?
Plunker: http://plnkr.co/edit/HP7lZt?p=preview
To give glance of what changes were made to the original tooltip.js:
The popover .directive is what has been modified the most:
.directive('iantooltipPopup', function () {
return {
restrict: 'E',
replace: true,
scope: { mediaid: '#', title: '=', content: '#', placement: '#', animation: '&', isOpen: '&' },
templateUrl: 'popover.html',
link: function (scope, element, attrs) {
scope.showForm = false;
scope.onRenameClick = function () {
console.log('onRenameClick()');
scope.showForm = true;
};
scope.onDoneClick = function () {
console.log('Title was changed to: ' + scope.title);
scope.showForm = false;
scope.$parent.$parent.item.title = scope.title;
scope.$parent.hide();
};
}
};
})
The tooltip .provider was only changed here, in an effort to get two-way binding to work on the title field :
var template =
'<'+ directiveName +'-popup '+
// removed
// 'title="'+startSym+'tt_title'+endSym+'" '+
'title="tt_title" ' +
'content="'+startSym+'tt_content'+endSym+'" '+
'placement="'+startSym+'tt_placement'+endSym+'" '+
'animation="tt_animation()" '+
'is-open="tt_isOpen"'+
'>'+
'</'+ directiveName +'-popup>';
I appreciate any help, I feel the compiled directives and providers seem to be large mental hurdles when getting started with Angular. I've been trying figure out and manipulate this directive so I can learn from it, just as much as actually needing the component itself.

Here is the working plunker
The problem is from the original tooltip. It removes the tooltip after you close but next time when you open it, it doesn't compile the tooltip again. (link function for the tooltip trigger only run in the first time.)
My approach is don't remove the tooltip, just control it by display attribute from CSS.
I also make a pull request to discuss this issue.
I just update the plunker.
The 2nd one is actually just make it link with the parent scope. However, it will create a child scope with my approach. I think you can use watch to do it as well.

Related

How to hook to "OnOpen" listener for uib-popover?

Context: I am using angular 1 and this UIB Popover control.
Since there is a text field in the popover template I called, my target is to focus on that text field whenever the popover is opened.
Unfortunately, there is no popover listener/event for "onOpen".
So I tried to do a
scope.$watch(()=>{return scope.isOpen}, (obj) ={
// where scope.isOpen is the local var in the popover-is-open
// expecting to write some code here to manipulate the element
// to realise the focus operation
// but there is no popover element yet when this is called
})
I was just wondering what other options I might have?
Thanks
I found nothing on the documentation talked about events and found this issue on the ui-bootstrap github stating that they do not support events nor do they ever plan to implement them. https://github.com/angular-ui/bootstrap/issues/5060
If you're looking for a different option that would give you access to the events would be to implement your own popover directive that simply wraps bootstrap popovers. In theory, they can function the same as the ui-bootstrap and allows you to tap directly into the events provided by bootstrap.
HTML
<div my-popover="Hello World" popover-title="Title" popover-shown="myCallback()">...</div>
JavaScript ('my-popover.directive.js')
angular
.module('myModule')
.directive('myPopover', myPopover);
function myPopover() {
return {
scope: {
popoverTitle: '#',
popoverShown: '&'
},
restrict: 'A',
link: function(scope, elem, attr) {
$(elem).popover({
title: scope.popoverTitle,
content: attr.myPopover
});
$(elem).on('shown.bs.popover', function () {
if(scope.popoverShown && typeof scope.popoverShown === 'function'){
scope.popoverShown();
}
});
}
};
}
Similar to uib-popover, you can add support for additional configurations by adding additional scoped properties.

AngularJs ngNotify dismiss on click

I have been asked to add dismiss on click functionality to our notifications (which are served up by ngNotify). I thought this would be a simple task, but I cannot find this functionality by default.
At first I created a directive that targeted the class ngn-sticky. It was simply this:
angular.module('sapphire.directives').directive('ngnSticky', directive);
function directive(ngNotify) {
return {
restrict: 'C',
link: lnkFn
};
function lnkFn(scope, element, attrs, controller) {
element.on('click', function (e) {
ngNotify.dismiss();
e.preventDefault();
});
};
};
But this did not work. I added a button to the page with the notification and gave it the class ngn-sticky and that did work, but that was no the desired effect.
So, I though that maybe I can override the template like we do with angular-bootstrap, so I looked into the actual directive and found that it has no templateUrl to override.
Does anyone know how I can override the template? Or somehow add an onclick method to any ngNotify message?
Overriding the template is not a good solution, as you then implicitly depend on the internals of the external library!
Anyhow, in their source code you can see a template being put into the $templateCache.
function ngNotifyCache($templateCache) {
var html =
'<div class="ngn" ng-class="ngNotify.notifyClass">' +
'<span ng-if="ngNotify.notifyHtml" class="ngn-message" ng-bind-html="ngNotify.notifyMessage"></span>' + // Display HTML notifications.
'<span ng-if="!ngNotify.notifyHtml" class="ngn-message" ng-bind="ngNotify.notifyMessage"></span>' + // Display escaped notifications.
'<span ng-show="ngNotify.notifyButton" class="ngn-dismiss" ng-click="dismiss()">×</span>' +
'</div>';
$templateCache.put(TEMPLATE, html);
}
You should be able to override that by just putting in a new template with a the same key. Just the proper timing needs to be found out. The new template would need a ng-click attribute at the whole div I assume.
Source: https://docs.angularjs.org/api/ng/service/$templateCache, https://docs.angularjs.org/api/ng/type/$cacheFactory.Cache

AngularJS: Should I observe optional attributes?

Assume I have a directive my-button with an optional attribute disabled. People could use this directive like:
<my-button>Button text</my-button>
or
<my-button disabled="variable">Button Text</my-button>
Should I be watching for a disabled attribute? Could these usages somehow transition from one to the other?
In response to JB Nizet's request for the code in question, here's a clean version of the directive function:
function ButtonDirective() {
var directive = {
link: link,
restrict: 'E',
scope: {
click: '&',
disabled: '=?',
},
template: '<a class="my-button" ' +
'data-ng-class="{\'my-button-disabled\': disabled}" ' +
'data-ng-click="disabled || click()" ng-transclude />',
transclude: true
};
function link(scope) {
if (typeof scope.disabled == 'undefined') scope.disabled = false;
}
return directive;
}
The directive creates an anchor tag styled as a button. It accepts two properties/parameters: click and disabled. The latter is optional. When disabled, the click event should fire when clicked, otherwise the the click event should fire when clicked.
To reiterate: Should I worry about someone somehow adding, removing, or modifying the disabled attribute after the fact? If so, how should I go about it?
After hashing things out with JB Nizet, he counseled me to not worry about the HTML attribute changing.

Controller must wait until Directive is loaded

I'm currently writing a small set of AngularJS controls, and I'm encountering an issue here.
The control which I'm creating is a button (let's start simple).
The directive looks like this:
officeButton.directive('officeButton', function() {
return {
restrict: 'E',
replace: false,
scope: {
isDefault: '#',
isDisabled: '#',
control: '=',
label: '#'
},
template: '<div class="button-wrapper" data-ng-click="onClick()">' +
'<a href="#" class="button normal-button">' +
'<span>{{label}}</span>' +
'</a>' +
'</div>',
controller: ['$scope', function($scope) {
// Controller code removed for clarity.
}],
link: function(scope, element, attributes, controller) {
// Link code removed for clarity.
}
}
});
I've removed some code because it will make my question very hard to understand and I don't believe it's needed here.
Inside my controller of my directive, I'm writing an API and that being done by executing the following code:
controller: ['$scope', function($scope) {
// Allows an API on the directive.
$scope.api = $scope.control || {};
$scope.api.changeLabel = function(label)
$scope.label = label;
}
}]
So, on my directive, I do have an isolated scope to which I can pass a control property. Through this property, I'll have access to the method changeLabel.
My control can be rendered by using the following directive in the HTML website:
<office-button control="buttonController"></office-button>
The controller on my application will looks like:
officeWebControlsApplication.controller('OfficeWebControlsController', ['$scope', function($scope) {
$scope.buttonController = {};
}]);
I can execute my changeLabel method right now by calling the following method in my scope
$scope.buttonController.changeLabel('Demo');
However, this doesn't work since at this point changeLabel is not defined. The button must first be completely rendered.
Any idea on how to resolve this particular issue?
Edit: Plunker Added
I've added a plunker to provide more information.
When the changeLabel method is called within the onClick event, then everything is working, but if I call it outside, it isn't working anymore. This is because the $scope.api.changeLabel is not defined at the moment of execution (button is not rendered yet). So I basically have to wait in my application's controller until the button is fully rendered.
Kind regards

sanitize text input from textangular

I'm working with textangular as a rich text solution for a project I am working on.
It is required to sanitize this input because we only allow certain html tags.
Since this is not a default possibility of textangular I created a directive that wraps around the textangular directive. I can successfully sanitize the input text, but the view is never updated, and I have ran out of ideas how to achieve this
directive:
.directive('chTextAngular', function(){
return {
restrict: 'E',
require: 'ngModel',
template: function(elements, attributes) {
return '<div text-angular ng-model="' + attributes.ngModel + '" id="' + attributes.id + '"></div>';
},
link: function(scope, element, attributes, ngModel) {
scope.$watch(attributes.ngModel, function(n, o) {
if(n !== undefined) {
// Replace all the divs with <br> tags
var sanitized = ngModel.$viewValue.replace(/<div>/gm, '<br>').replace(/<\/div>/gm, ' ');
sanitized = sanitized.replace(/<p>/gm, '').replace(/<\/p>/gm, '<br><br>');
sanitized = $.htmlClean(sanitized, {
allowedTags: ["strong", "em", "ul", "ol", "li", "a", "b", "i", "br"],
allowedAttributes: ["a", "href"]
});
console.log(sanitized);
ngModel.$setViewValue(sanitized);
}
});
}
}
});
The log prints out the model and it shows that it is actually change. I just can not figure out how to update the actual textarea in textangular.
Can anyone help me with this, or put me in the right direction?
Ok, first question I have to ask is; do you really need the view to update or can it be fine as it is if the model is the correct data.
Second, you should probably be registering your function via ngModel.$parsers.push(function(n,o){...}) to avoid extra watchers.
The reason it's not updating is that we have a catch in textAngular to prevent the model from updating the view while someone is typing. If you do this while the field is focussed then you get an issue with the cursor moving back to the start/end of the text field. If you do want to update the view from the model then register an blur event handler (element.on('blur', ...);) and call scope.updateTaBindtaTextElement() may work depending on which scope your directive attaches to. If that function doesn't work you'll have to add a name attribute to your textAngular element and then use the textAngularManager.refreshEditor(name); function.

Resources