I am trying to create treeview directive with isolated scope.
But node selection not working properly:
Directive
(function(l) {
l.module("angularTreeview", []).directive("treeModel",
function($compile) {
return {
restrict: "A",
scope: {
treeModel: '=',
currentNode: '='
},
link: function(scope, element) {
k = '<ul><li data-ng-repeat="node in treeModel"><i class="collapsed" data-ng-show="node.children.length && node.collapsed" data-ng-click="selectNodeHead(node)"></i><i class="expanded" data-ng-show="node.children.length && !node.collapsed" data-ng-click="selectNodeHead(node)"></i><i class="normal" data-ng-hide="node.children.length"></i> <span data-ng-class="node.selected" data-ng-click="selectNodeLabel(node)">{{node.roleName}}</span><div data-ng-hide="node.collapsed" data-tree-model="node.children" data-node-id="roleId" data-node-label="roleName" data-current-node="currentNode" data-node-children="children"></div></li></ul>';
scope.$watch('treeModel', function() {
element.empty().html($compile(k)(scope))
}),
scope.selectNodeLabel = function(q) {
scope.currentNode && scope.currentNode.selected && (scope.currentNode.selected = false);
q.selected = "selected";
scope.currentNode = q
};
}
}
})
})(angular);
Problem Fiddle
Any help would appreciated, Thanks.
Problem is because you compile the directive multiple times, you have different child scopes for each time you compile the directive. Now it's not a real problem but every child node change to currentNode changes the reference but does not update all of its parents scopes, meaning the original currentNode is not updated.
Instead you can access the currentNode through an object which will guarantee that every change will keep the referencs as they are:
// controller
$scope.obj = { currentNode :{} };
// directive
scope.currentNode.currentNode=q; // every line with scope.currenNode becomes scope.currentNode.currentNode for example.
See this fiddle
Related
I'm trying to create an angular directive for forming sentences. The goal is to take a list and iterate through them as necessary. The result of the directive would be something like:
shoes, pants and socks
or
shoes, pants and +5 more
I have the basic directive setup to work with a array of strings - but I'd like to customize it to allow custom templates for each sentence element (i.e. hyperlinks, styling, etc). That is:
<sentence values="article in articles">
<strong>{{article.title}}</strong> by <span>{{article.author}}</span>
</sentence>
The HTML the user sees in the browser needs to be something like:
$scope.articles = [
{ title: '...', author: '...'},
{ title: '...', author: '...'},
...
]
<span><strong>ABC</strong> by <span>123</span></span>
<span>, </span>
<span><strong>DEF</strong> by <span>456</span></span>
<span>and</span>
<span>+5 more</span>
I'm guessing it has something to do with transclude but cannot figure out the API. I've also experimented with using ng-repeat instead of the directive template but wasn't able to find a solution.
Something like this should work where maxArticles is a number defined on your scope
<sentence values="article in articles | limitTo: maxArticles">
<strong>{{article.title}}</strong> by <span>{{article.author}}</span>
<span ng-if="$index < maxArticles - 2">, </span>
<span ng-if="$index === articles.length - 1 && articles.length <= maxArticles">and</span>
</sentence>
<span ng-if="articles.length > maxArticles">
and +{{articles.length - maxArticles}} more.
</span>
Iterating AND providing dynamic content is a common use for a custom directive with the compile function + the $compile service. Watch out: essentially you are repeating the functionality of ng-repeat, you may want to consider alternatives.
E.g. instead of the articles list, use another one (perhaps named articlesLimited). The new list is constructed dynamically and contains the first elements from articles. A flag (e.g. hasMore) indicates whether the original articles has more elements, simply as: $scope.hasMore = articles.length > 5. You use the hasMore flag to show/hide the "+N more" message.
For what it's worth however, below is an implementation of the sentence directive. See the comment for weak points!
app.directive('sentence', ['$compile', function($compile) {
var RE = /^([a-z_0-9\$]+)\s+in\s([a-z_0-9\$]+)$/i, ONLY_WHITESPACE = /^\s*$/;
function extractTrimmedContent(tElem) {
var result = tElem.contents();
while( result[0].nodeType === 3 && ONLY_WHITESPACE.test(result[0].textContent) ) {
result.splice(0, 1);
}
while( result[result.length-1].nodeType === 3 && ONLY_WHITESPACE.test(result[result.length-1].textContent) ) {
result.length = result.length - 1;
}
return result;
}
function extractIterationMeta(tAttrs) {
var result = RE.exec(tAttrs.values);
if( !result ) {
throw new Error('malformed values expression, use "itervar in list": ', tAttrs.values);
}
var cutoff = parseInt(tAttrs.cutoff || '5');
if( isNaN(cutoff) ) {
throw new Error('malformed cutoff: ' + tAttrs.cutoff);
}
return {
varName: result[1],
list: result[2],
cutoff: cutoff
};
}
return {
scope: true, // investigate isolated scope too...
compile: function(tElem, tAttrs) {
var iterationMeta = extractIterationMeta(tAttrs);
var content = $compile(extractTrimmedContent(tElem));
tElem.empty();
return function link(scope, elem, attrs) {
var scopes = [];
scope.$watchCollection(
function() {
// this is (IMO) the only legit usage of scope.$parent:
// evaluating an expression we know is meant to run in our parent
return scope.$parent.$eval(iterationMeta.list);
},
function(newval, oldval) {
var i, item, childScope;
// this needs OPTIMIZING, the way ng-repeat does it (identities, track by); omitting for brevity
// if however the lists are not going to change, it is OK as it is
scopes.forEach(function(s) {
s.$destroy();
});
scopes.length = 0;
elem.empty();
for( i=0; i < newval.length && i < iterationMeta.cutoff; i++ ) {
childScope = scope.$new(false, scope);
childScope[iterationMeta.varName] = newval[i];
scopes.push(childScope);
content(childScope, function(clonedElement) {
if( i > 0 ) {
elem.append('<span class="sentence-sep">, </span>');
}
elem.append(clonedElement);
});
}
if( newval.length > iterationMeta.cutoff ) {
// this too can be parametric, leaving for another time ;)
elem.append('<span class="sentence-more"> +' + (newval.length - iterationMeta.cutoff) + ' more</span>');
}
}
);
};
}
};
}]);
And the fiddle: https://jsfiddle.net/aza6u64p/
This is a tricky problem. Transclude is used to wrap elements but when using transclude you don't have access to the directive scope, only to the scope of where the directive is being used:
AnglularJS: Creating Custom Directives
What does this transclude option do, exactly? transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside.
So a solution is to create another component to inject the template's scope inside the directive, like this:
.directive('myList', function() {
return {
restrict: 'E',
transclude: true,
scope: { items: '=' },
template: '<div ng-repeat="item in items" inject></div>'
};
})
.directive('inject', function() {
return {
link: function($scope, $element, $attrs, controller, $transclude) {
$transclude($scope, function(clone) {
$element.empty();
$element.append(clone);
});
}
};
})
<my-list items="articles">
<strong>{{item.title}}</strong> by <span>{{item.author}}</span>
</my-list>
This was taken from this discussion: #7874
And I made a Plnkr.
I'm relatively new to creating custom Angular directives, and I'm trying to replace a page in my application which is a series of JQuery components which get nested inside one another.
I'm wanting to create a set of custom Angular directives, which I can nest within each other allowing the user to build up a kind of form, whilst allowing them to delete and re-add any directives nested within one another if they need to.
I'm not sure how dynamically insert one directive into another, based on a user's choice, and how to set this within the directive so that a given directive's child can be deleted, re-added and then recompiled.
So far the only (unsophisticated) method I have come up with is to 2-way bind to an attribute on the directive's isolate scope, and have this determine the inner content of the directive like so, however upon changing this attribute object on the parent scope, the directive's DOM doesn't recompile, so I'm convinced there is a much better way to do this.
I'm assuming I might need to use transclusion or the link function in some way, so any guidance on this is much appreciated!
Current attempt:
app.directive("testCustomMapperThings", function($compile) {
var testTemplate1 = '<div>THIS IS FIRST THE DYNAMICALLY COMPILED TEST TEMPLATE 1</div>';
var testTemplate2 = '<div>THIS IS SECOND THE DYNAMICALLY COMPILED TEST TEMPLATE 2</div>';
var getTemplate = function(contentType) {
var template = '';
switch(contentType) {
case 'testTemplate1':
template = testTemplate1;
break;
case 'testTemplate2':
template = testTemplate2;
break;
}
return template;
};
var linker = function(scope, element, attrs) {
//reads the scope's content attribute (2 way bound to this directive's isolate scope) and sets as DOM
element.html(getTemplate(scope.content.testContent)).show();
//compiles the 2 way bound DOM, recompiles directive on changes to attributes. todo: CHECK DIRECTIVE RECOMPILES ON CHANGES TO ATTRIBUTES
$compile(element.contents())(scope);
};
return {
restrict: "E", //can only be an element
link: linker, //link function
scope: { //isolate scope, 2 way bind to a 'content' attribute
content:'='
}
};
});
Use of this directive in the DOM, where I attempted to alter the $scope.content object but the directive's inner content didn't recompile:
<test-custom-mapper-things content="testContent"></test-custom-mapper-things>
<button ng-click="changeContent()">Click to change the test content and see if DOM recompiles</button>
Controller for the parent scope of the directive:
$scope.testContent = {
testContent : "testTemplate1"
};
$scope.changeContent = function() {
if($scope.testContent.testContent == 'testTemplate1') {
$scope.testContent.testContent = 'testTemplate2';
} else {
$scope.testContent.testContent = 'testTemplate1';
}
};
Use the $compile service and scope.$watch method.
Example:
Javascript:
angular.module('dynamicTemplate',[])
.directive("testCustomMapperThings",['$compile',function($compile) {
return {
resctrict: 'AE',
scope: {
active: '='
},
link: function (scope, el, attrs, ctrl) {
var t1 = "<div>I'm 1st Template</div>",
t2 = "<div>I'm 2nd Template</div>",
newTemplate;
function loadTemplate() {
el.html('');
switch(scope.active) {
case 'template1':
newTemplate = t1;
break;
case 'template2':
newTemplate = t2;
break;
}
el.append($compile(newTemplate)(scope));
}
loadTemplate();
scope.$watch('active', function(newVal, oldVal){
if(newVal === oldVal) return;
loadTemplate();
});
}
}
}])
.controller('templateController', function() {
var vm = this;
vm.option = true;
vm.templateOption = vm.option? 'template1' : 'template2';
vm.change = function() {
vm.option = !vm.option;
vm.templateOption = vm.option? 'template1' : 'template2';
}
});
HTML
<div ng-app="dynamicTemplate">
<div ng-controller="templateController as t">
<button ng-click="t.change()"></button>
<test-custom-mapper-things active="t.templateOption"></test-custom-mapper-things>
</div>
</div>
Codepen: http://codepen.io/gpincheiraa/pen/vLOXGz
I want to build a angular.js directive, which by clicking the <span>, it will turn out into a editable input. and the following code works well, except when the model is empty or model length has 0, make it show <span> EMPTY </span>.
<span ng-editable="updateAccountProfile({'location':profile.location})" ng-editable-model="profile.location"></span>
app.directive('ngEditable', function() {
return {
template: '<span class="editable-wrapper">' + '<span data-ng-hide="edit" data-ng-click="edit=true;value=model;">{{model}}</span>' + '<input type="text" data-ng-model="value" data-ng-blur="edit = false; model = value" data-ng-show="edit" data-ng-enter="model=value;edit=false;"/>' + '</span>',
scope: {
model: '=ngEditableModel',
update: '&ngEditable'
},
replace: true,
link: function(scope, element, attrs) {
var value = scope.$eval(attrs.ngEditableModel);
console.log('value ' , attrs.ngEditableModel , value);
if (value == undefined || (value != undefined && value.length == 0)) {
console.log('none');
var placeHolder = $("<span>");
placeHolder.html("None");
placeHolder.addClass("label");
$(element).attr("title", "Empty value. Click to edit.");
}
scope.focus = function() {
element.find("input").focus();
};
scope.$watch('edit', function(isEditable) {
if (isEditable === false) {
scope.update();
} else {
// scope.focus();
}
});
}
};
});
the problem occurs at the this part of code that
var value = scope.$eval(attrs.ngEditableModel);
console.log('value ' , attrs.ngEditableModel , value);
attrs.ngEditableModel output the content 'profile.location', but then using scope.$eval() only output ' undefined ', even model 'profile.location' is not null
You have two ways to fix that.
1) You're calling $eval on the wrong scope. You have in scope in your link function your newly created isolated scope. attrs.ngEditableModel does contain a reference to the outer scope of your directive, which means you have to call $eval at scope.$parent.
scope.$parent.$eval(attrs.ngEditableModel)
or 2) a better way to handle it: You already have bound ngEditableModel via your scope definition of
scope: {
model: '=ngEditableModel',
So, instead of using your own $eval call, you can just use scope.model which points to the value of attrs.ngEditableModel. This is already two-way-bound.
Problem:
In a directive nested within 3 ui-views, I can't access the values of my isolate scope. scope.values returns {} but when I console.log scope I can see all the values on the values property.
In a different app I can make this works and I converted this one to that method as well but it still doesn't work and I'm tracing the routes, ctrl's and I can't find the difference between the two.
Where I'm trying to access it from
Init App > ui-view > ui-view > ui-view > form-elements > form-accordion-on
What I'm working with:
The view
<ul class='form-elements'>
<li
class='row-fluid'
ng-hide='group.hidden'
ng-repeat='group in main.card.groups'
card='main.card'
form-element
values='values'
group='group'>
</li>
</ul>
This directive contains tons of different form types and calls their respective directives.
.directive('formElement', [function () {
return {
scope: {
values: '=',
group: '='
},
link: function(scope, element) {
l(scope.$parent.values);
element.attr('data-type', scope.group.type);
},
restrict: 'AE',
template: "<label ng-hide='group.type == \"section-break\"'>" +
"{{ group.name }}" +
"<strong ng-if='group.required' style='font-size: 20px;' class='text-error'>*</strong> " +
"<i ng-if='group.hidden' class='icon-eye-close'></i>" +
"</label>" +
"<div ng-switch='group.type'>" +
"<div ng-switch-when='accordion-start' form-accordion-on card='card' values='values' group='group'></div>" +
"<div ng-switch-when='accordion-end' form-accordion-off values='values' class='text-center' group='group'><hr class='mbs mtn'></div>" +
"<div ng-switch-when='address' form-address values='values' group='group'>" +
"</div>"
};
}])
This is the directive an example directive.
.directive('formAccordionOn', ['$timeout', function($timeout) {
return {
scope: {
group: '=',
values: '='
},
template: "<div class='btn-group'>" +
"<button type='button' class='btn' ng-class='{ active: values[group.trackers[0].id] == option }' ng-model='values[group.trackers[0].id]' ng-click='values[group.trackers[0].id] = option; toggleInBetweenElements()' ng-repeat='option in group.trackers[0].dropdown track by $index'>{{ option }}</button>" +
"</div>",
link: function(scope, element) {
console.log(scope) // returns the scope with the values property and it's values.
console.log(scope.values); // returns {}
})
// etc code ...
Closely related to but I'm using = on every isolate scope object:
AngularJS: Can't get a value of variable from ctrl scope into directive
Update
Sorry if this is a bit vague I've been at this for hours trying to figure out a better solution. This is just what I have atm.
Update 2
I cannot believe it was that simple.
var init = false;
scope.$watch('values', function(newVal, oldVal) {
if (_.size(newVal) !== _.size(oldVal)) {
// scope.values has the value I sent with it!
init = true;
getInitValues();
}
});
But this feels hacky, is there a more elegant way of handling this?
Update 3
I attach a flag in my ctrl when the values are ready and when that happens bam!
scope.$watch('values', function(newVal) {
if (newVal.init) {
getInitValues();
}
});
The output of console.log() is a live view (that may depend on the browser though).
When you examine the output of console.log(scope); scope has already been updated in the meantime. What you see are the current values. That is when the link function is executed scope.values is indeed an empty object. Which in turn means that values get updated after the execution of link, obviously.
If your actual problem is not accessing values during the execution of link, the you need to provide more details.
Update
According to your comments and edits you seem to need some one time initialization, as soon as the values are there. I suggest the following:
var init = scope.$watch('values', function(newVal, oldVal) {
if (newVal ==== oldVal) { //Use this if values is replaced, otherwise use a comparison of your choice
getInitValues();
init();
}
});
init() removes the watcher.
I'm new to AngularJS.
Can someone explain me why the active class not toggle between tabs in this code: http://jsfiddle.net/eSe2y/1/?
angular.module('myApp', [])
.filter('split', function () {
return function (input, string) {
var temp = string.split('|');
for (var i in temp)
input.push(temp[i]);
return input;
};
})
.directive('myTabs', function () {
return {
restrict: 'E',
scope: { tabs: '#' },
template:
"<div>" +
"<a ng-repeat='e in [] | split:tabs' ng-click='selectedIndex = $index' ng-class='{active:$index==selectedIndex}'>{{e}}</a>" +
"</div>",
replace: true
}
});
If I move the ng-click expression to a method of the controller, the code works as expected: http://jsfiddle.net/g36DY/1/.
angular.module('myApp', [])
.filter('split', function () {
return function (input, string) {
var temp = string.split('|');
for (var i in temp)
input.push(temp[i]);
return input;
};
})
.directive('myTabs', function () {
return {
restrict: 'E',
scope: { tabs: '#' },
template:
"<div>" +
"<a ng-repeat='e in [] | split:tabs' ng-click='onSelect($index)' ng-class='{active:$index==selectedIndex}'>{{e}}</a>" +
"</div>",
replace: true,
controller: ['$scope', function ($scope) {
$scope.onSelect = function (index) {
$scope.selectedIndex = index;
}
}]
}
});
Can someone explain me the difference? And how to modify the first code to make it works without create a method to the controller?
Thanks in advance.
Explanation of the Problem
The problem has to do with javascript inheritance as it relates to scopes and directives in angular. Basically, when in a child scope, all properties of basic types (int, boolean, etc) are copied from the parent.
In your case, the ng-repeat directive creates a child scope for each element, so each one of the links has its own scope. in the first example, selectedIndex is only referenced from within the repeater, each repeater element references its own copy of the selectedIndex. You can investigate this using the
In the second example, you define a selectedIndex object in the controller, which is the parent scope for the repeater. Because the selectedIndex property is undefined initially when it is passed into the controllers, they look to the parent for a definition. When this definition has a value set in the onSelect method, all of the repeater elements "see" this value, and update accordingly.
How to Debug
In the future, you can investigate these types issue using the Angular Batarang.
browse to http://jsfiddle.net/eSe2y/1/show
left-click one of the tabs
right-click the same link
select "inspect element"
open the debug console and type $scope.selectedIndex
repeat the above steps for another tab, and note how the value differs
now go to the elements tab of the debugger, and click on the div
enter $scope.selectedIndex and note that it is undefined
On the second fiddle, try viewing just the $scope on each of the tabs (not $scope.selectedIndex). You will see that selectedIndex is not defined on the repeater elements, so they default to the value from their parent.
Best Practices
The typical angular best practice to avoid this problem is to always reference items that could be change on the scope "after the dot". This takes advantage of the fact that JavaScript objects are inherited by reference, so when the property changes in one place, it changes for all scopes in the parent-child hierarchy. I've posted an updated fiddler that fixes the problem by simply pushing the binding onto an object:
angular.module('myApp', [])
.filter('split', function () {
return function (input, string) {
var temp = string.split('|');
for (var i in temp)
input.push(temp[i]);
return input;
};
})
.directive('myTabs', function () {
return {
restrict: 'E',
scope: { tabs: '#' },
template:
"<div>" +
"<a ng-repeat='e in [] | split:tabs' ng-click='s.selectedIndex = $index' ng-class='{active:$index==s.selectedIndex}'>{{e}}</a>" +
"</div>",
replace: true,
controller: ['$scope', function ($scope) {
$scope.s = {};
}]
}
});
http://jsfiddle.net/g36DY/2/