I had an idea for an angular directive but I'm not sure if it's possible.
I often have to delete and item from an ng-repeat array, so the de facto solution is to have a function on the scope:
$scope.remove = function(item) {
var index = $scope.items.indexOf(item);
$scope.items.splice(index, 1);
}
I'm constantly writing this boiler plate code for every ng-repeat and it would be nice to be able to do something like this instead:
<li ng-repeat="item in items">
<button ng-click-remove="item"></button>
</li>
Basically I'm thinking the directive will simply wrap ng-click, but then I start thinking, is it even possible to access the items array from my directive without knowing its name or using $parent?
The way I do it is create a service handling the deletion/Creation of said task, and then just use that wrapper in the controller. I mean, the way you are suggesting, you still need to write the same code, but just do it in the directive. Also, yes it is possible to access it from the directive, you can pass in the controller to it.
function dir() {
return {
controller: 'ctrl as ctrl',
link: function(scope, elem, attrs, ctrl){
console.log(ctrl.items);
console.log(attrs.ngClickRemove); // gets the value of item.
}
}
}
Though I would caution using the ng syntax since it should be reserved for Angularjs.
Related
Here is the minimal code to describe the issue. On the page, I have:
<div ng-controller='AController as a'>
<div a-directive></div>
</div>
In js, I have:
app.directive("aDirective", function($compile){
return {
scope: true,
link: function(scope, element, attrs) {
var template = "<h1>{{a.label}}</h1>";
element.append($compile(template)(scope));
}
}
});
app.controller("AController", function($scope){
self = this;
self.label = "some text";
});
That works, but the issue is that {{a.label}}, which made the view and controller/model tightly coupled. Is there any way to get rid of that a., and not to mention the controllerAs-name in the directive code at all? (just like what I did in the controller code)
To improve this you can pass the text to display as a parameter to the directive. Something like this is the initial idea:
<div a-directive="a.label"></div>
However, I DO recommend using an alias for the controller, so I made a Plunker where you can see all of this working together with some improvements.
Check it out here: http://plnkr.co/edit/1hBSBxwSEPXoj9TULzRQ?p=preview
I would also recommend to use template instead of link and restricting the directive to an element instead of using it as attribute, since it is modifying the DOM. But yeah, you could keep improving it till the end of the times :)
What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!
I'm new to AngularJS. I've learned that I can find elements in the DOM using queries like the following:
var e = angular.element(document.querySelector('#id'));
var e = angular.element(elem.querySelector('.classname'));
This is useful for finding elements by ID, or by CSS class name. However, I need to be able to find an element using a different approach. I have an element that looks like the following:
<div my-directive class='myContainer'>...</div>
I can't query on 'myContainer' because of how much its reused. For that reason, I would like to find any element with the attribute 'my-directive'. How do I search the DOM and find any element that makes use of 'my-directive'?
Rather than querying the DOM for elements (which isn't very angular see "Thinking in AngularJS" if I have a jQuery background?) you should perform your DOM manipulation within your directive. The element is available to you in your link function.
So in your myDirective
return {
link: function (scope, element, attr) {
element.html('Hello world');
}
}
If you must perform the query outside of the directive then it would be possible to use querySelectorAll in modern browers
angular.element(document.querySelectorAll("[my-directive]"));
however you would need to use jquery to support IE8 and backwards
angular.element($("[my-directive]"));
or write your own method as demonstrated here Get elements by attribute when querySelectorAll is not available without using libraries?
You haven't stated where you're looking for the element. If it's within the scope of a controller, it is possible, despite the chorus you'll hear about it not being the 'Angular Way'. The chorus is right, but sometimes, in the real world, it's unavoidable. (If you disagree, get in touch—I have a challenge for you.)
If you pass $element into a controller, like you would $scope, you can use its find() function. Note that, in the jQueryLite included in Angular, find() will only locate tags by name, not attribute. However, if you include the full-blown jQuery in your project, all the functionality of find() can be used, including finding by attribute.
So, for this HTML:
<div ng-controller='MyCtrl'>
<div>
<div name='foo' class='myElementClass'>this one</div>
</div>
</div>
This AngularJS code should work:
angular.module('MyClient').controller('MyCtrl', [
'$scope',
'$element',
'$log',
function ($scope, $element, $log) {
// Find the element by its class attribute, within your controller's scope
var myElements = $element.find('.myElementClass');
// myElements is now an array of jQuery DOM elements
if (myElements.length == 0) {
// Not found. Are you sure you've included the full jQuery?
} else {
// There should only be one, and it will be element 0
$log.debug(myElements[0].name); // "foo"
}
}
]);
Your use-case isn't clear. However, if you are certain that you need this to be based on the DOM, and not model-data, then this is a way for one directive to have a reference to all elements with another directive specified on them.
The way is that the child directive can require the parent directive. The parent directive can expose a method that allows direct directive to register their element with the parent directive. Through this, the parent directive can access the child element(s). So if you have a template like:
<div parent-directive>
<div child-directive></div>
<div child-directive></div>
</div>
Then the directives can be coded like:
app.directive('parentDirective', function($window) {
return {
controller: function($scope) {
var registeredElements = [];
this.registerElement = function(childElement) {
registeredElements.push(childElement);
}
}
};
});
app.directive('childDirective', function() {
return {
require: '^parentDirective',
template: '<span>Child directive</span>',
link: function link(scope, iElement, iAttrs, parentController) {
parentController.registerElement(iElement);
}
};
});
You can see this in action at http://plnkr.co/edit/7zUgNp2MV3wMyAUYxlkz?p=preview
Once again I stumbled on slow performance of ng-repeat and can't figure how to build a directive that will render an array of elements without utilizing ng-repeat anywhere (even in the template)
So, how do you guys do that?
Even if I iterate through the array, using template for every single element:
.directive('foo', function($parse, $compile) {
return {
restrict: 'E',
scope: { items: '=myarray' },
link: function (scope, element, attrs){
var tmplt = '<div> {{name}} </div>';
scope.items.forEach(function(){
element.append(tmplt(scope));
// now I'd like to have a scope of that element,
// and pass it into the template, and use properties related
// only to that element
// but I don't know how to get a scope for a child element
});
scope.$watch(attrs.myarray, function(value) { console.log('something change'); })
}
}});
If I choose to have a single template for all the elements, then again I have no choice but to use ng-repeat in it and it will create ngRepeatWatchers and everything gets slow down again.
Though I agree with #Mark, but here is dirty solution for your question:
http://plnkr.co/edit/wxM8mSoNsRGXBJUao5Xb?p=preview
The idea is to create isolated child scopes:
scope.items.forEach(function(item){
var childScope = scope.$new(true);
angular.extend(childScope, item);
element.append($compile(tmplt)(childScope));
});
And do not forget to delete those child scopes unpon each refresh.
and sill this solution needed to be benchmarked to see is it faster and how much comparing to ngRepeat.
I wrote this simple jsfiddle in which I do (successfully) some basic addClass on a directive in a ng-repeat.
http://jsfiddle.net/rv6u2/5/
Now, my question is: which is the best (or intended) place to do such DOM manipulations:
A. In the directive?
B. In the controller?
Both possibilities are shown in my example.
Code:
var TestApp = angular.module("TestApp", ['ngResource']);
TestApp.directive('onLoad', function() {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
elm.addClass('loaded'); // A: DOM manipulation in directive
scope.initMe(scope.$eval(attrs.onLoad2), elm); // B: DOM manipulation handled in controller
}
};
});
thanks in advance :)
NEVER manipulate the dom inside of controllers.
Controllers should just use services and update attributes of $scope. All DOM manipulation should be made by directives and(in some cases) services(e.g. $anchorScroll)
See the concepts of angularjs here
UPDATE: Example of the correct way here
A more "Angular way" of setting class loaded2 would be as follows (which avoids DOM manipulation inside the controller):
In the HTML, declare a model for the class (myClass):
<div ng-repeat="item in items" ng-model="item" on-load="initMe(item)" ng-class="myClass">
In the link function, just call the controller method:
scope.initMe()
In the controller, manipulate the model/$scope property:
$scope.initMe = function() {
$scope.myClass = "loaded2";
}
Changing the model/scope will automatically update the view.
This method is useful if you want to declare in the HTML that a class is being controlled by $scope property myClass. However, using elm.addClass() inside the linking function is more self-contained and easier to maintain (and I like that approach better).