angular component directives and templateurl, different response to outside controller - angularjs

I am getting some behavior that was unexpected with my first foray into directives.
I am using a directive with a templateurl and isolated controller. I was expecting the dom inside the templateurl to react based on 2 way binding between the dom ( in this case a select element ) and the directives controller. I put a '$watch' on the bound attribute, which fires off when the directive is first loaded. But then if the user selects a different option, it does not fire. This is not the normal functionality ive come to expect from my experience with controllers so far.
my directive is as follows:
(function () {
'use strict';
var projMgrApp = angular.module('projMgrApp')
.directive('elementStructure', function () {
var structureController = ['$location', '$scope', '$window', '_', '$http', 'isAdmin', 'pmElement',
'GetProject', 'Enums', 'Users',
function ($location, $scope, $window, _, $http, isAdmin, pmElement, GetProject, Enums, Users) {
/* Ive removed most code for simplification
*/
$scope.NewElementType = null;
$scope.$watch('NewElementType', function (val) { alert(val); })
}];
return {
restrict: 'EA',
scope: {
elementType: '#',
projectId: '#',
structureTemplate: '#'
},
controller: structureController,
templateUrl: '/partials/structure.html',
};
});
})();
Inside my template url I have this select which im expecting to call the watcher..
<select class="form-control"
ng-options="d for d in ProductionCategoryOptions" ng-model="NewElementType"></select>
Im loading this directive twice as separate components ( and not intending to share any values )
<fieldset class="tab-pane fade in" id="ProductionStructure">
<element-structure element-type="Production" project-id={{Project.Id}}" structure-template={{Project.ProjectSettings.SceneStructureTemplate}}"></element-structure>
</fieldset>
<fieldset class="tab-pane fade in" id="AssetStructure">
<element-structure element-type="Asset" project-id="{{Project.Id}}"></element-structure>
</fieldset>

So I've got the directive working as intended. It seems as though there was a few fundamental quirks in the angular docs that hadn't quite clicked yet, and more than likely most that im still not.
To get it to work properly i needed to pull in an ngmodel into the directive and 2 way bind it ( '=' ). Prior to doing this i was creating the 'model' inside the directives controller, which then wasnt forcing the view to update ( or even calling the watcher - which im guessing is a scope issue ). So instead ive created the model in the outside controller, and bound the directive to that which seems to work now as i intended.
In my outside controller i create the object that is to bound to my directive:
$scope.ProductionStructureModel = {
Elements: null,
NewElement: {
Type: null,
Path: null,
Name: null,
Length: null,
Audio: null,
Parent: null,
}
};
my directive then references this as follows:
return {
restrict: 'EA',
requires: '^^ngModel',
scope: {
elementType: '#',
projectId: '#',
structureTemplate: '#',
Model: '=ngModel',
},
controller: structureController,
templateUrl: '/partials/structure.html',
link: function (scope, element, attrs) {
scope.$watch('Model', function (val) {
alert(val);
console.log(val);
});
}
};
the html that calls my directive:
<element-structure element-type="Production" project-id="{{Project.Id}}"
structure-template="{{Project.ProjectSettings.SceneStructureTemplate}}"
ng-model="ProductionStructureModel"
></element-structure>
One thing i havent figured out yet however, is that even though its adjusting the model as expected and updating the view - it still isnt calling the watcher.. Would anyone be able to shed some light on this for me?

Related

AngularJS setting directive scope variables from a service

