Dynamic directive controller using name property - javascript

I'm trying to implement directive with dynamic controller so that I can bind controller based on some condition just like Todd Motto has shown it here
Everything works fine expect that I can't send object property as a name to the directive like,
<directive-with-dynamic-controller ctrl="someObj.prop"></directive-with-dynamic-controller>
I've even tried this, but to no avail:
<directive-with-dynamic-controller ctrl="{{someObj.prop}}"></directive-with-dynamic-controller>
It gives error like this:
Argument is not a function, got undefined
Any ideas how I can solve this? Or any other way?
Thanks!

Any ideas how I can solve this? Or any other way? Thanks!
The problem is in the order of execution. Something that was left out in the mentioned article is the fact that you cannot pass an expression in the case of setting a "dynamic" controller (so much for dynamic).
If we look at the notation of a directives compile step, you would notice that there is no access to the current $scope.
This is because DOM compilation and controller initialisation happens before the angular parser kicks in and evaluates your expression(s).
As such, you cannot pass a $scope expression into the ctrl attribute, as it is simply a regular DOM attribute at this point in time. In essence, you are passing the raw string into the ctrl attribute.
<my-custom-dir ctrl="foo.bar"></my-custom-dir>
// Error: "foo.bar" is not a controller // is not a function // $minErrObscureStuffThatDoesnHelpYou.
I've been trying to figure out a slick way to get deferred directive compilation running for some time now, but to no avail...
One possible way of getting around this problem (ymmv):
.directive('...', function ($controller) {
controller: function ($scope, $element, $attrs) {
$attrs.$observe('ctrl', function (n, o) {
return $controller(n, {
$scope: $scope,
$element: $element,
$attrs: $attrs
});
});
}
});
Effectively, you would replace a pre-initialised controller (that does nothing) with the controller matched by the name you passed through your attrs.ctrl attribute. However, this would execute post-compilation - so I wouldn't seriously recommend it.
jsfiddle showing the order of execution
tl;dr There is currently no slick way to define a controller for a directive, based on a $scope expression. It has to be a raw string because compilation isn't scoped on a per-component basis, but more so in a 'global' order of execution.
DOM Compilation > Controller initialisation > Scope linkage fiddle

Related

How to call an angular directive within an a tag whilst passing a variable

I would like to do something like this:
<a <myDir myVar="someInput"></myDir>>Call myDir</A>
However I'm realising you can't nest a directive within an a tag and set parameters this way.
I'm actually using jade though. I call the directive fine with.
a(myDir) Call myDir
but am unsure how to pass the variable to the directive. I've tried the equivalent jade to the above html, which is this:
a(myDir(myVar="someInput")) Call myDir
As its an optional parameter I use the '=?' syntax in the directive.
However the issue is definitely the syntax for calling the function in jade.
So after much failure this is how you do it.
// In the HTML/Jade
a(myDir info="someInput")
// In the Directive
scope: {
info: '#?info'
}
Check out this question and #aifarfa's answer for an example. Though the question is different it's a perfect example of what I wanted to do.
How to call directive from template html in angularjs

Javascript \ Angular function help - mental block

