How to show angular bootstrap popover on custom event? - angularjs

I want to display bootstrap popover on custom event (after loading data from server). I don't understand why this code don't work. JSFiddle
And second question, how to trigger events if I set replace = true? Because of IE8 doesn't support custom tags and throws exception.
<div ng-app="tcApp">
<manual></manual>
</div>
var tcApp = angular.module('tcApp', [ 'ui.bootstrap' ]);
tcApp.config(function ($tooltipProvider) {
$tooltipProvider.setTriggers({
'openAfterLoad': 'closeOnClick'
});
});
tcApp.directive('manual', [ '$timeout', '$interval', function($timeout, $interval) {
return {
restrict: 'E',
scope: {},
template:
'<a ' +
' href="#" ' +
' ng-if="isManualVisible" ' +
' popover-append-to-body="true" ' +
' popover-placement="bottom" ' +
' popover-trigger="openAfterLoad" ' +
' popover-title="{{title}}" ' +
' popover="{{content}}" ' +
' tooltip="{{title}}" ' +
' >' +
' <span class="{{textClass}}">' +
' <span ng-show="!loading" class="glyphicon glyphicon-question-sign"></span>' +
' <span ng-show="loading" class="glyphicon glyphicon-refresh"></span>' +
' </span>' +
'</a>',
replace: false, // true for IE8 only
link: function (scope, element, attr) {
scope.isManualVisible = true;
scope.title = 'First title value';
scope.content = 'First content value';
scope.textClass = 'text-info';
scope.opened = false;
scope.loading = false;
scope.loaded = false;
scope.loadedTitle = false;
scope.loadedContent = false;
scope.checkLoadState = function () {
if (scope.loadedTitle && scope.loadedContent) {
scope.loaded = true;
if (scope.loading)
scope.loading = false;
element.triggerHandler('openAfterLoad');
}
};
scope.$watch('loadedTitle', scope.checkLoadState);
scope.$watch('loadedContent', scope.checkLoadState);
element.on('openAfterLoad', function(event) {
console.log('openAfterLoad');
scope.opened = true;
});
element.on('closeOnClick', function(event) {
console.log('closeOnClick');
scope.opened = false;
});
element.on('click', function(event) {
console.log('click');
if (scope.loaded) {
if (scope.opened)
element.triggerHandler('closeOnClick');
else
element.triggerHandler('openAfterLoad');
} else {
scope.loadData();
}
});
scope.loadData = function() {
if (!scope.loaded && !scope.loading) {
scope.loading = true;
$interval(function() {
scope.title = 'New title value';
scope.loadedTitle = true;
}, 5000, 1);
$interval(function() {
scope.content = 'New content value';
scope.loadedContent = true;
}, 5000, 1);
}
};
}
};
}]);

