Is it possible with Angularjs directive to watch vars in dynamic controller - angularjs

I have a service that's being used in different controllers, and I would like to consolidate them into a directive that can be applied to the different pages. To start, I'm just trying to get a simple example to work. Say I have two controllers, both of which have a common function:
angular.module('myApp')
.controller('CtrlOne', function() {
$scope.watchVar = 0;
$scope.changeVar = function() {
$scope.watchVar = 1;
}
});
angular.module('myApp')
.controller('CtrlTwo', function() {
$scope.watchVar = a;
$scope.changeVar = function() {
$scope.watchVar = b;
}
});
Then, in a directive I'm trying to determine the controller at runtime, so that this variable can be watched regardless of the controller the page has (as long as it uses the 'watchVar' variable):
angular.module('myApp').directive('myDirective', function () {
return {
scope: {
ctrl: '#myDirective',
watchVar: '=',
},
controller: ctrl,
link: function(scope, element, attrs) {
scope.$watch('watchVar', function(newValue, oldValue) {
if (newValue) {
console.log("watchVar changed to: "+newValue);
} else {
console.log("watchVar remained: "+oldValue);
}
}, true);
}
};
});
With the HTML being something like this:
<div p2a-filter-cache='CtrlOne'>
Is this even possible?

Related

Unable to call Angular Directiive Method on Button Click