I don't know what it is about JS, but I have a mental block. I apologize for the dumb question, but I'm at a loss because no matter how much I read I cannot get the academics into practice. Especially when it comes to nested functions.
I have a controller, lets say FileCtrl. Inside of it I have the the following that listens for file added to an input field via a directive. I'm attempting to inject an Angular JS factory service service called fileReader (a queue service for HTML5 FileReader).
However,I keep getting a undefined error on fileReader. I know why because, it cannot see fileReader, but injecting it at $scope.$on and then again on $scope.$apply doesn't work. Also, adding fileReader as a closure at the end of $scope.$on doesn't work either.
I should add that I can see the args.file and if I remove the fileReader code it will push the file no problem, but I then have no thumbnail. So I it works, just not with the fileReader and that is because Im doing something wrong with injection.
Side note, to Vals comment below I use apply as I found there was a image render sync issue without it which works fine for smaller images, but with larger images it freezes which is why I'm attempting to create and use a $q fileReader service. I suppose another way to solve for it would be to create a watch / directive on the array entry and when img comes back with the 64 encode string populate the html element ... like I said JS mental block :)
myApp.controller('FileController', ['$scope', 'FileReaderService', function($scope, FileReaderService ){
$scope.$on("fileSelected", function (event, args) {
$scope.$apply(function () {
$scope.progress = 0;
fileReader.readAsDataUrl(args.file, $scope)
.then(function(result) {
$scope.imageSrc = result;
});
$scope.files.push(args.file);
});
});
});
In AngularJS not all functions are been processed by Dependency Injection. In Controllers, Directives (in definition of directive and in controller, not on link or compile), Servicies AngularJS inject requested instances, but in some other functions (like event listeners) arguments are passed by position.
In your case you need to put fileReader into definition on controller, not on event listener.
Also you need to remove apply because event listeners added via $on are included into digest loop.
Thanks to all for your replies. Val you made me go back and do a little more research and I found the answer with a little debugging. Not sure I understand why yet, but I have an idea.
If there is an error in your factory service, in my case, FileReaderService angular won't always explode when bootstrapping the service, will only explode when you call the service, which makes kind of makes sense. If something is wrong in the service the entire service will not boot. Also, you won't get any error message when injecting it into the controller. I had to place a watch on the module and noticed there was a reference error. I found I had a missing function.
Purely inexperience on my end, but I kept trying to capture the results form the $q service, which is was doing fine, but then attempting to inject to outside the $q return i.e. I was attempting to capture $scope.imageSrc = result and insert it post the .then, which doesn't work as you have a sync issue. I could see the value in the $scope.files, but it would not console.log or show up in HTML. So I moved all the file manipulation into the .then and it works perfectly. Logical when you think about it :) why have a $q if you not going to use it ... lol.
// problem code
fileReader.readAsDataUrl(args.file, $scope)
.then(function(result) {
$scope.imageSrc = result;
});
// cannot and should not try to work the results outside the return promise
$scope.files.imgSource = $scope.imageSrc;
$scope.files.push(args.file);
//Fixed and working code
myApp.controller('FileController', ['$scope', 'FileReaderService', function($scope, FileReaderService ){
var reader;
$scope.files = [];
//listen for the file selected event
$scope.$on("fileSelected", function (event, args) {
$scope.progress = 0;
var file = args.file;
FileReaderService.readAsDataUrl(file, $scope)
.then(function(result) {
file.imgSource = result;
$scope.files.push(file);
});
});
});

How to fix injector error after Angular minification build?

Before speaking, I read about it made ​​recommendations but still causing error. Look the short code:
function IndexController($scope, $route, $routeParams, $location){
$scope.sfv = project.version.name;
}
angular.module("TkwebMobile", ['ngRoute', 'ngCookies'])
.controller('IndexController', ['$scope', '$route', '$routeParams', '$location', IndexController]);
Only this and the error persists. I'm using grunt to "uglify", and I'm also using the "concat" to unite the codes in a "lib". Even I using "injection" recommended in the Angular documentation.
Uncaught Error: [$injector:modulerr] Failed to instantiate module TkwebMobile due to:
Error: [$injector:unpr] Unknown provider: a
Is it problem of grunt concat? (grunt-contrib-concat)
This is due to your minification, specifically options to minify and mangle your variable names.
Angular determines what value to inject into your functions from the name of the parameters. For example...
angular.factory('MyFactory', function($location) {...});
...will cause angular to look for whatever dependency is named '$location' and then call your function with the $location value passed as it's parameter.
When you minify your javascript, with an option called mangle turned on, then the variable names get mangled. The previous function will turn into this...
angular.factory('MyFactory', function(a) {...});
Angular no longer has the correct parameter name in your source code, as $location is now a. This saves on size of your javascript but totally destroys Angular's implicit dependency resolution. You can solve this in one of two ways.
The first is a feature that angular provides for you.
angular.factory('MyFactory', ['$location', function(a) {...}]);
You provide the names of the parameters in an array, with the last element of the array being the function to inject the parameters into. This way, it doesn't matter what you call your parameters in the code, and the minifier will never change a string literal so Angular always knows what you're wanting.
The other way if you don't want to lose the convenience of not having to use the array notation is to turn off the mangle setting on your minifier. This obviously means you don't minify to the same degree, but ask yourself if it's really worth those extra bytes.
A halfway house is to use something like ngMin, to allow annotation of the array notation into your code and then continue with the minification. This is the best of both world's imo, but increases the complexity of deploying your clientside js.
EDIT
The correct settings to turn off the mangle behaviour in grunt would be this...
uglify: {
options: {
report: 'min',
mangle: false
}
}
But the ngAnnotate package can avoid this. See here for more info. (ngAnnotate is the package that has taken over ngMin)
I've had a similar problem. It turned out that I was not properly identifying and formatting the dependencies for controllers and services, etc. I believe I discovered this by looking at the minification output. (It was rough, let me tell you.)
Basically I had to look through all my files and verify that the dependency list matched what I was using in my controllers and services. It's strange because it worked without the changes.
Here is an example of what I had to do:
Original:
angular.module('FootCtrl', []).controller('FooterController', function($scope) {
$scope.footer = 'Copyright \u00A9 ' + new Date().getFullYear() + name;
});
Fixed
angular.module('FootCtrl', []).controller('FooterController', ["$scope", function($scope) {
$scope.footer = 'Copyright \u00A9 ' + new Date().getFullYear() + name;
}]);
Maybe take note of where I use single quotes vs. double quotes as well.
Hopefully this helps a bit. I'm not sure if you have more code than what is shown- if not, I'm not too sure.
I had this problem and it took me a lot of time to figure out what the issue was because I tried disabling mangling of variable names, using $inject array instead of just passing the services and provider names to function definitions while relying on angular implicit dependency injection but still the problem persisted. It turned out that in one of my controller which uses IIFE, was missing semicolon at the end. Look at the code below.
Before:
(function(){
})()
The above code works okay before minification due to automatic semicolon insertion but it breaks after minification because the absence of semicolon screw things up. So after correction it looked like below.
Correct:
(function(){
})();
This fixed my problem. I hope this might help some one
Note: I use grunt useminPrepare, usemin, copy, uglify and ngAnnotate.

Adding directives to an element using the compile function

I'm trying to create a simple directive to avoid having bulky elements for hovering. This is what I have:
app.directive('ngHover', function(){
return {
compile: function($element,attr) {
$element.attr('ng-mouseenter',attr.ngHover + ' = true')
.attr('ng-mouseleave',attr.ngHover + ' = false')
.removeAttr('ng-hover');
}
}
})
The resulting element is what I would have wrote (and would have worked) but it doesn't seem to be added before angular uses $compile. I could use $compile manually but I want to understand why this doesn't work.
DEMO
This is how the compiler of Angular works.
Compiler is an Angular service which traverses the DOM looking for
attributes. The compilation process happens in two phases.
Compile: traverse the DOM and collect all of the directives. The
result is a linking function.
Link: combine the directives with a scope and produce a live view...
Which means that by the time when you are inside of compile function where you add the attributes, it's over and compiler will never discover and recognise ng-mouseenter and ng-mouseleave as directives. In order to achieve this you will need to trigger another round of compilation with $compile, as you said.
Also see this message and the whole thread. There you can see that it would work if you were setting extra directives on children of the current element, but not on itself.

Should angular.module be set variable on AngularJS

I’ve read two AngularJS sample on Github. I’m confused to how to modularize controller. The first on is set variable(foodMeApp) and re-use it. Controllers need less arguments. It’s easier to read.
But, second one doesn’t use variable(foodMeApp) and has more arguments. I think this way is frequently used on AngularJS samples.
Is this any merit to use the second way?
1.https://github.com/IgorMinar/foodme
var foodMeApp = angular.module('foodMeApp', ['ngResource']);
foodMeApp.constant('CONFIG_A', {
baseUrl: '/databases/',
});
foodMeApp.controller('HogeController', function HogeController($scope, CONFIG_A) {
console.log(“less arguments");
});
2.https://github.com/angular-app/angular-app
angular.module('foodMeApp', ['ngResource']);
angular.module('foodMeApp').constant('CONFIG_B', {
baseUrl: '/databases/',
});
angular.module('foodMeApp').controller('HogeController', ['$scope', 'CONFIG_B', function($scope, CONFIG_B) {
console.log("more arguments");
}]);
since angular.module('...') ,constant,provide,controller,factory ... return the same mdoule you can chain module method calls if you want to or not... it's just javascript.
you can write
angular.module('foo',[])
.controller('bar',function(){})
.service('baz',functinon(){})
.constant('buzz',something)
.value('bizz',somethingelse);
it makes no different.
This example is using an array for your dependencies which are going to be injected into your controller. The array method is typically used when the code is going to minified at some time and allows angularjs to know exactly which items will be injected. If this array were not there, angular would only see 'a', 'b' as the items injected into the function and would not be able to figure out what those things were. Items in the array are strings and will not be changed when the minification happens.
angular.module('foodMeApp').controller('HogeController', ['$scope', 'CONFIG_B',
function($scope, CONFIG_B) {
console.log("more arguments");
}]);
Hope this helps explain the differences which you have seen.
There is no such 'merit' of going for 2nd option. 1st approach is more popular as it avoids repeating angular.module('foodMeApp') so many times.
If you give it a name "foodMeApp" you can directly use foodMeApp as you would have used in 2nd approach. Less repetition, simpler life.

Categories

Resources