Ok, I'm found solution.
First of all, mixing of replace=true and ng-if on root template element is a bad idea. Next, I separated angular on/broadcast events and native js events. Native events still required for popover.
JSFiddle
<div ng-app="tcApp">
<manual></manual>
</div>
var tcApp = angular.module('tcApp', [ 'ui.bootstrap' ]);
tcApp.config(function ($tooltipProvider) {
$tooltipProvider.setTriggers({
'openAfterLoad': 'closeOnClick'
});
});
tcApp.directive('manual', [ '$timeout', '$interval', function($timeout, $interval) {
return {
restrict: 'E',
scope: {},
template:
'<a ' +
' href="#" ' +
' ng-show="isManualVisible" ' +
' ng-click="clickHandler()" ' +
' popover-append-to-body="true" ' +
' popover-placement="bottom" ' +
' popover-trigger="openAfterLoad" ' +
' popover-title="{{title}}" ' +
' popover="{{content}}" ' +
' tooltip="{{title}}" ' +
' >' +
' <span class="{{textClass}}">' +
' <span ng-show="!loading" class="glyphicon glyphicon-question-sign"></span>' +
' <span ng-show="loading" class="glyphicon glyphicon-refresh"></span>' +
' </span>' +
'</a>',
replace: true, // true for IE8 only
link: function (scope, element, attr) {
scope.isManualVisible = true;
scope.title = 'First title value';
scope.content = 'First content value';
scope.textClass = 'text-info';
scope.opened = false;
scope.loading = false;
scope.loaded = false;
scope.loadedTitle = false;
scope.loadedContent = false;
scope.checkLoadState = function () {
if (scope.loadedTitle && scope.loadedContent) {
scope.loaded = true;
if (scope.loading)
scope.loading = false;
scope.$broadcast('openAfterLoad');
}
};
scope.$watch('loadedTitle', scope.checkLoadState);
scope.$watch('loadedContent', scope.checkLoadState);
scope.$on('openAfterLoad', function(event) {
console.log('openAfterLoad');
scope.opened = true;
$timeout(function() {
// trigger popover
element.triggerHandler('openAfterLoad');
});
});
scope.$on('closeOnClick', function(event) {
console.log('closeOnClick');
scope.opened = false;
$timeout(function() {
// trigger popover
element.triggerHandler('closeOnClick');
});
});
scope.clickHandler = function(event) {
console.log('click');
if (scope.loaded) {
if (scope.opened)
scope.$broadcast('closeOnClick');
else
scope.$broadcast('openAfterLoad');
} else {
scope.loadData();
}
};
scope.loadData = function() {
if (!scope.loaded && !scope.loading) {
scope.loading = true;
$interval(function() {
scope.title = 'New title value';
scope.loadedTitle = true;
}, 2000, 1);
$interval(function() {
scope.content = 'New content value';
scope.loadedContent = true;
}, 3000, 1);
}
};
}
};
}]);

Related

Close dropdown onclick outside in AngularJs

var app = angular.module('brandPortalApp');
app.directive('multiselectDropdown', function () {
return {
restrict: 'E',
scope: {
model: '=',
options: '=',
},
template:
"<div class='btn-group' data-ng-class='{open: open}' style='width: 200px;'>" +
"<button class='btn btn-small' style='width: 160px;' data-ng-click='openDropdown1();'>---Select---</button>" +
"<button class='btn btn-small dropdown-toggle' data-ng-click='openDropdown1();' style='width: 40px;' ><span class='caret'></span></button>" +
"<ul class='dropdown-menu' aria-labelledby='dropdownMenu' style='position: relative;'>" +
"<li style='cursor:pointer;' data-ng-repeat='option in options'><a data-ng-click='toggleSelectItem(option)'><span data-ng-class='getClassName(option)' aria-hidden='true'></span> {{option.barcode}}</a></li>" +
"</ul>" +
"</div>",
link: function (scope) {
scope.openDropdown1 = function () {
scope.open = !scope.open;
};
scope.selectAll = function () {
scope.model = [];
angular.forEach(scope.options, function (item, index) {
scope.model.push(item);
});
};
scope.deselectAll = function () {
scope.model = [];
};
scope.toggleSelectItem = function (option) {
var intIndex = -1;
angular.forEach(scope.model, function (item, index) {
if (item.id == option.id) {
intIndex = index;
}
});
if (intIndex >= 0) {
scope.model.splice(intIndex, 1);
} else {
scope.model.push(option);
}
};
scope.getClassName = function (option) {
var varClassName = 'glyphicon glyphicon-remove-circle';
angular.forEach(scope.model, function (item, index) {
if (item.id == option.id) {
varClassName = 'glyphicon glyphicon-ok-circle';
}
});
return (varClassName);
};
}
}
});
Hi.I am new to AngularJs.I created a multiselectDropdown using the above directive.Dropdown works fine.Now on click outside I need to close the dropdown. I have no idea how to proceed.Can anyone help.
to avoide popup close "data-backdrop="static" data-keyboard="false""
try opposite of this, you may get...
You can use clickOutside as below :
return {
restrict: 'A',
scope: {
clickOutside: '&'
},
link: function(scope, el, attr) {
$document.on('click', function(e) {
if (el !== e.target && !el[0].contains(e.target)) {
scope.$apply(function() {
scope.$eval(scope.clickOutside);
});
}
});
}
}
scope.settings = {
closeOnBlur: true
};
if (scope.settings.closeOnBlur) {
document.on('click', function (e) {
var target = e.target.parentElement;
var parentFound = false;
while (angular.isDefined(target) && target !== null && !parentFound) {
if (_.contains(target.className.split(' '), 'multiselect-parent') && !parentFound) {
if (target === dropdownTrigger) {
parentFound = true;
}
}
target = target.parentElement;
}
if (!parentFound) {
scope.$apply(function () {
scope.open = false;
});
}
});
}
Adding this in my directive made my dropdown close.