I'm trying to call directive method on button click from the calling controller.
Here is the directive code:
myApp.directive("helloDirective", function() {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
controller: 'helloDirectiveController',
controllerAs: 'model'
}
})
.controller("helloDirectiveController", function() {
var self = this;
self.actions = {
get: function() {
return self.msg;
},
set: function(msgData) {
self.msg = msgData;
}
});
I have call the get and set method from controller..
myApp.controller("indexController", [function() {
var self = this;
self.helloParam ={};
self.get = function() {
//how to call the Directive get method from here
}
}]);
i tried to create a fiddle here
plnkr
The idea
For me, the cleanest solution (so far) for your problem is a solution used by Angular Material developers (I don't know if they were the authors, but I found it there). I've used it once in my project and it worked like a charm.
The idea is to create a global registry for directives' actions. Directives would be stored there by unique ids. We can also create a service dedicated for each directive, just in case we need some external logic.
The solution
1. Components registry
Firstly, we need a components registry. It can be a really simple service:
angular.module('app');
.service('$componentsRegistry', function() {
var self = this;
var components = {};
self.put = function(id, actions) {
components[id] = actions;
}
self.get = function(id) {
return components[id];
}
})
The components registry has methods for storing and getting components by ids. Of course, there might be much more methods and they might be more complicated, but this is just a simple example.
2. Service for our directive
Let's say we have a simple show-message directive, so we can create a $showMessageService:
angular.module('app')
.service('$showMessageService', ['$componentsRegistry', function($componentsRegistry) {
return function(id) {
return $componentsRegistry.get(id);
}
}])
For now, the only task of the service is to return our directive's actions. But it can be extended in the future, of course.
3. Directive's code
The last thing we need is our show-message directive:
angular.module('app')
.directive('showMessage', function($componentsRegistry) {
return {
restrict: 'E',
scope: {
directiveId: '#' // Unique id is passed from the view
},
template: '<div>{{ text }}</div>',
link: function(scope) {
scope.text = 'TEST TEXT';
// Create actions
scope.actions = {
set: function(value) {
scope.text = value;
}
}
// Store actions in the components registry
$componentsRegistry.put(scope.directiveId, scope.actions);
}
}
})
In a link function, we need to simply register our actions in components registry. We pass the unique id from the view so that developer has control over it inside views/controllers.
Example of usage
And now we can finally use the directive in our application. Here is a simple code which shows how to do that:
View
<div ng-controller="testController as test">
<show-message directive-id="testDirective"></show-message>
<button ng-click="test.changeText()">CHANGE</button>
</div>
Controller
angular.module('app')
.controller('testController', function(['$showMessageService', $showMessageService) {
var self = this;
self.changeText = function() {
$showMessageService('testDirective').set('NEW');
}
}])
I've Change The Directive Code, moved Controller code to link and it's working fine.
testApp.directive("helloDirective", function () {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
link: function (scope, element, attrs) {
var self = scope.model;
var assignMethod = function () {
if (self.param == undefined) {
self.param = {};
}
self.param.actions = {
get: function () {
return self.msg;
},
set: function (msgData) {
self.msg = msgData;
}
};
};
assignMethod();
},
controller: function () { },
controllerAs: 'model'
}
});
now i can call the directive get Method from calling controller like,
self.helloParam = {};
self.click = function () {
alert(self.helloParam.actions.get());
}

Include different angular directives in ng-repeat

I have several angular directives.
I want to include them in an accordion directive with an ng-repeat depending on the type.
I want it to be reusable, so I don't want the accordion to have knowledge of the type of directives it is rendering.
That's why I don't want to use an ng-switch directive inside the accordion.
Here is a very simplified demo. In reality those directives will have their own attributes for angular to compile.
var testApp = angular.module('testApp', []);
(function() {
function Element1() {
return {
template: '<span>hello</span>',
restrict: 'E',
replace: true
}
}
testApp.directive('elementOne', Element1);
}());
(function() {
function Element2() {
return {
template: '<span>world</span>',
restrict: 'E',
replace: true
}
}
testApp.directive('elementTwo', Element2);
}());
(function() {
function Accordion() {
return {
template: '<ul><li ng-repeat="element in elements"><button>Toggle</button> Here should be the directive</li></ul>',
restrict: 'E',
replace: true,
controller: function($scope) {
$scope.elements = [{
type: 1
}, {
type: 2
}];
}
}
}
testApp.directive('elementAccordion', Accordion);
}());
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="testApp">
<element-accordion></element-accordion>
</div>
Thanks!
I've built something very similar a few weeks ago. I wanted to render a list of directives and select the type of directive based on the type of an element in an array (just like you). I tried to avoid ngSwitch because I've really many directives and want to add new directives without changing the ngSwitch statement every time. Let me show a simplified version of what I did. I hope this will help you to build your own version.
First, we need a global directory of element diretives and a helper function to register new directives. In my case, the helper function even creates the directive, but I'll skip this to keep it simple:
angular.module("app").provider('elementDirectory', function() {
return {
elements: [],
$get: function () {
return {
elements: this.elements
}
}
};
});
function registerElementDirective(type, directiveName) {
angular.module("app").config(['elementDirectoryProvider', function (elementDirectoryProvider) {
elementDirectoryProvider.elements.push({
type : type,
directive : directiveName
});
}]);
}
Now, we can create some directives like this:
angular.module("app").directive('elementA', function() {
return {
template: '<span>ElementA</span>',
}
});
registerElementDirective('A', 'element-a');
The most interesting part is a directive I called elementSwitch. It takes an array of elements and dynamically adds an angular.element for each element. Therefore, it creates prototypes for each element in the elementDirectoy and uses the clone() method on changes. (I think we could skip this part, it was an optimization).
angular.module('app').directive('elementSwitch', elementSwitch);
elementSwitch.$inject = ['$compile', 'elementDirectory'];
function elementSwitch($compile, elementDirectory) {
var prototypes = {};
elementDirectory.elements.forEach(function (e) {
var directiveElem = angular.element('<' + e.directive + '>');
prototypes[e.type] = directiveElem;
});
return {
scope: {
elements: '=elementSwitch'
},
link: function (scope, elem, attr) {
var childScopes = [];
scope.$watchCollection('elements', function (newValue, oldValue) {
childScopes.forEach(function (s) {
s.$destroy();
});
childScopes = [];
elem.empty();
newValue.forEach(function (element, index) {
var childScope = scope.$new();
childScopes.push(childScope);
var directiveElem = prototypes[element].clone();
directiveElem.attr('element', '_elements[' + index + ']');
$compile(directiveElem)(childScope);
elem.append(directiveElem);
});
scope._elements = newValue;
});
}
}
}
Here is a complete example: https://codepen.io/hansmaad/pen/oLvRmj
It doesn't do the same thing you want, but you should get an idea how you could achieve your goal.

where to put the functions in angularjs, service or directive

I have a grid directive and related service. The code like below:
(function (window, angular) {
'use strict';
angular.module("myModule")
.service("myGrid", [
function () {
var self = this;
this.init_grid = function (scope, config) {
scope.itemsList = []; // particular grid data
};
this.methodsList = {
add: function () {
// add logic...
},
edit: function (scope, evt, data) {
}
};
}
])
.directive("myGridView", ["$compile", "myGrid", function ($compile, myGrid) {
return {
restrict: "E",
replace: true,
transclusion: true,
templateUrl: "views/gridTemplate.html",
scope: {
config: "="
},
link: function ($scope, iElement, iAttrs, controller) {
myGrid.init_grid(scope, scope.config);
// register method in scope
angular.forEach(myGrid.methodsList, function (method, k) {
scope[k] = method;
//scope[k] = function() {method($scope)}
});
}
};
}
])
;
})(window, window.angular);
<form>
<my-grid-view config="config1"></my-grid-view>
<my-grid-view config="config2"></my-grid-view>
<my-grid-view config="config3"></my-grid-view>
</form>
My question is that:
As the form contains more than one grid, and they call the same functions in service, how can the add or edit functions know which grid calling and how to dealer with the particular data in grid?
Currently,
(1)I pass the scope to the add or edit function to implement//scope[k] = function() {method($scope)},
(2)Another way, in link function, define $scope.add = function() {}
(3)I can also define a controller in directive and defines these function.
What is the best practice to implement this purpose?

AngularJS: access directive data from controller

Lets assume I have a AngularJS directive looking like this:
app.directive('psDIR', [
function() {
return {
template: "<div style='padding: 5px; border: 1px solid red; margin-bottom: 10px;'><p>This is a direcive:</p> <textarea rows='5' cols='50' ng-model='md'></textarea></div>",
restrict: 'AEC',
scope: {}
}
}
]);
I am using this directive number of times on a single page. How do I get a value of every directive instance/scope of the ng-model="md" in my MainCtrl (i.e. I want to save this value in the add()) :
app.controller('MainCtrl', ['$scope',
function($scope) {
console.log("init");
$scope.add = function() {
console.log($scope);
}
}
]);
Plunker demo: http://embed.plnkr.co/Q5bw6CBxPYeNe7q6vPsk/preview
Any suggestions much appreciated.
Since you are creating isolated scope and otherwise too you cannot access the child scope from parent scope.
The way out it to pass the model as parameter from parent like
<div class="psDIR" model='field2'></div>
<div class="psDIR" model='field1'></div>
Then in the directive update them with attribute binding. See update plunkr
http://plnkr.co/edit/GKZHSDe5J0eCzqlgaf4g?p=preview
Usually the right thing to do when you want to communicate between scopes (i.e. directives and other controllers) is to use a service.
You can take a look at a simple plunker here: http://plnkr.co/edit/8Al31Bq9BfoazUCpqhWy?p=preview
The psDirs service keeps a registry of the directives:
app.service('psDirs', function() {
var psDirs = {
dirs: [],
register: function (dir) {
psDirs.dirs.push(dir);
}
};
return psDirs;
});
And the directives register themselves and update the value when they change:
link: function (scope, elm, attr) {
var dir = { val: "" };
psDirs.register(dir);
scope.$watch('md', function (n, o) {
if (n !== o) {
dir.val = n;
}
});
}
Then your controller can inject the psDirs service and access the directive registry as needed. This prevents brittle scope relationships, and allows the the directive registry service to be used elsewhere, in multiple controllers and sections of your application.
A possible solution is with require:
app.directive('psDIR', [
function() {
return {
...,
require: "ngController",
link: function(scope, elem, attrs, ngCtrl) {
ngCtrl.hook(scope);
}
}
}
]);
And the necessary change to the controller:
app.controller('MainCtrl', ['$scope',
function($scope) {
console.log("init");
$scope.add = function() {
var i;
for( i=0; i < psDirs.length; i++ ) {
console.log(i + " -> " + psDirs[i].md);
}
}
var psDirs = [];
this.hook = function(scope) {
psDirs.push(scope);
};
}
]);
Try it out here: http://plnkr.co/edit/zCZ1TOm3aK8V4piCAY6u?p=preview
If you do decide to go with this solution, I'd suggest implementing the "wrapper" controller in a specialized wrapper directive, so as to avoid situations where ng-controller may be set in some other element in the hierarchy. In this case, just require: "wrapperDirective".
EDIT: The HTML for the wrapper directive case would simply look like:
<div wrapper-directive>
<div class="psDIR"></div>
<div class="psDIR"></div>
<button ng-click="add()">Add</button>
</div>
And the directive itself uses the code of the previous ng-controller:
app.directive('wrapperDirective', function() {
return {
restrict: "A",
// YOU MAY WANT ISOLATED SCOPE, JUST ADD scope: {},
controller: ["$scope", function($scope) {
// SAME CODE AS BEFORE
console.log("init");
$scope.add = function() {
var i;
for( i=0; i < psDirs.length; i++ ) {
console.log(i + " -> " + psDirs[i].md);
}
}
var psDirs = [];
this.hook = function(scope) {
psDirs.push(scope);
};
}]
};
});
And of course change the require configuration:
app.directive('psDIR', [
...
require: "^wrapperDirective",
link: function(scope, elem, attrs, wrapperDirectiveCtrl) {
wrapperDirectiveCtrl.hook(scope);
}

Get AngularJS to compile directive and link function before executing the controller

Due to the nature of my program, I require functions to be placed on the scope and shared between the directive's link function and the controller, like so..
.controller("controller", function($scope, $location, $timeout, model) {
//irrelevant code
$scope.addObject(draggables[i]);
};
.directive("designCanvas", function($timeout) {
return {
restrict: "A",
link: function($scope, element) {
$scope.addObject = function(draggable) {
// irrelevant code
}
}
}
}
When I make this function call I get '$scope.addObject is not a function'.
My problem is the controller is being executed before angularJS has evaluated the link function, because the function call works fine when I delay it by a few seconds using $timeout
So my question is, how can I get the contents of the link function to compile first?
I recommend writing this function as a service and inject the service into the directive and controller. Shared function should be implemented as a service.
.factory("objectService",function(){
return {
addObject : function (draggable){
//your code of this function
}
};
});
.controller("controller", function($scope, $location, $timeout, model,objectService) {
//irrelevant code
$scope.addObject = function (draggable){
objectService.addObject(draggable); //reuse this function
//objectService.addObject.call($scope,draggable) if you want to call this function with $scope as the context.
};
};
.directive("designCanvas", function($timeout,objectService) {
return {
restrict: "A",
link: function($scope, element) {
$scope.addObject = function(draggable) {
objectService.addObject(draggable); //reuse this function.
//objectService.addObject.call($scope,draggable) if you want to call this function with $scope as the context.
//write more logic specific to this function, like modifying link's local data.
}
}
}
}
I would create $watch in controller that listen on some flag in directive.
Something like:
.controller("controller", function($scope, $location, $timeout, model) {
//irrelevant code
$scope.flag = false;
$scope.$watch(function () {
return $scope.flag;
},
function (newValue, oldValue) {
if(newValue == true){
$scope.addObject(draggables[i]);
}
}, true);
};
.directive("designCanvas", function($timeout) {
return {
restrict: "A",
link: function($scope, element) {
$scope.flag = true;
$scope.addObject = function(draggable) {
// irrelevant code
}
}
}
}
It's a bit messy but I think this is a direction. Instead $scope.flag you can try use callback like:
.controller("controller", function($scope, $location, $timeout, model) {
$scope.callback= function() {
$scope.addObject(draggables[i]);
}
};
.directive("designCanvas", function($timeout) {
return {
restrict: "A",
link: function($scope, element) {
$scope.addObject = function(draggable) {
// ....
$scope.callback();
}
}
}
}
Hope it will help,
I would create another directive and make it depend on designCanvas:
.directive("designCanvas", function() {
return {
controller: function() {
this.addObject = function(draggable) {
alert(draggable.name);
}
}
}
})
.directive("draggable", function() {
return {
require: '^designCanvas',
link: function(scope, element, attrs, designCanvasController) {
designCanvasController.addObject(scope.item);
}
}
});
Usage something like this:
<div design-canvas>
<div ng-repeat="item in draggables" draggable>{{item.name}}</div>
</div>
Demo plunker.
Tying directive to controller is not the best idea, I think. You should either go with service or one more directive.

Resources