I have a directive like below. It's supposed to load a file from an <input type=file> and set it to an ng-model provided. It's also got some custom validation only incidental to my question. It's also on plunkr.
What's wrong is that the ngModel never gets set at all. It's always undefined. Why?
app.directive('fileInput', function () {
var link = function (scope, element, attrs, ngModel) {
var VALIDTYPES = ['text/csv', 'text/directory', 'text/vcard'];
var updateModel = function () {
var file = element[0].files[0];
if (file) {
scope.$apply(function () {
if (VALIDTYPES.indexOf(file.type) >= 0) {
ngModel.$setValidity("mimetype", true);
ngModel.$setViewValue(file);
} else {
ngModel.$setValidity("mimetype", false);
alert("Sorry, can only accept VCF and CSV files.");
}
})
}
};
element.bind('change', updateModel);
};
return {
restrict: 'A',
require: 'ngModel',
template: "<input type='file'>",
replace: true,
link: link,
scope: {},
}
});
This is fixed as of 1.2.0 due to this change:
Fixes issue with isolate scope leaking all over the place into other directives on the same element.
Isolate scope is now available only to the isolate directive that requested it and its template.
Before 1.2.0 if any directive on an element requested an isolate scope then all the directives on that element shared that scope.
In your case this caused the input directive to use the isolate scope you were requesting for your directive instead of the parent scope that the html <p>Filename is: {{ file.name }}</p> is on. Thus file was undefined because file was on a child scope
You can see that in action, pre 1.2.0, by copying the $viewValue up to the parent scope using this one line below your $setViewValue:
ngModel.$setViewValue(file);
scope.$parent.file = ngModel.$viewValue;
You'll see in this updated plunker that fixes this problem in pre-1.2.0 code.
The best solution though is to move to 1.2.0. With 1.2.0 your isolate scope will only affect your directive and not the 'input' directive so everything works as expected in this plunker
Related
I would like the ng-model of an input to be created automatically based off of the name of the input it is on. This is because Html.TextBoxFor etc in MVC creates the proper name to bound the input to the server side model. To reduce user error from having to retype the exact string into the ng-model, I would like my team to just put a directive and it gets created. I found this code on stackoverflow for this.
datatableApp.directive('automaticangularmodelbinding', function ($compile) {
return {
restrict: 'A',
replace: false,
priority: 10000,
terminal: true, // these terminal and a high priority will stop all other directive from being compiled at first run,
scope: {
automaticangularmodelbinding: '##'
},
link: function (scope, element, attrs) {
attrs.$set('ngModel', (scope.automaticangularmodelbinding != '') ? (scope.automaticangularmodelbinding + '.' + attrs.name) : attrs.name); // set value of ng-model to be the same as the name attribute
attrs.$set('automaticangularmodelbinding', null); // remove itself to avoid a recusion
$compile(element)(scope); // begin compiling other directives
}
};
});
This works and the ng-model is created with the name of the element. However, when I pull the data down from the server and set it, the inputs do not get filled in with the data. If I take out the automatic directive and define it normally via ng-model, it does work.
My code for the server pull down.
$scope.getEditStreet = function (streetID) {
$http.post('#Url.Action(Model.GetFormControllerFunctionName, Model.GetFormControllerName)', "{ #Model.JavascriptEditPropertyName : " + streetID + "}").then(function (response) {
$scope.editFormData = response.data.ResultObject;
$scope.$apply();
}, function (response) {
alert("fail" + response.statusText);
});
};
With ng-model, I needed the scope.apply call to get the checkboxes to check. After using this automatic version, scope.apply errors. If I remove scope apply though it still doesn't work even on the text boxes even though that worked before without the apply.
It seems to be the fact that I added ng-model after the fact, that it is not working the same way as it being there from the start. How can I get this to work?
Edit:
After reading zaitsman comments, the final version that works is as follows. I removed scope from the directive and used attrs['automaticangularmodelbinding'] for my passing of the data I needed.
datatableApp.directive('automaticangularmodelbinding', function ($compile) {
return {
restrict: 'A',
replace: false,
priority: 10000,
terminal: true, // these terminal and a high priority will stop all other directive from being compiled at first run,
link: function (scope, element, attrs) {
attrs.$set('ngModel', (attrs['automaticangularmodelbinding'] != '') ? (attrs['automaticangularmodelbinding'] + '.' + attrs.name) : attrs.name); // set value of ng-model to be the same as the name attribute
attrs.$set('automaticangularmodelbinding', null); // remove itself to avoid a recusion
$compile(element)(scope); // begin compiling other directives
}
};
});
As discussed, i suggest you would skip isolated scope so that you can use variables from the declaring scope of the directive.
To access the values passed to the directive you can use the attrs object.
So remove scope from the directive completely, and then to obtain the value, in the link function you can do:
var myPara = scope[attrs['automaticangularmodelbinding']];
and that will contain extraParameterInFront from the parent scope.
If that parameter is just a string it is even easier:
var myPara = attrs['automaticangularmodelbinding'];
I have a video player directive that uses an ng-src in its template. How do I run directive code after the ng-src has been evaluated so the video is actually loaded?
Here is the directive code:
return {
restrict: 'A',
replace: false,
transclude: false,
scope: true,
templateUrl: "/modules/didyouknow/views/slideshow-frame.directive.client.view.html",
link: {
pre: function() {
console.log('a');
},
post: function(scope, element, attrs) {
/**
* scope.frame - frame information
*/
scope.frame = scope[attrs.slideshowFrame];
}
}
};
both link functions execute before {{expr}} has been evaluated in the template.
The whole point of post link is it that it's executed after child post-links, in reverse order as pre links. So why isn't it executing last? It executes immediately after the prelink function so why are they even separate functions?
You could have $observe inside your directive that will work same as that of the $watch but the difference is it evaluates {{}} interpolation expression.
Inside $observe you could see if the ng-src has value the only call the directive method. otherwise wait.
link: function(scope, element, attrs){
attrs.$observe('ngSrc', function(newVal, oldVal){
if(newValue){
//call desired code when `ng-src` have value
}
});
}
There is a couple of recipes to execute the code in link at the moment when directive DOM 'is there'. One is using zero-delayed timeout
$timeout(function () {
...
});
It is is generally preferable if you're after rendered DOM or interpolated values. It is not an option here, because templateUrl is used and directive template is loaded asynchronously, and the template is not available during the linking phase.
Another recipe is using scope watchers/attribute observers (one-time if you don't care about data bindings).
var unobserveNgSrc = attrs.$observe('ngSrc', function (ngSrc, oldNgSrc) {
if (!ngSrc) return;
unobserveNgSrc();
...
})
It executes immediately after the prelink function so why are they even separate functions?
This behaviour suggests what it can be used for. In parent preLink some things can be done that must precede child preLinks or the latter could benefit from, which you will rarely find useful for simple directives. And parent postLink executes last, and that's a good moment for 'ok, all of my children are already compiled and linked, let's do something at last'.
so I did a little experiment based on my project
I create 2 directive, one using isolated scope and the other one is not..
the question are :
is there a way to get scope attribute without using isolated scope ?
because in my project I didn't have an isolated scope for the custom
directive environment and also I need to access the parent scope
could I manipulate the dom using angular.element('#' + scope.id) ?
if not is there a way to do this ?
this is the unisolated custom directive
<test-directive item="item" content="item.context"></test-directive>
this is the js codes
app.directive('testDirective', function() {
return {
restrict: "EA",
scope: false,
template:"<div>test directive</div>",
link: function(scope, elm, attr) {
console.log(attr.item); //I want it like the result gives in line 39
console.log(attr.id); //I want it like the result gives in line 41
console.log(attr.content); //I want it like the result gives in line 43
console.log(scope.name);
}
}
});
this is the isolated one
<isolated-directive id=item.id item="item" content="item.context"></isolated-directive>
this is the js codes
app.directive('isolatedDirective', function() {
return {
restrict: "EA",
scope:{
item:'=item',
id:'=id',
content:'=content',
},
template:"<div>isolated directive</div>",
link:function(scope,elm,attr) {
console.log(scope.item.id);
console.log(scope.id);
console.log(scope.content);
console.log(scope.name); //I want it like the result gives in line 27
}
}
});
and this is the working plunkr
anyone care to help?
You can use scope: true and the scope will prototypically inherit from the location it is inserted. However, if all you need is access to the parent scope, you can always use $parent, even in an isolated scope. It is not recommended, but it is possible.
Q1:
app.directive('testDirective', function() {
return {
restrict: "EA",
scope: false,
template:"<div>test directive</div>",
link: function(scope, elm, attr) {
console.log(scope[attr.item]); //I want it like the result gives in line 39
console.log(scope[attr.id]); //I want it like the result gives in line 41
console.log(scope[attr.content]); //I want it like the result gives in line 43
console.log(scope.name);
}
}
});
Q2:
basically the parm elm in link function is your directive related DOM. if you really want the scope.item.id in your DOM attribute, use ng-attr to define your attributes like below. note that angular.element is just used to wrap a DOM to JQuery element but not used for DOM finding.
<isolated-directive ng-attr-id="{{item.id}}" item="item" content="item.context"></isolated-directive>
I have this code:
<body ng-controller="testController">
<div test-directive transform="transform()">
</div>
<script type="text/ng-template" id="testDirective.html">
<div>
<p>
{{transform()}}
</p>
</div>
</script>
<script>
angular.module("Test", [])
.directive("testDirective", function() {
return {
templateUrl: "testDirective.html",
scope: {
transform: "&"
},
link: function(scope) {
}
};
})
.controller("testController", function($scope) {
$scope.transform = function() {
return "<a ng-click='somethingInController()'>Do Something</a>";
};
$scope.somethingInController = function() {
alert("Good!");
};
});
</script>
</body>
So basically what I want to accomplish is to create a directive with a method that will be called from the controller. And that method will do something with the values passed (in this example it does not receives nothing, but in the real code it does).
Up to that point is working. However, the next thing I want to do is create an element that will call a method in the controller. The directive does not knows what kind of element will be (can be anything) nor what method will be. Is there any way to do it?
Fiddle Example:
http://jsfiddle.net/abrahamsustaita/C57Ft/0/ - Version 0
http://jsfiddle.net/abrahamsustaita/C57Ft/1/ - Version 1
FIDDLE EXAMPLE WORKING
http://jsfiddle.net/abrahamsustaita/C57Ft/2/ - Version 2
The version 2 is now working (I'm not sure if this is the way to go, but it works...). However, I cannot execute the method in the parent controller.
Yes. However there is a few problems with your code. I will start by answering your question.
<test-directive transform='mycustommethod'></test-directive>
// transform in the directive scope will point to mycustommethod
angular.module('app').directive('testDirective', function() {
return {
restrict: 'E',
scope: {
transform: '&'
}
}
});
The problem is that printing the html will be escaped and you will get < instead of < (etc.). You can use ng-bind-html instead but the returned html will not be bound. You will need to inject the html manually (you can use jquery for this) in your link method and use var compiled = $compile(html)(scope) to bind the result. Then call ele.after(compiled) or ele.replace(compiled) to add it to your page.
I finally get to get it working.
The solution is combined. First of all, I needed to add another directive to parse the element I wanted:
.directive("tableAppendElement", function ($compile) {
return {
restrict: "E",
replace: true,
link: function(scope, element, attrs) {
var el = angular.element("<span />");
el.append(attrs.element);
$compile(el)(scope);
element.append(el);
}
}
})
This will receive the element/text that will be appended and then will registered it to the scope.
However, the problem still exists. How to access the scope of the controller? Since my directive will be used by a lot of controllers, and will depend on the model of the controller, then I just set scope: false. And with that, every method in the controller is now accessible from the directive :D
See the fiddle working here. This also helped me because now, there is no need to pass the transform method, so the controller can be the one handling that as well.
I'm using the AngularUI bootstrap library in my project. In particular, I'm using the accordion and a custom template to display the accordion groups etc. On the template I have an ng-click event which seems to be working only when I don't have the parameter on it. Inside my directive I have a scope variable that produces a unique identifier which I have included as a parameter on the ng-click event. What am I missing to get this working? I'm using Angular 1.0.8.
thanks in advance
!-- template
<a callback="accordion_group_opened($index)" ng-click="callSubmit(currentRow)">
!-- scope variable incremented each time the directive is called
countRow = generateUnique.getNextIdStartingAtOne();
scope.currentRow = countRow;
Edit:
Added the compilation still not getting the value from the scope into my ng-click param. Any ideas on a work around?
compile:function (scope, element, attrs) {
var countRow = generateUnique.getNextIdStartingAtOne();
scope.currentRow = countRow;
return function(scope, element, attr) {
$timeout(function() {
var fn = $parse(attr["ngClick"]);
element[0].on("click", function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
})
};
}