I'm wondering if there is any way I can get the original {{expression}} after angular has complied the directives and interpolated the expressions. For instance if there is a text e.g. Hi, Filip and the user clicks on it, I want to be able to show a pop-up with Hi, {{name}}.
Now, one way I thought of doing that is by analysing the DOM before angular (e.g. during run) and then saving the expressions as additional attributes to the parent element. However, I run into various problems with that (e.g. if parent has other child elements and they are removed, e.g. with ng-if, then I can't reliably know which expression belongs to which text node).
Since Angular keeps watchers for these expressions, it must have a reference to the text nodes they are applied on. Is there any way I could access those?
The second question is, can I somehow get the original element of ng-repeat (before it was compiled and transcluded), for the similar purpose (allowing the user to modify it on-the-fly).
I want to avoid introducing new directives as this is meant to work on existing angular applications.
I'm not concerned about performance or security (i.e. this is not for production applications but rather for prototyping/debugging).
Use factories to supply to your html expressions with reusable logic. Assumes you're using controller as syntax so you can the controller's scope as this in your view.
// factory
function() {
return 'bob';
}];
// in your controller
['somefactory', function(factory) {
this.factoryString = factory.toString(); // => "function() { return 'bob'; }"
this.factory = factory;
}];
// view
<div>hi {{this.factory()}} you were made with {{this.factoryString}}</div>
// interpolated
<div>hi bob, you were made with function() { return 'bob' }</div>
I didn't test any of that though.
Related
I am trying to create a directive for md-autocomplete. I tried using the answer provide by AngularJS - Create a directive that uses ng-model, but it does not work for me.
My CodePen is here: http://codepen.io/colbroth/pen/QyMaQX/?editors=101.
The code is based on the Angular material demo for autocomplete at https://material.angularjs.org/latest/demo/autocomplete. I have multiple pages, which need an autocomplete field to select a state. I do not want to repeat the code for each web page component.
state-directive takes an md-autocomplete input, I need demoCtrl.selected state to reflect the same value in both cases. But when I update the input element, the state-directive does not reflect this and vice versa.
<input ng-model="demoCtrl.selectedState">
<state-directive ng-model="demoCtrl.selectedState">...</state-directive>
You are on the right track. Your problem is that your model is a string - a primitive in javascript but a ngModel always needs to be an object if you want to avoid these kind of problems.
Scope inheritance is normally straightforward, and you often don't even need to know it is happening... until you try 2-way data binding (i.e., form elements, ng-model) to a primitive (e.g., number, string, boolean) defined on the parent scope from inside the child scope. It doesn't work the way most people expect it should work. What happens is that the child scope gets its own property that hides/shadows the parent property of the same name. This is not something AngularJS is doing – this is how JavaScript prototypal inheritance works. New AngularJS developers often do not realize that ng-repeat, ng-switch, ng-view and ng-include all create new child scopes, so the problem often shows up when these directives are involved. (See this example for a quick illustration of the problem.)
This issue with primitives can be easily avoided by following the "best practice" of always have a '.' in your ng-models.
Taken from Understanding-Scopes
It also links to this Youtube video - 3 minutes of well invested time
function DemoCtrl() {
var self = this;
self.state = {
selected: "Maine"
};
}
Fixed codepen
So I need to be able to call a function of a directives controller, have it wrap an existing child directive in a new one while maintaining the now wrapped directive's state (both it's controller and all possible children) and otherwise modify the parent directive's template. This process also has to able to reversed.
Quick and super dirty fiddle to show the idea
So the DOM starts as:
<split>
<innerDirective></innerDirective>
</split>
When the split directive's controller's function is called, this should happen:
<split> // same outer parent
<split> // new directive
<innerDirective></innerDirective>// SAME innerDirective instance
</split>
<split>// new directive
<innerDirective></innerDirective> //New innderDirective instance
</split>
</split>
I originally used the .html function to set the split directives content, and used $compile service to reconstruct the DOM. This of course destroys the original innerDirective. Since it may be deep, it is not feasible to store it's data in the parent split and retrieve with a link function (although I tried).
I also tried using jquery's .wrap and .unwrap functions, which works, except the added tags aren't compiled as directives, so not really.
Is there a proper way to do this, or a way to compile just the outer tag added with the .wrap, and update the scope of the innerDirective to the new parent?
Thanks for any help and advice you may have!
Edit:
I have a controller along this lies, lets say set to control the split 'E' directive:
function SplitCtrl($scope, $compile){
//stuff
//First way, redefine html contents and recompile
this.split1 = function(){
//element has been linked to the directive's element
$scope.element.html('<split></split><split></split>');
$compile($scope.element.contents())($scope);
};
//Second way, jquery.wrap
this.split2 = function(){
$($scope.element).children('innerDirective').wrap('<split />');
// then I need to instantiate the new split
// without reinstantiating now wrapped directive.
// Adding second split is fine in this case, I can do that.
};
};
The template is:
<innderDirective></innerDirective>
I have this part of code inside ng-repeat:
<div ng-show="parameter == 'MyTESTtext'">{{parameter}}</div>
where parameter is some $scope string variable ...
I wanted to see if its possible to check (inside ng-show for instance) whether parameter contains a substring.
You could do this with:
<div ng-show="parameter.indexOf('TEST') != -1">{{parameter}}</div>
which seems to be working; it displays every parameter that contains 'TEST' keyword.
I was wondering:
is this a correct way of doing this within AngularJS app?
Is it OK to use javascript built in functions like that?
EDIT:
parameter is actually formed like this: (and is thus not a $scope variable as I said above, sorry)
<div ng-repeat="(parameter,value) in oneOfMyScopeArrays">
UPDATE
Since you're dealing with strings in ngRepeat and not objects, there's no place to set flag to in your data elements. In this case I would advise using a custom directive. I do not agree with Darryl Snow's opinion that directive in this case is redundant. With directive (as it was the case with flag in controller) you can evaluate parameter once instead of doing so in every $digest cycle. Furthermore, if you decide to implement same functionality in other template, instead of copying the expression around, which is redundant, you'd reuse same directive. Here's a quick idea of such directive:
.directive('parameter', function() {
return {
link: function($scope, $element, $attrs) {
$attrs.$observe('parameter', function(parameter) {
if (parameter.indexOf('TEST') == -1) {
$element.hide();
} else {
$element.text(parameter);
$element.show();
}
});
}
}
});
Template:
<div parameter="{{parameter}}"></div>
This directive even sets up one watcher less per parameter comparing to your original solution, which is better performance wise. On the other hand, it disables two-way binding (parameter text is rendered once), so it won't work in case you want to edit parameter string in place.
ORIGINAL ANSWER
Is it correct way? Technically yes, because it works. Is it OK? Not so much because of several reasons:
Performance. Everytime $digest loop runs (it might run quite a lot, depending on interactivity of application), it has to process every such expression. Therefore string parameter.indexOf('TEST') != -1 has to be parsed and evaluated, which means calling .indexOf up to several times after each interaction, for example click on element with ngClick directive. Wouldn't it be more performant to test this assumption parameter.indexOf('TEST') != -1 once in Controller and set a flag, e.g.
$scope.showParameter = parameter.indexOf('TEST') != -1
In template you would write
<div ng-show="showParameter">{{parameter}}</div>
Model logic in template. It's hard to tell the actual reasoning from your example when the parameter should be visible, but is it up to the template to have this logic? I think this belongs to controller, if not model to decide, that your view layer would be decoupled from making assumptions about how the model actually works.
Yes, perfectly ok I think. You could write a separate directive of your own that does the same thing - it may look a bit tidier but it's ultimately redundant when angular comes with ng-show already built in, and it means a slight additional payload to the user. You could also do a $scope.$watch on parameter and set another scope variable for ng-show, but that just moves the mess from your view to your controller.
$scope.$watch('parameter', function(){
if(parameter.indexOf('TEST') != -1)
$scope.showit = true;
});
and then in the view:
<div ng-show="showit">{{parameter}}</div>
According to the documentation a template can be a function which takes two parameters, an element and attributes and returns a string value representing the template. It replaces the current element with the contents of the HTML. The replacement process migrates all of the attributes and classes from the old element to the new one.
The compile function deals with transforming the template DOM. It takes three parameters, an element, attributes and transclude function. The transclude parameter has been deprecated. It returns a link function.
It appears that a template and a compile functions are very similar and can achieve the same goal. The template function defines a template and compile function modifies the template DOM. However, it can be done in the template function itself. I can't see why modify the template DOM outside the template function. And vice-versa if the DOM can be modified in the compile function then what's the need for a template function?
The compilation function can be used to change the DOM before the resulting template function is bound to the scope.
Consider the following example:
<div my-directive></div>
You can use the compile function to change the template DOM like this:
app.directive('myDirective', function(){
return {
// Compile function acts on template DOM
// This happens before it is bound to the scope, so that is why no scope
// is injected
compile: function(tElem, tAttrs){
// This will change the markup before it is passed to the link function
// and the "another-directive" directive will also be processed by Angular
tElem.append('<div another-directive></div>');
// Link function acts on instance, not on template and is passed the scope
// to generate a dynamic view
return function(scope, iElem, iAttrs){
// When trying to add the same markup here, Angular will no longer
// process the "another-directive" directive since the compilation is
// already done and we're merely linking with the scope here
iElem.append('<div another-directive></div>');
}
}
}
});
So you can use the compile function to change the template DOM to whatever you like if your directive requires it.
In most cases tElem and iElem will be the same DOM element, but sometimes it can be different if a directive clones the template to stamp out multiple copies (cf. ngRepeat).
Behind the scenes, Angular uses a 2-way rendering process (compile + link) to stamp out copies of a compiled piece of DOM, to prevent Angular from having to process (= parse directives) the same DOM over and over again for each instance in case the directive stamps out multiple clones resulting in much better performance.
Hope that helps!
ADDED AFTER COMMENT:
The difference between a template and compile function:
Template function
{
template: function(tElem, tAttrs){
// Generate string content that will be used by the template
// function to replace the innerHTML with or replace the
// complete markup with in case of 'replace:true'
return 'string to use as template';
}
}
Compile function
{
compile: function(tElem, tAttrs){
// Manipulate DOM of the element yourself
// and return linking function
return linkFn(){};
}
}
The template function is called before the compile function is called.
Although they can perform almost identical stuff and share the same 'signature', the key difference is that the return value of the template function will replace the content of the directive (or the complete directive markup if replace: true), while a compile function is expected to change the DOM programmatically and return a link function (or object with pre and post link function).
In that sense you can think of the template function as some kind of convenience function for not having to use the compile function if you simply need to replace the content with a string value.
Hope that helps!
One of the best uses of the template function is to conditionally generate a template. This allows you to automate the creation of a template based on an attribute or any other condition.
I have seen some very large templates that use ng-if to hide sections of the template. But instead of placing everything into the template and using ng-if, which can cause excessive binding, you can remove sections of the DOM from the output of the template function that will never be used.
Let's say you have a directive that will include either sub-directive item-first or item-second. And the sub-directive will not ever change for the life of the outer directive. You can adjust the output of the template, prior to the compile function being called.
<my-item data-type="first"></my-item>
<my-item data-type="second"></my-item>
And the template string for these would be:
<div>
<item-first></item-first>
</div>
and
<div>
<item-second></item-second>
</div>
I agree that this is an extreme simplification, But I have some very complicated directives and the outer directive needs to display one of, about, 20 different inner directives based on a type. Instead of using transclude, I can set the type on the outer directive and have the template function generate the correct template with the correct inner directive.
That correctly formatted template string is then passed on to the compile function, etc.
I'm quite new to the superheroic framework AngularJS, so please excuse my noobness! So, while I was trying to develop a simple SPA that allowed admins to change permissions of other users in my website through a table filled with selection checkboxes, I came by an issue. I defined $scope.checked so I could know how many users the administrator chose and also $scope.mainCheckBox which would hold the correct CSS class for the major checkbox (like the one from Gmail, which you use to select all/none) in each of the three situations: all users selected, no users selected and partial selection. This simple logic goes as follows:
$scope.updateMainCheckBox = function(){
if(this.checked==this.users.length) this.mainCheckBox = "qm-icon-checked";
else if(!this.checked) this.mainCheckBox = "";
else this.mainCheckBox = "qm-icon-minus";
};
Running this at the end of the ng-click event callback of the other checkboxes, this code was capable of choosing the class correctly but it wouldn't assign the class to the mainCheckBox. When I changed every this.mainCheckBoxto $scope.mainCheckBox it all went well and the page would behave as expected.
So, I know in vanilla Js this is a reference to the window object but since AngularJS has its own event handling loop, I believe that's not the case now. I expected it to be a ref to $scope, since checked and users were found, but apparently it's not. So, what's happening here? And also, is there a more "angular" way to implement this?
You're mostly right--in the case of your function, this did reference a scope. But, it did not reference $scope; instead, it referenced one of $scope's children.
Try this: add some console.log statements to log out the ID of the $scope variable, and then the ID of the this scope in your callback:
$scope.updateMainCheckBox = function(){
console.log('$scope:', $scope.$id);
console.log('this:', this.$id);
// ...
};
You'll find the IDs are different. This is because the ngRepeat directive creates a child scope, which is a scope that inherits prototypally from its parent. This means that when you assign directly to a value on that scope, it's not reaching the parent scope, which is $scope.
I highly recommend a read through the excellent (and lengthy) "The Nuances of Scope Prototypal Inheritance" by Mark Rajcok; it esplains why you see this seemingly strange behavior on child scopes. (You can also find a copy on the AngularJS wiki.)