I'm using angular directive with jquery datatables, in the mRender function I used the following code to render row actions:
datatables mRender function:
var renderRowActions = function (data, type, row) {
//var markup = $('#rowActions').html().replace(/{rowId}/g, row[0]);
var markup = '<div row-actions action="delete(" + row[0] + ")" ></div>';
return markup;
};
directive code:
app.directive('rowActions', function () {
return {
restrict: 'A',
replace: true,
template: '<button data-ng-click="action()"></button>',
scope: {
action: "&"
}
};
});
This question is old as dirt, but here goes.... For my directive called "spinner":
{
className: 'spinner-control',
orderable: false,
data: null,
render: function (data, type, row, meta) {
return '<spinner ng-model="accounts['+meta.row+'].alloc" ng-format="percentage" width="75px" step="0.5"></spinner>';
}
}
You can then update the jQuery after the fact and replace the innerHTML with the compiled element. Code is written "long form" to make it obvious what it's doing.
$('.spinner-control').each(function(){
var innerHTML = $(this);
console.log("InnerHTML", innerHTML);
var html = $compile(innerHTML)($scope);
console.log("HTML", html);
$(this).innerHTML = '';
$(this).append(html);
});
Related
I am using modals to display forms that are presented to users. The forms are all components with an onSave and onCancel method
bindings: {
entity: '<',
readOnly: '<',
onSave: '&',
onCancel: '&'
},
I want to pass into a modal the tag of the form to present in the modal and then pass the parameters returned by the component's onSave/onCancel event back to the modal which will return it to the caller. To do this, I am putting an directive that sets the properties of the component and then runs it through the $compile method to generate it:
function link(scope, elem, attrs) {
if (scope.formType != null && scope.formType != '') {
var objString = "<" + scope.formType + " "; //create beginning tag
objString += "entity='assignedEntity' read-only='readOnly' ";
objString += "on-save='onSave(entity)' on-cancel='onCancel(entity)'>";
objString += "</" + scope.formType + ">"; //add end tag
var obj = $compile(objString)(scope);
elem.append(obj);
}
};
return {
restrict: 'E',
scope: {
formType: '=',
onSave: '&',
onCancel: '&',
assignedEntity: '<',
readOnly: '<'
},
link: link
}
I then call the directive and pass the appropriate properties from a generic modal box like so:
<form-generator
form-type="vm.ui.properties.formType"
on-save="vm.ok(entity)"
on-cancel="vm.cancel(entity)"
assigned-entity="vm.returnItem.entity"
read-only="vm.ui.properties.readOnly">
</form-generator>
This successfully generates the specified form component and passes in the right values for each property down to the form component. My issue is that when the onSave or onCancel events are thrown by the component, the modal controller is receiving the event (vm.ok or vm.cancel gets called) but the parameters passed to those events are not passed up with the call. the properties passed to vm.ok and vm.cancel are always undefined.
From the component, I am calling the onSave/onCancel method like this:
ctrl.onSave({
entity: ctrl.entity
});
and I have verified that ctrl.entity does in fact have a value in it.
Any thoughts as to why the parameters passed back up the call tree are undefined by the time it gets to the modal controller?
I created this plunkr to help define the problem I am having: Example
Please review the code, after a bit of debugging it seems like you just forgot to attach the entity as a part of the function that listens for click $event. Here's the working plunker.
(function() {
var directiveID = "formGenerator";
angular.module('app').directive(directiveID, ['$compile', FormGenerator]);
function FormGenerator($compile) {
function link(scope, elem, attrs) {
console.log(scope, elem, attrs);
if (scope.formType !== null && scope.formType !== '') {
var objString = "<" + scope.formType + " "; //create beginning tag
//PLEASE TAKE A LOOK HERE, WE'RE EXPECTING THE EVENT TO PROPOGATE TO THE PARENT CONTROLLER
//so we take into account the event on-save, the same would have to be done for on-cancel
objString += "on-save='onFormSave($event)' on-cancel='onFormCancel(entity)'>";
objString += "</" + scope.formType + ">"; //add end tag
var obj = $compile(objString)(scope);
elem.append(obj);
}
}
return {
restrict: 'E',
scope: {
formType: '=',
onFormSave: '&',
onFormCancel: '&'
},
link: link
}
}
})();
(function() {
var componentID = "testForm";
var app = angular.module("app");
function TestFormController() {
var ctrl = this;
ctrl.entity = {
name: "this is the entity passed up"
};
ctrl.save = function(event) {
console.log(event);
console.log("In component: " + ctrl.entity.name);
ctrl.onSave({
//AND ON SAVE, we make the this controllers model-properties which you'd like to pass on a part of the event.
$event: {
entity: ctrl.entity
}
});
};
ctrl.cancel = function() {
ctrl.onCancel({
entity: ctrl.entity
});
};
}
app.component(componentID, {
bindings: {
onSave: '&',
onCancel: '&'
},
//Here also we pass the $event to function
template: '<h1> This is a test</h1><button type="button" ng-click="$ctrl.save($event);">Save</button>',
controller: TestFormController
})
}());
I need in a angularjs single page application a google-places autocomplete input, that shall run as a service and shall be initialized once at runtime. In case of navigation, the with goolge-places initialized element and the appropriate scope are destroyed.
I will re-use the places input field after navigate to the page containing the places autocomplete input field. With the method element.replaceWith() it works well.
After replacing the element, I can not reset the input by the "reset" button. How can I bind the new generated scope to the "reset" button and the old scope variables. Because the old scope and elements are destroyed by the navigation event?
.factory('myService', function() {
var gPlace;
var s, e;
var options = {
types: [],
componentRestrictions: {country: 'in'}
};
function init() {
}
function set(element, scope) {
console.log('set');
if (!gPlace) {
e = element;
gPlace = new google.maps.places.Autocomplete(element[0], options);
google.maps.event.addListener(gPlace, 'place_changed', function() {
scope.$apply(function() {
scope.place.chosenPlace = element.val();
});
});
} else {
element.replaceWith(e);
}
}
init();
return {
'init':init,
'set':set
};
});
the navigation (element and scope destroying) will be simulated in this plunk by the ng-if directive that will be triggered by the "remove" button.
see here plunk
If you want you can create a service that holds the last selected place and shares it among controllers and directives:
.service('myPlaceService', function(){
var _place;
this.setPlace = function(place){
_place = place;
}
this.getPlace = function(){
return _place;
}
return this;
});
Then create a directive that uses this service:
.directive('googlePlaces', function(myPlaceService) {
return {
restrict: 'E',
scope: {
types: '=',
options: '=',
place: '=',
reset: '='
},
template: '<div>' +
'<input id="gPlaces" type="text"> <button ng-click="resetPlace()">Reset</button>' +
'</div>',
link: function(scope, el, attr){
var input = document.querySelector('#gPlaces');
var jqEl = angular.element(input);
var gPlace = new google.maps.places.Autocomplete(input, scope.options || {});
var listener = google.maps.event.addListener(gPlace, 'place_changed', function() {
var place = autocomplete.getPlace();
scope.$apply(function() {
scope.place.chosenPlace = jqEl.val();
//Whenever place changes, update the service.
//For a more robust solution you could emit an event using scope.$broadcast
//then catch the event where updates are needed.
//Alternatively you can $scope.$watch(myPlaceService.getPlace, function() {...})
myPlaceService.setPlace(jqEl.val());
});
scope.reset = function(){
scope.place.chosenPlace = null;
jqEl.val("");
}
scope.$on('$destroy', function(){
if(listener)
google.maps.event.removeListener(listener);
});
});
}
}
});
Now you can use it like so:
<google-places place="vm.place" options="vm.gPlacesOpts"/>
Where:
vm.gPlacesOpts = {types: [], componentRestrictions: {country: 'in'}}
I am trying to develop a friendly dialog plugin with angularjs & bootstrap.
I found dialog based in directive was not that easy to use, add a html tag first and define controller and variable, that is too complicated.
So I intend to add a method to angular, create new element dynamic and set variable to root scope, but there seems to be some problems about compile.
Here is mainly code:
var defaultOption = {
title: 'Title',
content: 'Content',
backdrop: false,
buttons: [],
$dom: null
};
var prefix = '__dialog',
index = 0;
var newId = function (scope) {
var id = prefix + index;
if (!scope[id]) return id;
index++;
return arguments.callee(scope);
};
var app = angular.module("app", []);
app.directive('bootstrapModal', ['$compile', function ($compile) {
return {
restrict: 'A',
replace: true,
scope: {
bootstrapModal: '='
},
link: function (scope, $ele) {
var $dom = $(defaultTemplate),
body = $ele.children();
if (body.length) $dom.find('.modal-body').html(body);
$ele.replaceWith($dom);
$compile($dom)(scope);
scope.bootstrapModal.$dom = $dom;
}
};
}]);
angular.ui = {};
angular.ui.dialog = function (args) {
var $body = angular.element(document.body),
$rootScope = $body.scope().$root,
option = $.extend({}, defaultOption, args);
option.id = option.id || newId($rootScope);
option.show = function () {
this.$dom.modal('show');
};
option.hide = function () {
this.$dom.modal('hide');
};
$body.injector().invoke(function ($compile) {
$rootScope[option.id] = option;
var dialog = '<div bootstrap-modal="{0}"></<div>'.format(option.id);
var html = $compile(dialog)($rootScope);
$('body').append(html);
});
return option;
};
$(function () {
angular.ui.dialog({ //test
title: 'Alert',
content: 'test content for alert',
buttons: [{
name: 'Close',
focus: true
}]
}).show();
});
The entire code is too long, so I put it in JSFIDDLE
Thanks!
Put a $rootScope.$apply(function() { ... }) around your code where you compile your dialog in injector().invoke(...).
Updated fiddle
I use angularjs with "ui_select2" directive. Select2 draws new tags with formatting function, there are "" elements with "ng-click" attribute. How to tell angularjs about new DOM elements? Otherwise new "ng-clicks" wont work.
HTML:
<input type="text" name="contact_ids" ui-select2="unit.participantsOptions" ng-model="unit.contactIds" />
JS (angular controller):
anyFunction = function(id) {
console.log(id)
}
formatContactSelection = function(state) {
return "<a class=\"select2-search-choice-favorite\" tabindex=\"-1\" href=\"\" ng-click=\"anyFunction(state.id)\"></a>"
}
return $scope.unit.participantsOptions = {
tags: [],
formatSelection: formatContactSelection,
escapeMarkup: function(m) {
return m
},
ajax: {
url: '/contacts/search',
quietMillis: 100,
data: function(term, page) {
return {
term: term,
limit: 20,
page: page
}
},
results: function(data, page) {
return {
results: data,
more: (page * 10) < data.total
}
}
}
}
The problem is that select2 creates DOM elements, that not yet discovered by angularjs, I read that new DOM elements need to be appended to some element with using angularjs $compile function, but I cannot use it in controller.
I found a solution - I created the directive, that watches for changes in ngModel and apply it on the element, that has already ui_select2 directive. "uiRequiredTags" implements custom behavior I need for my select2 tag choices. The solution is to watch changes in ngModel attribute.
angular.module("myAppModule", []).directive("uiRequiredTags", function() {
return {
restrict: 'A',
require: "ngModel",
link: function(scope, el, attrs) {
var opts;
opts = scope.$eval("{" + attrs.uiRequiredTags + "}");
return scope.$watch(attrs.ngModel, function(val) {
var $requireLink;
$requireLink = el.parent().find(opts.path);
$requireLink.off('click');
$requireLink.on('click', function() {
var id, n, tagIds;
id = "" + ($(this).data('requiredTagId'));
if (opts.removeLinkPath && opts.innerContainer) {
$(this).parents(opts.innerContainer).find(opts.removeLinkPath).data('requiredTagId', id);
}
tagIds = scope.$eval(opts.model).split(',');
n = tagIds.indexOf(id);
if (n < 0) {
tagIds.push(id);
} else {
tagIds.splice(n, 1);
}
scope.$eval("" + opts.model + " = '" + tagIds + "'");
scope.$apply();
return $(this).toggleClass('active');
});
});
}
};
I have this directive, which is basicly wrapper for typeahead plugin from bootstrap. Everything is working like a charm. But now I have to set initial vaule in typeahead's input field. The value is passed as a string in attrs.init. But I don't know how to insert it into text field.
angular.module('rcApp')
.directive('rcAutocomplete', ['$injector', function ($injector) {
return {
scope: {
model: '#',
search: '#',
key: '#',
show: '#',
init: '#',
ngModel: '='
},
template: '<input type="text">',
replace: true,
restrict: 'E',
require: 'ngModel',
link: function (scope, element, attrs) {
// inject model service
var service = $injector.get(attrs.model);
// define search function
var searchFunction = attrs.search;
// holds picked object id
scope.id = 0;
// holds objects that matched query, mapped by "show" value
scope.map = {};
element.on('focusout ac.itempicked', function () {
scope.$apply(function () {
scope.ngModel = scope.id;
});
});
// launch typehead plugin
element.typeahead(
{
source: function (query, process) {
// clear cache
scope.id = 0;
scope.map = {};
service[searchFunction](query).then(function (result) {
var dataValues = [];
var fieldsToShow = scope.show.split('|');
$.each(result.data, function (index, dataItem) {
// generate key-show string
var valueHash = '';
for (var i = 0; i < fieldsToShow.length; i++) {
valueHash += dataItem[fieldsToShow[i]] + ' ';
}
valueHash = $.trim(valueHash);
// map results
scope.map[valueHash] = dataItem;
// prepare return strings
dataValues.push(valueHash);
});
// return content
process(dataValues);
});
},
updater: function (item) {
if (typeof scope.key === 'undefined') {
scope.id = scope.map[item];
}
else {
scope.id = scope.map[item][scope.key];
}
element.trigger('ac.itempicked');
return item;
}
}
);
}
};
}]);
** UPDATE **
Solution, that worked for me is adding code like this to link function:
// init value
if (typeof attrs.init !== 'undefined') {
window.setTimeout(function () {
element.val(attrs.init);
scope.$apply();
}, 10);
}
But still I don't quite understand why "element.val(attrs.init);" don't updated the view, and calling scope.$apply() did, but throw an "$digest already in progress" error. Wrapping it in window.setTimeout helped, but this also is a hack for me....
It's got to be a way to make it cleaner/simpler...