Transcluding nested directive gets rendered twice - angularjs

I have created two directives, one for simple text-input box (apt-field), one for bootstrap panel with tabbed-view (apt-panel-with-tab).
Here is the Plunker link:
http://plnkr.co/edit/CsxOWGXVzkQugdmpdMyl
In the code, you will see I have used apt-field alone which works just fine, and other within apt-panel-with-tab directive in which apt-field directive gets rendered twice.
I have tried it with both Angular 1.4.8 and 1.5.0-rc.0 and the same result.
Would anyone have any clue what I have been doing wrong? Or could this be a bug in AngularJS?
PS:
SO does not allow me to put plunker link without accompanying code, so here is a portion of the code available on the plunker site:
app.directive('aptField', [
function() {
var directiveObject = {
restrict: 'E',
compile: function(elem, attrs) {
var tpl = null;
var bindTo = null;
if (attrs.field) {
bindTo = 'formData.' + attrs.field;
if (angular.isDefined(attrs.modelBase)) {
bindTo = attrs.modelBase + '.' + attrs.field;
if (bindTo.indexOf('.') == 0) {
bindTo = bindTo.substr(1);
}
}
}
var control = {
tag: 'input',
attrs: {
type: 'text',
class: 'form-control'
},
selfClose: true,
formify: true
};
var tpl = '<' + control.tag;
for (var attr in control.attrs) {
tpl += ' ' + attr + '="' + control.attrs[attr] + '"';
}
tpl += control.selfClose ? ' />' : '></' + control.tag + '>';
var $tpl = angular.element(tpl);
if (bindTo) {
$tpl.attr('data-ng-model', bindTo);
}
elem.append($tpl);
}
};
return directiveObject;
}
]);
Thanks.

Related

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) );

generating a variable-length dropdown based on input value in an AngularJS directive

I want to provide a page selection directive that generates "Page [ 1 ] of x". The number of pages in the dropdown is dependent upon values passed into the directive, so it can't be part of a static template. I'm having a difficult time figuring out how/where to generate the <select><option>...</select>.
I have tried, unsuccessfully, to do it via:
an $observe (and $watch) in link
a function added to $scope in controller, which returns $compile(markup)($scope) (This gives the error Error: [$parse:isecdom] Referencing DOM nodes in Angular expressions is disallowed!)
a sub-directive for the <select> element (The link $observer never seemed to get the recordCount updates, regardless of inherited or shared scope.)
ng-repeat in the template
Here's my mangled code, as it currently stands.
HTML
<x-pager
record-count="{{recordCount}}"
page-size="pageSize"
page-number="pageNumber"
set-page="selectPage(page)"
></x-pager>
JS
module.directive("pager", ["$compile",
function ($compile)
{
return {
template: "<div class='pager' ng-show='recordCount > pageSize'>\
{{recordCount}} results\
<button>« Prev</button>\
page <select>\
<option>#</option>\
</select> of {{calcPages()}}\
<button>Next »</button>\
</div>",
replace: true,
restrict: "E",
scope: {
recordCount: "#",
pageSize: "=",
pageNumber: "=",
setPage: "&"
},
link: function (scope, element, attrs)
{
/*
* We can't build the page selection dropdown until
* we know how many records we have. Register an
* observer to do this when recordCount changes.
*/
attrs.$observe("recordCount", function (recCnt)
{
var html;
var pages;
var i;
if (angular.isDefined(recCnt)) {
html = "<select>\n";
pages = Math.ceil(scope.recordCount / scope.pageSize);
for (i=1; i<=pages; i++) {
html += " <option value='" + i + "'>" + i + "</option>\n";
}
html += "</select>";
console.log("generatePageSelect html", html);
html = $compile(html)(scope);
// add the template content
// angular.element("select.page-selector").html(html);
// template: page <select class='page-selector'></select> of {{calcPages()}}\
}
});
},
controller: function ($scope)
{
$scope.calcPages = function ()
{
return Math.ceil($scope.recordCount / $scope.pageSize);
};
function genPagesArray ()
{
var pages = $scope.calcPages();
var i;
var pagesArray = [];
for (i=0; i<pages; i++) {
pagesArray.push(i);
}
return pagesArray;
}
$scope.pagesArray = genPagesArray();
console.log("$scope.pagesArray", $scope.pagesArray);
// template: page {{generatePageSelect()}} of {{calcPages()}}\
$scope.generatePageSelect = function ()
{
var html = "<select>\n";
var pages = $scope.calcPages();
var i;
for (i=1; i<=pages; i++) {
html += " <option value='" + i + "'>" + i + "</option>\n";
}
html += "</select>";
return $compile(html)($scope);
};
}
};
}
]);
To expand on my comment from earlier, here's a directive that does (most of) what you want it to do.
angular.module('Test', []).controller('TestCtrl', function($scope) {
$scope.pageSize = 10;
$scope.pageNumber = 1;
$scope.recordCount = 30;
}).directive("pager", function () {
return {
template: '<div class="pager" ng-show="recordCount > pageSize">\
{{recordCount}} results\
<button ng-click="pageNumber = pageNumber - 1" ng-disabled="pageNumber <= 1">« Prev</button>\
page <select ng-model="pageNumber" ng-options="i for i in pages"></select> of {{totalPages}}\
<button ng-click="pageNumber = pageNumber + 1" ng-disabled="pageNumber >= totalPages">Next »</button>\
</div>',
replace: true,
restrict: "E",
scope: {
recordCount: "#",
pageSize: "=",
pageNumber: "=",
setPage: "&"
},
link: function (scope, element, attrs) {
attrs.$observe("recordCount", function (count) {
if (angular.isDefined(count)) {
scope.recordCount = parseInt(count);
var i;
scope.totalPages = Math.ceil(scope.recordCount / scope.pageSize);
scope.pages = [];
for (i=1; i<=scope.totalPages; i++) {
scope.pages.push(i);
}
}
});
}
}
});
Plunkr here.

