We mostly write our controllers in this fashion:
app.controller('MyCtrl', function($scope, $location) {
//do something with $scope & $location
});
I am writing a directive, and I am faced with a scenario where I have to render a view based on a certain controller instance. The directive will be called as follows:
<my-directive src="srcVar" controller="myctrl"></my-directive>
This directive takes care of loading the template specified by srcVar and instancing the controller using the $controller service. So there are few lines in my code that does like:
$controller(çtrlExp, {'$scope' : scope.$new() });
The above works for simple cases where the controller has only one argument. For the above controller example, you can stuff work the following manner:
var locals = { '$scope' : $scope.$new(), '$location' : $injector.get('$location') };
$controller('MyCtrl', locals);
Now how to write it for a generic case, where the user's controller can include any number of injectable constructs like services, values, constants, etc, all of which are usually defined during module creation.
Ps: if you are looking for workable code...refer my github repo: https://github.com/deostroll/ngFrame/blob/master/app/scripts/viewutils.js . This is still a work in progress sort of library.
You only need to add the $scope in your locals the rest will be done automatically by Angular's DI.
Please have a look at the demo below or this jsfiddle.
angular.module('demoApp', [])
.controller('mainController', MainController)
.directive('myDirective', MyDirective);
function MainController($scope, $location) {
this.testTemplate = 'testTemplate.html';
console.log($location, $scope); // everything available here!
}
MainController.$inject = ['$scope', '$location'];
function MyDirective($compile) {
return {
restrict: 'E',
template: 'hello',
controller: function($scope, $attrs, $controller){
console.log($attrs.controller, $scope);
$controller($attrs.controller, {'$scope': new $scope.$new()});
},
compile: function(element, attrs, $scope) {
var template = angular.element(document.getElementById(attrs.src))
.html();
console.log(template);
element.replaceWith(template);
return function(scope, element, attrs) {
//template is compiled now and in DOM, add scope variable
scope.hello='Hello from directive';
};
}
};
}
MyDirective.$inject = ['$compile'];
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="demoApp">
<script type="text/ng-template" id="testTemplate.html">
<div>
<h1>Test template</h1>
{{hello}}
</div>
</script>
<my-directive src="testTemplate.html" controller="mainController"></my-directive>
</div>
Related
I am adding this my-resize directive:
<body ng-controller="MainCtrl" my-resize="resize">
<p>width:{{width}} height:{{height}}</p>
</body>
,where resize is a function defined in the MainCtrl controller (see: http://plnkr.co/edit/Z8ckbLbRcA6P6XqzU1fx)
The directive is very simple:
app.directive('myResize', function($scope){
return{
restrict: 'A',
scope: {
ngResize: '&'
},
link: function($scope, $elem, $attr){
$scope.$on('resize', ngResize);
}
};
});
Yet I am getting Error: $injector:unpr Unknown Provider and I have no idea why.
var app = angular.module('app', []);
app.directive('myResize', ['$window', function($window) {
return {
link: function(scope, elem, attrs) {
scope.onResize = function() {
scope.height = $window.innerHeight;
scope.width = $window.innerWidth;
}
scope.onResize();
angular.element($window).bind('resize', function() {
scope.onResize();
scope.$apply();
})
}
}
}])
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div my-resize>
window height: {{height}}
window width:{{width}}<br />
</div>
</div>
$scope can not be injected to directive. You have change the code to inject $scope in controller of directive.
$scope is not a service($scopeProvider is not exist in angular js) it is something special that is injected by angular itself into the controller as a child of $rootScope.
so you cannot explicitly inject it in service,directive...etc.
you can inject it explicitly in the controller of direcitve (not directly to the directive).
http://plnkr.co/edit/mxEMXxIKOrJtRqQa4IQb?p=preview
I want o get the value of and attribute in my angular controller but it is undefined. If it helps everything is inside an MVC.net application.
The simplified codes:
TestCtrl.js:
angular.module('CMM')
.controller('TestCtrl', ['$scope', '$element', '$attrs', function ($scope, elem, $attrs) {
$scope.name = "mohsen";
$scope.api = $attrs.api;
alert($attrs.api);
alert(elem.data('api'));
console.log('hi, I am here');
}]);
app.js:
var app = angular.module('CMM', ['ngRoute','slServices', 'slControllers', 'ngAnimate', 'smart-table', 'ui.bootstrap']);
.....
usage:
<div id="content" ng-controller="TestCtrl">
<h3>
<input type ="text" ng-model="name"/>
<input type="text" ng-model="api" api="/sl/asdf"/>
....
Your code is not working as written because $element and $attrs are not injectable. As discussed in the comments, you can use a directive to get access to the attributes and reference the controller from the directive. This is not really the proper way to do this, but it can be made to work.
So your controller could expose a method that is used to provide it with the API value from the directive, like this:
.controller("TestCtrl", function ($scope) {
this.SetApi = function (api) {
$scope.api = api;
};
})
And your directive can reference the controller and call the method to supply the value, like this:
.directive("getApi", function () {
return {
controller: "TestCtrl",
link: function (scope, element, attr, controller) {
controller.SetApi(attr.api);
}
};
});
Here is a working example: https://jsfiddle.net/n5t6pfcc/
I have (sort of) the following html:
<div ng-controller="MyController">
<my-sub-directive></my-sub-directive>
</div>
how the controller looks is not important:
app.controller("MyController", function($scope) {
$scope.foo = "bar";
})
and my directive looks like this:
function mySubDirective() {
return {
restrict: "E",
templateUrl:"aTemplate.html",
require: "^MyController",
link: function($scope, element) {
}
};
}
app.directive("mySubDirective", mySubDirective);
In the documentation they always specify another directive in the require-property, but it says that it means you require the controller. So I wanted to try this solution. However I get the error
"Controller 'MyController', required by directive 'mySubDirective', can't be found".
Is it not possible to require a controller from the directive if it is set by ng-controller?
You can only do:
require: "^ngController"
So, you can't be more specific than that, i.e. you can't ask for "MainCtrl" or "MyController" by name, but it will get you the controller instance:
.controller("SomeController", function(){
this.doSomething = function(){
//
};
})
.directive("foo", function(){
return {
require: "?^ngController",
link: function(scope, element, attrs, ctrl){
if (ctrl && ctrl.doSomething){
ctrl.doSomething();
}
}
}
});
<div ng-controller="SomeController">
<foo></foo>
</div>
I don't think, though, that this is a good approach, since it makes the directive very dependent on where it is used. You could follow\ the recommendation in the comments to pass the controller instance directly - it makes it somewhat more explicit:
<div ng-controller="SomeController as ctrl">
<foo ctrl="ctrl"></foo>
</div>
but it still is a too generic of an object and could easily be misused by users of your directive.
Instead, expose a well-defined API (via attributes) and pass references to functions/properties defined in the controller:
<div ng-controller="SomeController as ctrl">
<foo do="ctrl.doSomething()"></foo>
</div>
You can use element.controller() in the directive link function to test the closest controller specified by ngController. A limitation of this method is that it doesn't tell you which controller it is. There are probably several ways you can do it, but I'm opting to name the controller constructor, and expose it in the scope, so you can use instanceof
// Deliberately not adding to global scope
(function() {
var app = angular.module('my-app', []);
// Exposed in so can do "instanceof" in directive
function MyController($scope) {}
app.controller('MyController', MyController);
app.directive("foo", function(){
return {
link: function($scope, $element){
var controller = $element.controller();
// True or false depending on whether the closest
// ngController is a MyController
console.log(controller instanceof MyController);
}
};
})
})();
You can see this at http://plnkr.co/edit/AVmr7Eb7dQD70Mpmhpjm?p=preview
However, this won't work if you have nested ngControllers, and you want to test for one that isn't necessarily the closest. For that, you can defined a recursive function to walk up the DOM tree:
app.directive("foo", function(){
function getAncestorController(element, controllerConstructor) {
var controller = element.controller();
if (controller instanceof controllerConstructor) {
return controller;
} else if (element.parent().length) {
return getAncestorController(element.parent(), controllerConstructor);
} else {
return void(0); // undefined
}
}
return {
link: function(scope, element){
var controller = getAncestorController(element, MyController);
// The ancestor controller instance, or undefined
console.log(controller);
}
};
})
You can see this at http://plnkr.co/edit/xM5or4skle62Y9UPKfwG?p=preview
For reference the docs state that the controller function can be used to find controllers specified with ngController:
By default retrieves controller associated with the ngController directive
I've got an Angular view thusly:
<div ng-include="'components/navbar/navbar.html'" class="ui centered grid" id="navbar" onload="setDropdown()"></div>
<div class="sixteen wide centered column full-height ui grid" style="margin-top:160px">
<!-- other stuff -->
<import-elements></import-elements>
</div>
This is controlled by UI-Router, which is assigning the controller, just FYI.
The controller for this view looks like this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
$scope.loadOfficialFrameworks();
// other stuff here
});
The <import-elements> directive, looks like this:
angular.module('pcfApp').directive('importElements', function($state, $stateParams, $timeout, $window, Framework, OfficialFramework) {
var link = function(scope, el, attrs) {
scope.loadOfficialFrameworks = function() {
OfficialFramework.query(function(data) {
scope.officialFrameworks = data;
$(".ui.dropdown").dropdown({
onChange: function(value, text, $item) {
loadSections($item.attr("data-id"));
}
});
window.setTimeout(function() {
$(".ui.dropdown").dropdown('set selected', data[0]._id);
}, 0);
});
}
return {
link: link,
replace: true,
templateUrl: "app/importElements/components/import_elements_component.html"
}
});
I was under the impression that I'd be able to call the directive's loadOfficialFrameworks() method from my controller in this way (since I'm not specifying isolate scope), but I'm getting a method undefined error on the controller. What am I missing here?
The problem is that your controller function runs before your link function runs, so loadOfficialFrameworks is not available yet when you try to call it.
Try this:
angular.module('pcfApp')
.controller('ImportElementsCtrl', function($scope, $http, $location, $stateParams, $timeout, Framework, OfficialFramework) {
//this will fail because loadOfficialFrameworks doesn't exist yet.
//$scope.loadOfficialFrameworks();
//wait until the directive's link function adds loadOfficialFrameworks to $scope
var disconnectWatch = $scope.$watch('loadOfficialFrameworks', function (loadOfficialFrameworks) {
if (loadOfficialFrameworks !== undefined) {
disconnectWatch();
//execute the function now that we know it has finally been added to scope
$scope.loadOfficialFrameworks();
}
});
});
Here's a fiddle with this example in action: http://jsfiddle.net/81bcofgy/
The directive scope and controller scope are two differents object
you should use in CTRL
$scope.$broadcast('loadOfficialFrameworks_event');
//And in the directive
scope.$on('loadOfficialFrameworks_event', function(){
scope.loadOfficialFrameworks();
})
I have a custom directive and I would like to use it to include an html content to the document after clicking on it.
Plunker: http://plnkr.co/edit/u2KUKU3WgVf637PGA9A1?p=preview
JS:
angular.module("app", [])
.controller("MyController", function ($scope) {
})
.directive('addFooter', ['$compile', '$rootScope', function($compile, $rootScope){
return {
restrict: 'E',
template: '<button>add footer</button>',
controller: 'MyController',
link: function( scope, element, attrs, controller) {
element.bind( "click", function() {
scope.footer = "'footer.html'";
})}
};
}])
HTML:
<body ng-app="app">
<script type="text/ng-template" id="footer.html">
FOOTER
</script>
<div ng-controller="MyController">
<add-footer></add-footer>
<div ng-include="footer"></div>
</div>
</body>
Not sure why it is not working, as it worked fine before it was moved into the directive. Outside the directive, I was also referencing to $scope.footer with some link. I tried using $rootScope, but also no effect. Any tips please?
First. Remove unnecessary quote symbols:
element.bind( "click", function() {
scope.footer = "footer.html"; // not "'footer.html'"
});
Second. You should notify angularjs that you have asynchronously updated scope values:
element.bind("click", function() {
scope.$apply(function() {
scope.footer = "footer.html";
});
});
Or like that
element.bind("click", function() {
scope.footer = "footer.html";
scope.$apply();
});