Angular js directive call with controller data binding

View
<star-rating ratingValue="ratings" readonly="true"></star-rating>
<div><strong>Rating 1:</strong>{{ratings}}</div>
Controller
app.controller('ProductCtrl', function ($scope, $http, $ionicSlideBoxDelegate, $resource, $state, $stateParams, $rootScope, $compile, $ionicPopup, $location, $sce) {
$scope.ratings = 0;
this.isReadonly = true;
this.rateFunction = function(rating) {
console.log('Rating selected: ' + rating);
};
$http.defaults.useXDomain = true;
$http.get(web_service + 'product/get', {
params: {id: $stateParams.ProductId},
headers: {}
}).success(function (response) {
$scope.product = response.product;
console.log(response.product);
$ionicSlideBoxDelegate.update();
$scope.ratings = response.product.rating;
this.rateFunction = function(rating) {
console.log('Rating selected: ' + rating);
};
})
.error(function (err) {
alert("ERROR");
});
}).directive('starRating', starRating);
Directive
function starRating() {
return {
restrict: 'EA',
template:
'<ul class="star-rating" ng-class="{readonly: readonly}">' +
' <li ng-repeat="star in stars" class="star" ng-class="{filled: star.filled}" ng-click="toggle($index)">' +
' <i class="ion-ios-star"></i>' + // or &#9733
' </li>' +
'</ul>',
scope: {
ratingValue: '=?',
max: '=?', // optional (default is 5)
onRatingSelect: '&?',
readonly: '=?'
},
link: function(scope, element, attributes) {
if (scope.max == undefined) {
scope.max = 5;
}
scope.$observe('ratingValue', function(value){
console.log(value);
//$scope.nav.selection = value
});
function updateStars() {
scope.stars = [];
for (var i = 0; i < scope.max; i++) {
scope.stars.push({
filled: i < scope.ratingValue
});
}
};
scope.toggle = function(index) {
if (scope.readonly == undefined || scope.readonly === false){
scope.ratingValue = index + 1;
scope.onRatingSelect({
rating: index + 1
});
}
};
scope.$watch('ratingValue', function(oldValue, newValue) {
if (newValue) {
updateStars();
}
});
}
};
}
When initial value of $scope.ratings is number like 1,2,3 then starts prints but the value retrieved by ajax request is not getting added to directive and in directive values shows "undefined" and no starts getting printed.
The tag below directive in view code gives retrieved value referring to this Codepen: http://codepen.io/TepigMC/pen/FIdHb
What I am missing in directive?
use ng-if so that the directive is called after you have $scope.ratings.
<star-rating ng-if="ratings" ratingValue="ratings" readonly="true"></star-rating>

Angular UI Typeahead filter without options remove