Angularjs, select2 with dynamic tags and onclick

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');
});
});
}
};

How to create a tree list in Angular with ajax loading of each new layer?

I'm new to Angular and trying to wrap my brain around how to do stuff.
I'm trying to create a list that's populated on page load by an ajax call to a REST API service, and the elements of that list would fire ajax calls to that same service when clicked on that populated sub-lists below those elements, and so on to depth n.
The initial population of the list is easy: the controller makes the ajax call, gets the JSON object, assigns it to scope and the DOM is handled with a ng-repeat directive. I'm having trouble with the subsequent loading of the sub-lists.
In jQuery, I would have a function tied to each appropriately classed element clicked upon via onClick which would get the required parameters, take the JSON output, parse it into HTML and append that HTML after the element which fired the event. This is direct DOM manipulation and therefore Angular heresy.
I've already looked at this question here, but I still don't quite understand how to implement something like this "the Angular way".
Help?
Edit: Solved this problem by making a recursive directive. Instructions from here: http://jsfiddle.net/alalonde/NZum5/light/.
Code:
var myApp = angular.module('myApp',[]);
myApp.directive('uiTree', function() {
return {
template: '<ul class="uiTree"><ui-tree-node ng-repeat="node in tree"></ui-tree-node></ul>',
replace: true,
transclude: true,
restrict: 'E',
scope: {
tree: '=ngModel',
attrNodeId: "#",
loadFn: '=',
expandTo: '=',
selectedId: '='
},
controller: function($scope, $element, $attrs) {
$scope.loadFnName = $attrs.loadFn;
// this seems like an egregious hack, but it is necessary for recursively-generated
// trees to have access to the loader function
if($scope.$parent.loadFn)
$scope.loadFn = $scope.$parent.loadFn;
// TODO expandTo shouldn't be two-way, currently we're copying it
if($scope.expandTo && $scope.expandTo.length) {
$scope.expansionNodes = angular.copy($scope.expandTo);
var arrExpandTo = $scope.expansionNodes.split(",");
$scope.nextExpandTo = arrExpandTo.shift();
$scope.expansionNodes = arrExpandTo.join(",");
}
}
};
})
.directive('uiTreeNode', ['$compile', '$timeout', function($compile, $timeout) {
return {
restrict: 'E',
replace: true,
template: '<li>' +
'<div class="node" data-node-id="{{ nodeId() }}">' +
'<a class="icon" ng-click="toggleNode(nodeId())""></a>' +
'<a ng-hide="selectedId" ng-href="#/assets/{{ nodeId() }}">{{ node.name }}</a>' +
'<span ng-show="selectedId" ng-class="css()" ng-click="setSelected(node)">' +
'{{ node.name }}</span>' +
'</div>' +
'</li>',
link: function(scope, elm, attrs) {
scope.nodeId = function(node) {
var localNode = node || scope.node;
return localNode[scope.attrNodeId];
};
scope.toggleNode = function(nodeId) {
var isVisible = elm.children(".uiTree:visible").length > 0;
var childrenTree = elm.children(".uiTree");
if(isVisible) {
scope.$emit('nodeCollapsed', nodeId);
} else if(nodeId) {
scope.$emit('nodeExpanded', nodeId);
}
if(!isVisible && scope.loadFn && childrenTree.length === 0) {
// load the children asynchronously
var callback = function(arrChildren) {
scope.node.children = arrChildren;
scope.appendChildren();
elm.find("a.icon i").show();
elm.find("a.icon img").remove();
scope.toggleNode(); // show it
};
var promiseOrNodes = scope.loadFn(nodeId, callback);
if(promiseOrNodes && promiseOrNodes.then) {
promiseOrNodes.then(callback);
} else {
$timeout(function() {
callback(promiseOrNodes);
}, 0);
}
elm.find("a.icon i").hide();
var imgUrl = "http://www.efsa.europa.eu/efsa_rep/repository/images/ajax-loader.gif";
elm.find("a.icon").append('<img src="' + imgUrl + '" width="18" height="18">');
} else {
childrenTree.toggle(!isVisible);
elm.find("a.icon i").toggleClass("icon-chevron-right");
elm.find("a.icon i").toggleClass("icon-chevron-down");
}
};
scope.appendChildren = function() {
// Add children by $compiling and doing a new ui-tree directive
// We need the load-fn attribute in there if it has been provided
var childrenHtml = '<ui-tree ng-model="node.children" attr-node-id="' +
scope.attrNodeId + '"';
if(scope.loadFn) {
childrenHtml += ' load-fn="' + scope.loadFnName + '"';
}
// pass along all the variables
if(scope.expansionNodes) {
childrenHtml += ' expand-to="expansionNodes"';
}
if(scope.selectedId) {
childrenHtml += ' selected-id="selectedId"';
}
childrenHtml += ' style="display: none"></ui-tree>';
return elm.append($compile(childrenHtml)(scope));
};
scope.css = function() {
return {
nodeLabel: true,
selected: scope.selectedId && scope.nodeId() === scope.selectedId
};
};
// emit an event up the scope. Then, from the scope above this tree, a "selectNode"
// event is expected to be broadcasted downwards to each node in the tree.
// TODO this needs to be re-thought such that the controller doesn't need to manually
// broadcast "selectNode" from outside of the directive scope.
scope.setSelected = function(node) {
scope.$emit("nodeSelected", node);
};
scope.$on("selectNode", function(event, node) {
scope.selectedId = scope.nodeId(node);
});
if(scope.node.hasChildren) {
elm.find("a.icon").append('<i class="icon-chevron-right"></i>');
}
if(scope.nextExpandTo && scope.nodeId() == parseInt(scope.nextExpandTo, 10)) {
scope.toggleNode(scope.nodeId());
}
}
};
}]);
function MyCtrl($scope, $timeout) {
$scope.assets = [
{ assetId: 1, name: "parent 1", hasChildren: true},
{ assetId: 2, name: "parent 2", hasChildren: false}
];
$scope.selected = {name: "child 111"};
$scope.hierarchy = "1,11";
$scope.loadChildren = function(nodeId) {
return [
{assetId: parseInt(nodeId + "1"), name: "child " + nodeId + "1", hasChildren: true},
{assetId: parseInt(nodeId + "2"), name: "child " + nodeId + "2"}
];
}
$scope.$on("nodeSelected", function(event, node) {
$scope.selected = node;
$scope.$broadcast("selectNode", node);
});
}
Template:
<div ng-controller="MyCtrl">
<ui-tree ng-model="assets" load-fn="loadChildren" expand-to="hierarchy" selected-id="111" attr-node-id="assetId"></ui-tree>
<div>selected: {{ selected.name }}</div>
</div>
Here's a solution which I prototyped for my own use.
https://embed.plnkr.co/PYVpWYrduDpLlsvto0wR/
Link updated