OK so I am trying to make a simple chat widget directive and I want it to be dynamically created wherever I want it on my app ie I do not want a hard coded solution. So I want the directive to be created from a service and therefore have its scope variables set via service helper methods. I think I see a few examples of this in some angularjs material directives for example I want to do something like:
$mdToast.simple()
.textContent('Marked as read')
.action('UNDO')
but instead my chat widget will be like:
$chatWidget.setParent(parent).setUser(user).setMessages(messages);
Here is an extremely simplified version of the directive so far:
angular
.module('app')
.directive('reChat', function ($chatWidget) {
return {
restrict: 'E',
replace: true,
template:
'<div id="chat-widget-container"><div id="chat-widget-header">User.name</div>'+
'<div id="chat-widget-body"<div ng-repeat="message in reMessages"><div>' +
'<p> {{message.body}} </p></div></div></div>' +
'<textarea ng-model="messageToSend" id="message-to-send" placeholder ="Type your message" rows="3"></textarea>' +
'<button class="btn btn-default" ng-click="sendMessage()">Send</button></div>'
,
scope: {
reMessages: '=',
reParent: '<',
reUser: '<'
},
link: function (scope, element, attrs) {
scope.sendMessage = function () {
//logic here...
};
};
});
So how do I set the three scope variables in my directive above (reMessages, reParent, reUser) from the following factory?
angular
.module('app')
.factory('$chatWidget', function ($document, $compile, $controller, $http, $rootScope, $q, $timeout) {
return {
// functions
}
})
Any help greatly appreciated.
In general, to set a directive scope property from a service, simply assign it:
app.directive('reChat', function ($chatWidget) {
return {
scope: {
reMessages: '=',
reParent: '<',
reUser: '<'
},
link: function (scope, element, attrs) {
scope.reMessages=$chatWidget.reMessages;
//OR asynchronously
$chatWidget.getReMessages()
.then(function(reMessages) {
scope.reMessages = reMessages;
}).catch(function(error) {
console.log(error);
}};
},
};
})
Since one-way < binding was introduced with V1.5, the AngularJS team recommends that two-way = binding be avoided. For inputs, use one-way < bindings and attribute # bindings. For outputs, use expression & bindings which function as callbacks to component events. The general rule should be to never change a parent object or array property from the component. This will make transition to Angular 2+ easier.
For more information, see AngularJS Developer Guide - Component-based application architecture.

Triggering ngChange from inside a custom directive manually

I want to build custom directives having the same ng-change syntax as a normal input field. Let's say I have a directive like this:
;(function() {
'use strict';
angular.module('myMod', []);
angular.module('myMod').directive('myDir', myDirDirective);
function myDirDirective() {
return {
restrict: 'E',
template: '
<input ng-change="checkInput1()" type=...>
<input ng-change="checkInput2()" type=...>
',
controllerAs: 'myDirDirective',
controller: myDirController,
require: 'ngModel',
scope: {},
bindToController: {
model: '='
}
};
}
// Controller goes here...
})(); // EOF
Now I want to define the input check methods like
function checkInput1() {
...
if( <changes in input 1 are legit> ) {
fireOuterNgChange();
}
}
function checkInput2() {
...
if( <changes in input 2 are legit> ) {
fireOuterNgChange();
}
}
And finally I want to be able to use my custom directive like:
<myDir ng-change="doSomethingAfterSomethingChanged()">
...
</myDir>
A simple use case for this would be a time picker with several input fields for hours : minutes : seconds : milliseconds. Just to give an example. I tried different approaches without success; How can I do this?
So here you need to perform two things :
Intercepting changes on your internal directive <input> elements.
Forwarding these changes to the parent of your custom <my-dir>.
For (1), you'll simply do as your said : You register callbacks in your directive's scope (scope.checkInput1 = function() { ... }).
Then, to forward the event to the parent (finally imitating the <input ng-change> behavior), you will need to declare an expression binding in the Directive's isolated scope like this :
scope: {
onDateChange: '&'
}
On the parent Controller, assuming that you declared some $scope.onDirectiveDateChange = function() { ... } in the scope, you just pass the callback into your custom Directive like this :
<my-dir on-date-change="onDirectiveDateChange()"></my-dir>
Then you call it from your directive's checkInput2 :
scope.onDateChange(); // This will call parent's "onDirectiveDateChange()" if defined
#Neozaru answer works perfect. But to be complete I'm posting a complete code example for easier understanding. Following the John Papa's Style Guide and using the controllerAs Syntax instead of $scope (example use case: having a re-usable userform):
Implement you custom directive with your custom ng-change events
First, the template
// my-dir-template.html
Username: <input type="text" name="username" ng-change="passwordForm.changeHandler()" ng-model="passwordForm.model">
Password: <input type="text" name="password" ng-change="passwordForm.changeHandler()" ng-model="passwordForm.model">
The directive and the controller
;(function() {
'use strict';
angular.module('myMod', []);
angular.module('myMod').directive('passwordForm', passwordFormDirective);
function passwordFormDirective() {
return {
restrict: 'E',
templateUrl: 'password-form.html',
controllerAs: 'passwordForm',
controller: passwordFormController,
require: 'ngModel',
scope: {},
bindToController: {
model: '=',
ngChange: '&' // << bind ng-change to controller (not scope)
}
};
}
function passwordFormController() { // no scope injected
// Pseudo this
var vm = this;
// implement controller
vm.changeHandler = changeHandler ;
// // // Method implementations
...
function changeHandler() {
// we could do validation here, altough I'm not sure if this would be a good idea here.
// for now, we only do the change listener notification
if(vm.ngChange === 'function') {
vm.ngChange();
}
}
}
})(); // EOF
Now we can use our directive with the normal ng-change listener. Maybe for registration of new users:
<password-form ng-model="the-model" ng-change="checkIfUsernameExists()">

Passing a parent directive attribute to a child directive attribute

I'm creating directives for a library that customers can use. I need to let the customers create their own templates for a directive and pass the absolute url value of that template into my directives. One of my directives will have another custom directive inside of it, and it's template will be figured out based upon the value of one of the parent directive's attributes. Here's an example:
<parent-dir menu-template="this.html" item-template="that.html"></parent-dir>
I have a template for this directive that looks like this:
<ul style="list: none" ng-repeat="item in menu">
<child-dir template="{{itemTemplate}}"></child-dir>
</ul>
My directives look like this:
angular.module('myApp')
.directive('parentDir', function () {
return {
restrict: 'E',
scope: {
menuTemplate: '#',
itemTemplate: '#',
menuType: '#',
menuName: '#',
menuId: '#',
},
templateUrl: function (element, attrs) {
alert('Scope: ' + attrs.menuTemplate);
return attrs.menuTemplate;
},
controller: function ($scope, $element, $attrs) {
$scope.defaultSubmit = false;
alert('Menu: '+$attrs.menuTemplate);
alert('Item: ' + $attrs.itemTemplate);
$scope.itemTemplate = $attrs.itemTemplate;
if ($attrs.$attr.hasOwnProperty('defaultSubmit')) {
alert('It does');
$scope.defaultSubmit = true;
}
}
};
})
.directive('childDir', function () {
return {
restrict: 'E',
require: '^parentDir',
templateUrl: function (element, attrs) {
alert('Item Template: ' + attrs.template);
return attrs.template;
},
controller: function ($scope, $element, $attrs) {
$scope.job;
alert('Under job: ' + $scope.itemTemplate);
}
};
});
I'm not showing all of the code but this is the main piece of my problem. When I run this, I keep getting undefined for the template on the childDir.
What is the best practice in perpetuating the value of itemTemplate from the parentDir so that the childDir can use it as it's template?
The reason you're running into problems is because the function that generates the templateUrl is running before a scope has been assigned to your directive - something that has to be done before interpolated data can be replaced.
In other words: at the point that the templateUrl function runs, the value of the template attribute is still "{{itemTemplate}}". This will remain the case until the directive's link (preLink to be precise) function runs.
I created a plunker to demonstrate the point here. Be sure to open the console. You'll see that templateUrl runs before both the parent and child linking functions.
So what do you do instead?
Fortunately, angular provides a $templateRequest service which allows you to request the template in the same way it would using templateUrl (it also uses the $templateCache which is handy).
put this code in your link function:
$templateRequest(attrs.template)
.then(function (tplString){
// compile the template then link the result with the scope.
contents = $compile(tplString)(scope);
// Insert the compiled, linked element into the DOM
elem.append(contents);
})
You can then remove any reference to the template in the directive definition object, and this will safely run once the attribute has been interpolated.

Controller must wait until Directive is loaded

I'm currently writing a small set of AngularJS controls, and I'm encountering an issue here.
The control which I'm creating is a button (let's start simple).
The directive looks like this:
officeButton.directive('officeButton', function() {
return {
restrict: 'E',
replace: false,
scope: {
isDefault: '#',
isDisabled: '#',
control: '=',
label: '#'
},
template: '<div class="button-wrapper" data-ng-click="onClick()">' +
'<a href="#" class="button normal-button">' +
'<span>{{label}}</span>' +
'</a>' +
'</div>',
controller: ['$scope', function($scope) {
// Controller code removed for clarity.
}],
link: function(scope, element, attributes, controller) {
// Link code removed for clarity.
}
}
});
I've removed some code because it will make my question very hard to understand and I don't believe it's needed here.
Inside my controller of my directive, I'm writing an API and that being done by executing the following code:
controller: ['$scope', function($scope) {
// Allows an API on the directive.
$scope.api = $scope.control || {};
$scope.api.changeLabel = function(label)
$scope.label = label;
}
}]
So, on my directive, I do have an isolated scope to which I can pass a control property. Through this property, I'll have access to the method changeLabel.
My control can be rendered by using the following directive in the HTML website:
<office-button control="buttonController"></office-button>
The controller on my application will looks like:
officeWebControlsApplication.controller('OfficeWebControlsController', ['$scope', function($scope) {
$scope.buttonController = {};
}]);
I can execute my changeLabel method right now by calling the following method in my scope
$scope.buttonController.changeLabel('Demo');
However, this doesn't work since at this point changeLabel is not defined. The button must first be completely rendered.
Any idea on how to resolve this particular issue?
Edit: Plunker Added
I've added a plunker to provide more information.
When the changeLabel method is called within the onClick event, then everything is working, but if I call it outside, it isn't working anymore. This is because the $scope.api.changeLabel is not defined at the moment of execution (button is not rendered yet). So I basically have to wait in my application's controller until the button is fully rendered.
Kind regards