I'm wondering if there's a way to "highlight" the best choice in Angular Typeahead while writing (as the filter does) but without removing all others options:
uib-typeahead="item.prop as item.prop for item in vm.items | filter: $viewValue"
vm.items is just a simple-static-array, no async/remote search.
Thank you!
There is ui-select which is part of angular-ui lib. it has what you need.
I answer my own question with this directive that embeds the Typeahead directive and mimics the functionality of a combobox/select:
js:
.directive('combobox', ['$compile', '$timeout', function($compile, $timeout) {
var directive = {
restrict: 'E',
scope: {
model: "=",
item: "#",
items: "=i",
onSelect: "&",
onBlur: "&"
},
compile: compile
};
return directive;
function compile(element, attrs) {
return function(scope, element, attrs) {
scope.focusing = false;
scope.first = null;
scope.open = function() {
var ctrl = element.find('input').controller('ngModel');
var temp = scope.model;
ctrl.$setViewValue(' ');
$timeout(function() {
ctrl.$setViewValue('');
scope.model = temp;
});
};
scope.select = function(item, model, label) {
scope.focusing = true;
scope.onSelect({item: item, model: model, label: label});
};
scope.blur = function() {
scope.focusing = false;
scope.onBlur();
};
scope.focus = function() {
if (scope.focusing === false) {
scope.focusing = true;
element.find('input')[0].focus();
scope.first = scope.model;
scope.open();
}
};
scope.keyup = function(key) {
if (key === 27) {
scope.model = scope.first;
scope.open();
}
};
var template = '<div class="combo combo-static">' +
' <input ng-model="model"' +
' uib-typeahead="' + attrs.item + '"' +
' typeahead-focus-first="false"' +
' typeahead-min-length="0"' +
' typeahead-editable="false"' +
' typeahead-on-select="select($item, $model, $label)"' +
' ng-focus="focus()"' +
' ng-blur="blur()"' +
' ng-keyup="keyup($event.keyCode)"' +
' placeholder="' + attrs.placeholder + '"' +
' type="text"' +
' class="' + attrs.class + '">' +
' <div class="combo-label" ng-click="focus()" ng-hide="focusing">' +
' <span>{{model || "' + attrs.placeholder + '"}}</span>' +
' <i class="fa fa-caret-down"></i>' +
' </div>' +
'</div>';
element.html(template);
element.removeClass(attrs.class);
$compile(element.contents())(scope);
};
}
}]);
Here is my plunk: http://plnkr.co/edit/zWN950IxOndlYQHBXdY9?p=preview
Hope it could help someone.

Is it okay to have $compile inside the link part of a directive?

I am using AngularJS v1.4.1 and I have the following directive. It was I thought working but now for some reason when my page loads the directive gets called twice. I checked everything I could but I cannot see anything different from before to now except the directive no longer works. Specifically what happens is it appears to be getting called twice.
app.directive('pagedownAdmin', ['$compile','$timeout', function ($compile, $timeout) {
var nextId = 0;
var converter = Markdown.getSanitizingConverter();
converter.hooks.chain("preBlockGamut", function (text, rbg) {
return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
return "<blockquote>" + rbg(inner) + "</blockquote>\n";
});
});
return {
require: 'ngModel',
replace: true,
scope: {
modal: '=modal'
},
template: '<div class="pagedown-bootstrap-editor"></div>',
link: function (scope, iElement, attrs: any, ngModel) {
var editorUniqueId;
if (attrs.id == null) {
editorUniqueId = nextId++;
} else {
editorUniqueId = attrs.id;
}
scope.showPagedownButtons = function () {
document.getElementById("wmd-button-bar-" + editorUniqueId).style.display = 'block';
};
var newElement = $compile(
'<div>' +
'<div class="wmd-panel">' +
'<div data-ng-hide="modal.wmdPreview == true" id="wmd-button-bar-' + editorUniqueId + '" style="display:none;"></div>' +
'<textarea ng-click="showPagedownButtons()" data-ng-hide="modal.wmdPreview == true" class="wmd-input" id="wmd-input-' + editorUniqueId + '">' +
'</textarea>' +
'</div>' +
'<div data-ng-show="modal.wmdPreview == true" id="wmd-preview-' + editorUniqueId + '" class="pagedownPreview wmd-panel wmd-preview"></div>' +
'</div>')(scope);
// iElement.html(newElement);
iElement.append(newElement);
var hide = function () {
document.getElementById("wmd-button-bar-" + editorUniqueId).style.display = 'none';
}
var editor = new Markdown.Editor(converter, "-" + editorUniqueId, {
handler: hide
});
// var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
var $wmdInput = angular.element(document.getElementById("wmd-input-" + editorUniqueId));
var init = false;
editor.hooks.chain("onPreviewRefresh", function () {
var val = $wmdInput.val();
if (init && val !== ngModel.$modelValue) {
$timeout(function () {
scope.$apply(function () {
ngModel.$setViewValue(val);
ngModel.$render();
});
});
}
});
ngModel.$formatters.push(function (value) {
init = true;
$wmdInput.val(value);
editor.refreshPreview();
return value;
});
editor.run();
}
}
}]);
Here is the code that calls the directive:
<textarea data-pagedown-admin
data-modal="cos"
id="contentText"
name="contentText"
ng-minlength="5"
ng-model="cos.content.text"
ng-required="true"></textarea>
When I debug the directive by putting a breakpoint on "var editorUniqueId" then I see it goes there twice.
Does anyone have any ideas what might be happening?
I have done this in the past without issue:
var newElement = '<div>' +
// ... more HTML ...
'</div>';
iElement.append( $compile(newElement)(scope) );