angular directive - interpolation issues when calling a function in an attribute in the template function that is defined in the link method

Ok, learning angular directives at the moment and became stuck with one problem.
I have a directive where in the linking function there is a function created that is linked to the scope which is called on the template function as an attribute in a html element. It is also called in-between the html tags as well. Neither of the tags update. I suspect it is not compiled yet or something. However, I can not understand the documentation as it seems to talk about too many things at once and doesn't make much sense. Or they have omitted some key information.
Either way below is a snapshot of my code.
{{labelText()}} does not update/interpolate. Any help with a solution would be great!
var app = angular.module('mmApp', []);
app.directive('mmField', function(){
return {
'restrict': 'E',
'priority': 5,
'replace': true,
'scope': {
'path': '#',
'label': '#',
'type': '#',
'editable': '#'
},
'template': '<div class="mm-field">' +
'<label for="{{inputId()}}" ng-show="labelText()">{{labelText()}}</label> ' +
'</div>',
'link': function (scope, element, attrs) {
var query = null;
//this is where the labelText() function is defined
scope.labelText = function () {
var labelAttrValue = (scope.label || attrs['withLabel'] || '');
// cater for custom labels specified via the label or with-label attribute
if (labelAttrValue && labelAttrValue.toLowerCase() !== 'true' && labelAttrValue.toLowerCase() !== 'false') {
return (labelAttrValue || '') + ':';
} else if (labelAttrValue.toLowerCase() !== 'false' && scope.field) {
return (scope.field['name'] || 'FIELD_NAME_NOT_DEFINED') + ':';
} else if (labelAttrValue.toLowerCase() == 'false') {
return '';
} else {
return 'Loading...';
}
};
}
}
])
My test function is below. Using mocha and chai testing suite.
describe('LabelText', function () {
it('should compile and run the directive and interpolate labelText()', function () {
var element = $($compile('<div id="#test">' +
'<mm-field with-label="MONKEY" editable="false" path="`enter something.name"></mm-field>' +
'</div>')($scope));
$scope.$digest();
expect(element.find('label').attr('ng-show')).to.not.equal(labelText()); //test fails and equals labelText()
});
});
I think it's merely syntax problem somewhere.
I copied your code and ran it in my local. I got it to work but only after fixing some mismatched curly brackets and removed all of the dependency injections you put in there, as none of them is used in the link function.
directive('mmField', function () {
return {
'restrict': 'E',
'priority': 5,
'replace': true,
'scope': {
'path': '#',
'label': '#'
},
'template': '<div class="mm-field">' +
'<label for="{{inputId()}}" ng-class="labelClass()" ng-show="labelText()">{{labelText()}}</label> ' +
'<span class="field-value" ng-class="spanClass()" ng-click="handleSpanClick($event)" ng-show="!error">{{displayValue}} <span class="units" ng-show="getUnits()">{{getUnits()}}</span></span> ' +
'</div>',
'link': function (scope, element, attrs) {
var query = null;
scope.labelText = function () {
var labelAttrValue = (scope.label || attrs['withLabel'] || '');
if (scope.error) return scope.error;
// cater for custom labels specified via the label or with-label attribute
if (labelAttrValue && labelAttrValue.toLowerCase() !== 'true' && labelAttrValue.toLowerCase() !== 'false') {
return (labelAttrValue || '') + ':';
} else if (labelAttrValue.toLowerCase() !== 'false' && scope.field) {
return (scope.field['name'] || 'FIELD_NAME_NOT_DEFINED') + ':';
} else if (labelAttrValue.toLowerCase() == 'false') {
return '';
} else {
return 'Loading...';
}
};
scope.inputId = function () {
return scope.path.replace(/[^a-z0-9]/gi, '_');
};
}
}
});

Resources