Triggering a function with ngClick within ngTransclude

I have an unordered list loaded with four items from an array while using ngRepeat. The anchor tag in the list item has a function in the ngClick attribute that fires up a message. The function call works well when used like this:
<ul>
<li ng-repeat="n in supsNames">
<a ng-click="myAlert(n.name)">{{n.name}}</a>
</li>
</ul>
I created a simple directive for inserting unordered lists with list items. The list is loaded just fine but the same functioned I previously mentioned does not fire up. The code is as follows:
<div list items="supsNames">
<a ng-click="myAlert({{item.name}})">{{item.name}}</a>
</div>
Here is my javascript and angularjs code:
var app = angular.module('myapp', []);
app.controller('myCtrl', function($scope) {
$scope.title = 'ngClick within ngTransclude';
$scope.supsNames = [
{"name" : "Superman"},
{"name" : "Batman"},
{"name" : "Aquaman"},
{"name" : "Flash"}
];
$scope.myAlert = function(name) {
alert('Hello ' + name + '!');
};
});
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '='
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
console.log(scope);
}
};
});
I also have a plnkr in case you want to see what I tried to do:
http://plnkr.co/edit/ycaAUMggKZEsWaYjeSO9?p=preview
Thanks for any help.
I got the plunkr working. I'm not sure if its exactly what you're looking for. I copied the main code changes below.
Here's the plunkr:
http://plnkr.co/edit/GEiGBIMywkjWAaDMKFNq?p=preview
The modified directive looks like this now:
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '=',
ctrlFn: '&' //this function is defined on controller
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
//directive fn that calls controller defined function
scope.dirFn = function(param) {
if(scope.ctrlFn && typeof scope.ctrlFn == 'function') { //make sure its a defined function
scope.ctrlFn( {'name': param} ); //not sure why param has to be passed this way
}
}
}
};
});
And here's how it's called in the html file that's bound to your controller:
<div list items="supsNames" ctrl-fn="myAlert(name)">
<a ng-click="dirFn(item.name)">{{item.name}}</a>
</div>
I think what was happening before is that you were trying to use a function defined in your controller within the isolated scope of the directive, so it wasn't working--that function was undefined in the directive. So what I did was added another parameter to the directive that accepts method binding (I think that's what its called) with the '&'.
So basically you pass your controller method to the directive, and that method gets invoked however you want by the directive defined method I creatively named "dirFn". I don't know if this is the best way per se, but I've used it in an existing project with good results ;)
you need to pass the function to the directive
scope: {
items: '=', 'myAlert': '='
},
The ng-repeat inside the template of the directive insert a new scope and it require to call transclude funcion manually to work. I suggest remove ng-repeat and make the transclusion manually passing a copy of the controller scope and setting the item on each copy:
for(var i=0,len=scope.items.length;i<len;i++){
var item=scope.items[i];
var itemScope=scope.$parent.$new();
$transcludeFn(itemScope, function (clone,scope) {
// be sure elements are inserted
// into html before linking
scope.item=item;
element.after(clone);
});
};
I edit the pluker and I hope that could be helpfull: http://plnkr.co/edit/97ueb8SFj3Ljyvx1a8U1?p=preview
For more info about transclusion see: Transclusion: $transcludeFn

Resources