Angular when and how to release DOM to prevent memory leak?

Angular newbie here. I have this custom directive that wraps a table row to show information of a tag. When I click 'edit' button in the row, the directive template will be changed and allow the user to update the tag name, when I click 'apply' button, the row will be changed back with the updated tag name, or if I click 'cancel edit' button, the row will be changed back too without any updates. So the editTag and cancelEditTag event function goes like this:
scope.editTag = function() {
scope.originalTagName = scope.tag.name;
element.html(getTemplate(true));
$compile(element.contents())(scope);
};
scope.cancelEditTag = function() {
scope.tag.name = scope.originalTagName;
element.html(getTemplate(false));
$compile(element.contents())(scope);
scope.tagSubmitError = false;
scope.errorMessage = '';
};
Yet when profiling this app using Chrome dev tool, I realized while switching on and off 'edit mode' by clicking 'edit' and 'cancel edit' button, the memory usage keeps climbing up(about 0.1-0.2mb each time), I think I've got a memory leak here, my guess is that after $compile, the old DOM hasn't been released? If so, how should I deal with it? If this is not the case, what else could be the troublemaker? Or is it not a memory leak at all? For the full context, below is the full code for my directive:
app.directive('taginfo', function($compile ,$http) {
var directive = {};
directive.tagSubmitError = true;
directive.errorMessage = '';
directive.originalTagName = '';
directive.restrict = 'A';
directive.scope = {
tag : '=',
selectedTagIds : '=selected',
};
function getTemplate(isEditing) {
if (isEditing) {
return '<th><input type="checkbox" ng-click="selectTag()" ng-checked="selectedTagIds.indexOf(tag.id) != -1"></th>' +
'<th>' +
'<input type="text" class="form-control" ng-model="tag.name" placeholder="请输入标签名称">' +
'<div class="alert alert-danger" style="margin-top: 5px; " ng-show="tagSubmitError" ng-bind="errorMessage"></div>' +
'</th>' +
'<th><span class="label num-post"><%tag.num_items%></span></th>' +
'<th><button class="action-submit-edit" ng-click="submitEditTag()"><i class="icon-ok-2"></i></button> <button class="action-cancel-edit" ng-click="cancelEditTag()"><i class="icon-ban"></i></button></th>';
} else {
return '<th><input type="checkbox" ng-click="selectTag()" ng-checked="selectedTagIds.indexOf(tag.id) != -1"></th>' +
'<th><%tag.name%></th>' +
'<th><span class="label num-post"><%tag.num_items%></span></th>' +
'<th><button class="action-edit" ng-click="editTag()"><i class="icon-pencil"></i></button> <button class="action-delete"><i class="icon-bin"></i></button></th>';
}
}
directive.template = getTemplate(false);
directive.link = function(scope, element, attributes) {
scope.selectTag = function() {
var index = scope.selectedTagIds.indexOf(scope.tag.id);
if (index == -1) {
scope.selectedTagIds.push(scope.tag.id);
} else {
scope.selectedTagIds.splice(index, 1);
}
};
scope.submitEditTag = function() {
if (scope.tag.name.length === 0) {
scope.tagSubmitError = true;
scope.errorMessage = '请输入标签名称';
} else {
$http.post('/admin/posts/edit_tag', {'tagId': scope.tag.id, 'tagName': scope.tag.name}).success(function(data, status, headers, config) {
if (data.statusCode == 'error') {
scope.tagSubmitError = true;
scope.errorMessage = data.errorMessage;
} else if (data.statusCode == 'success') {
scope.tag.name = data.tag_name;
scope.tagSubmitError = false;
scope.errorMessage = '';
element.html(getTemplate(false));
$compile(element.contents())(scope);
}
});
}
};
scope.editTag = function() {
scope.originalTagName = scope.tag.name;
element.html(getTemplate(true));
$compile(element.contents())(scope);
};
scope.cancelEditTag = function() {
scope.tag.name = scope.originalTagName;
element.html(getTemplate(false));
$compile(element.contents())(scope);
scope.tagSubmitError = false;
scope.errorMessage = '';
};
};
return directive;
});
Any help will be appreciated, thanks in advance!
So, I've figured a way to not compile directive template dynamically, thus to avoid memory usage climbing up. That is to add a boolean flag named 'isEditMode' which will be used in ng-if to decide which DOM to show, and the source code is follows:
app.directive('taginfo', function($http, $animate, listService) {
var directive = {};
directive.editTagSubmitError = false;
directive.errorMessage = '';
directive.originalTagName = '';
directive.restrict = 'A';
directive.isEditMode = false;
directive.scope = {
tag : '=',
pagination : '=',
data : '='
};
directive.template = '<th><input type="checkbox" ng-click="selectTag()" ng-checked="data.selectedIds.indexOf(tag.id) != -1"></th>' +
'<th ng-if="isEditMode">' +
'<input type="text" class="form-control" ng-model="tag.name" placeholder="请输入标签名称">' +
'<div class="alert alert-danger" style="margin-top: 5px; " ng-show="editTagSubmitError" ng-bind="errorMessage"></div>' +
'</th>' +
'<th ng-if="!isEditMode"><%tag.name%></th>' +
'<th><span class="label num-posts"><%tag.num_items%></span></th>' +
'<th ng-if="isEditMode"><button class="action-submit-edit" ng-click="submitEditTag()"><i class="icon-ok-2"></i></button> <button class="action-cancel-edit" ng-click="cancelEditTag()"><i class="icon-ban"></i></button></th>' +
'<th ng-if="!isEditMode"><button class="action-edit" ng-click="editTag()"><i class="icon-pencil"></i></button> <button class="action-delete" ng-click="deleteTag()"><i class="icon-bin"></i></button></th>';
directive.link = function(scope, element, attributes) {
scope.selectTag = function() {
listService.selectEntry(scope.tag, scope.data);
};
scope.submitEditTag = function() {
if (!scope.tag.name) {
scope.editTagSubmitError = true;
scope.errorMessage = '请输入标签名称';
} else {
bootbox.confirm('是否确定修改标签名称为' + scope.tag.name +'?', function(result) {
if (result === true) {
$http.post('/admin/posts/edit_tag', {'tagId': scope.tag.id, 'tagName': scope.tag.name}).success(function(response, status, headers, config) {
if (response.statusCode == 'error') {
scope.editTagSubmitError = true;
scope.errorMessage = response.errorMessage;
} else if (response.statusCode == 'success') {
scope.isEditMode = false;
scope.tag.name = response.tag_name;
scope.editTagSubmitError = false;
scope.errorMessage = '';
$animate.removeClass(element, 'editing');
}
});
}
});
}
};
scope.editTag = function() {
scope.isEditMode = true;
scope.originalTagName = scope.tag.name;
if (!element.hasClass('editing')) {
element.addClass('editing');
}
};
scope.cancelEditTag = function() {
scope.isEditMode = false;
scope.tag.name = scope.originalTagName;
scope.editTagSubmitError = false;
scope.errorMessage = '';
};
scope.deleteTag = function() {
listService.deleteEntry(scope.tag, scope.tag.name, scope.data, '/admin/posts/delete_tag', scope.pagination, 'tag');
};
};
return directive;
});
This way, the directive template will not be compiled for editing/non-editing mode repeatedly but only show different DOM based on 'ng-if="isEditMode"'. It solved my problem. Yet I am still wondering if there's a way to remove memory leak for dynamic directive template compilation. Any thoughts would be appreciated.

Resources