Angular: custom directive not updating controller - javascript

I started to learn Angular not so long time ago and I'm trying to understand scope, binding and etc.
I have an order details controller:
orderApp.controller('OrderDetailsController', ['$http','$routeParams','$scope','config', function($http, $routeParams, $scope, config){
var orderCtrl = this;
orderCtrl.orderId = $routeParams.orderId;
orderCtrl.order = {};
orderCtrl.editingView = false;
...
}]);
On order details page I want to output all information about selected order. Also we need to give user ability to edit order. Information about editing mode is stored in orderCtrl.editingView.
I decided to create custom directive. If edit mode is off - display text, otherwise display input.
orderApp.directive('editableText', function(){
return {
restrict: 'E',
scope: {
property: '=property',
editMode: '=editMode'
},
controller: 'OrderDetailsController',
controllerAs: 'orderCtrl',
templateUrl: '/pages/editable-text.html'
}
});
This is template:
<div class="col-xs-8" ng-if="!editMode">{{property}}</div>
<div class="col-xs-8" ng-if="editMode"><input type="text" class="form-control" ng-model="property"></div>
And this is how I use directive in html files:
<editable-text property="orderCtrl.order.coid" edit-mode="orderCtrl.editingView"></editable-text>
Text and input are switching when edit mode is on/off. Problem is that orderCtrl.order.coid property is not updated when I change it in input.
Before edit property looks like:
Turn on edit mode and change value:
Turn off edit mode and we see old value:
Do I need to synchronise controller values and directive scope? I thought that with 2-ways binding it should happen automatically. Probably there is any other way to write this functionality? Will appreciate any help.
UPD
Directive code:
orderApp.directive('editableText', function(){
return {
restrict: 'E',
bindToController: {
property: '=property',
editMode: '=editMode'
},
controller: 'OrderDetailsController',
controllerAs: 'orderCtrl',
templateUrl: '/pages/editable-text.html'
}
});
Directive template:
<div class="col-xs-8" ng-if="!orderCtrl.editMode">{{orderCtrl.property}}</div>
<div class="col-xs-8" ng-if="orderCtrl.editMode"><input type="text" class="form-control" ng-model="orderCtrl.property"/></div>
Directive usage:
<editable-text property="orderCtrl.order.coid" edit-mode="orderCtrl.editingView"></editable-text>
I'm not sure that we really need to pass edit-mode attribute.

You should use bindToController: { ..scope properties.. } option here inside your directive to make sure that isolated scope properties should get bounded to controller this context.
Directive
orderApp.directive('editableText', function(){
return {
restrict: 'E',
bindToController: {
property: '=property',
editMode: '=editMode'
},
controller: 'OrderDetailsController',
controllerAs: 'orderCtrl',
templateUrl: '/pages/editable-text.html'
}
});
Template
<div class="col-xs-8" ng-if="!orderCtrl.editMode">
{{orderCtrl.property}}
</div>
<div class="col-xs-8" ng-if="orderCtrl.editMode">
<input type="text" class="form-control" ng-model="orderCtrl.property"/>
</div>
Note:- this above bindToController: { ..scope properties.. } option available for angular 1.4+ versions.
For Angular 1.3 > version & 1.4 > version you should use former way of doing it by having bindingToController: true to bind scope variable to controller context & do keep the varaibles inside scope: { ...props... }
scope: {
property: '=property',
editMode: '=editMode'
},
bindToController: true

Related

Controller to be put inside Directive

I know it is possible to put Controller inside the Directive but I can't make it work of how it should be.
Added plunker link below.
My Working Directive and Controller in One Javascript File
This code works perfectly but I want to modify the app.controller and move it inside my app.directive
app.controller('NameCtrl', function () {
var vm = this;
vm.namePattern = '/^[a-zA-Z ]{1,25}$/';
});
app.directive('nameDirective', function () {
return {
restrict: 'AE',
templateUrl: '/name.html'
}
});
some codes in my Index HTML file
the first <div> is for the above code, and the second <div> is for my directive that I am working on(below).
<div ng-controller="NameCtrl" name-directive></div>
<!--<div name-directive></div> -->
codes in templateURl name.html file
it seems that ng-pattern="ctrl.namePattern" is not working at all, but it is completely working when I use the first JS code above and just modify it to ng-pattern="namePattern".
<input type="text" name="username" class="form-control" placeholder="Enter your first name"
ng-model="username"
ng-minlength="2"
ng-maxlength="20"
required
ng-pattern="ctrl.namePattern"/>
The codes I am working on my JS file (from the codes above)
app.directive('nameDirective', function () {
return {
restrict: 'AE',
scope: {
},
controller: function($scope) {
$scope.namePattern = '/^[a-zA-Z ]{1,25}$/';
},
controllerAs: 'ctrl',
bindToController: true,
templateUrl: '/name.html'
}
});
I hope, I explain clearly my problem.
This is the link of my sample code that I am working on.
If aren't using isolated scope, just remove it from your directive or use scope: true...
app.directive('nameDirective', function () {
return {
restrict: 'AE',
//scope: {}, //Or you can use scope: true (It hasn't much sense in your case)
controllerAs: 'ctrl',
bindToController: true,
controller: function() {
var vm = this;
vm.namePattern = '/^[a-zA-Z ]{1,25}$/';
},
templateUrl: '/name.html'
}
});
See your directive working, I've updated your PLUNKER.

AngularJS - Wrapping directives

It seems like I get confused by isolated scopes in directives and hope you can help me out.
I tried to wrap a piece of code (which contains some custom directives) into a new directive to reduce code duplication. Obviously I needed to give some attributes like ng-model into my new directive as a parameter to make the directive reusable. ng-model does not like expressions though (I tried ng-model="{{myVariableWhichContainsDesiredNgModelString}}" at first) and thus I ended up at this article: AngularJS - Create a directive that uses ng-model.
While the accepted answer seems to work for a simple setup, I edited the plunker from the accepted answer to test out if it would work with nested directives as well: (in my app I need to wrap directives from a third party-library which I can not edit) Plunker. In my code each directive seems to generate its own scope and two-way-databinding by using = in the scope definition does not seem to work out as desired.
EDIT: Since it was not clear what i am asking I edited the Plunker above and will rephrase the question: In the Plunker I have three input-fields which are supposed to bind to the same model-value. This works initially, but as soon as I edit the third input-field it generates its own variable in its isolated scope instead of updating the initial value. Obviously the third input field refers to the new variable from that point on. How can I avoid that behaviour and keep the input linked to $scope.model.name?
Observation: removing the isolated-scope-directive from the template makes everything work as expected...
template: '<div><my-input ng-model="myDirectiveVar"></my-input></div>',
instead of
template: '<div><my-isolated-scope-directive><my-input ng-model="myDirectiveVar"></my-input></my-isolated-scope-directive></div>',
Plunker
HTML:
<!-- this binds to the model which i would like all my inputs to bind to.-->
<input ng-model="name">
<!-- Example 1: This seems to generate a new modelvalue in the isolated-scope directive. Can I avoid this without modifying that directive?-->
<my-isolated-scope-directive><my-input ng-model="name"></my-input></my-isolated-scope-directive>
<!-- Example 2: This is what i would like my code to look like in the end: One directive which uses the code-snippet of Example 1 as template and passes some parameters into that template.-->
<my-wrapper-directive my-directive-var="name"></my-wrapper-directive>
Directives:
my-input contains a modified input-field:
app.directive('myInput', function() {
return {
restrict: 'E',
replace: true,
require: 'ngModel',
template: '<input class="some">',
link: function($scope, elem, attr, ctrl) {
console.debug($scope);
}
};
})
my-isolated-scope-directive is a placeholder-directive with its own isolated scope to simulate the behaviour for nested directives:
.directive('myIsolatedScopeDirective', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
something: '='
},
template: '<div ng-transclude></div>',
link: function($scope, elem, attr, ctrl) {
console.debug($scope);
}
};
})
my-wrapper-directive encapsulates both previous directives and accepts a parameter which should be used as ng-model value of the input field:
.directive('myWrapperDirective', function() {
return {
restrict: 'E',
transclude: false,
replace: true,
scope: {
myDirectiveVar: '='
},
template: '<div><my-isolated-scope-directive><my-input ng-model="myDirectiveVar"></my-input></my-isolated-scope-directive></div>',
link: function($scope, elem, attr, ctrl) {
console.debug($scope);
}
};
});
Any suggestions and hints on what I am missing are appreciated. Can I maybe somehow link ng-model to a service-instance without making my directive dependant on that service?
I wouldn't do it like this as it is old notation using scopes... I would use controllerAs and bindToController
script:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function() {
this.model = { name: 'World' };
this.name = "Felipe";
});
app.directive('myInput', function() {
return {
restrict: 'E',
replace: true,
// controllerAs: 'app',
require: 'ngModel',
template: '<input class="some">',
controller: function(){
}
};
})
.directive('myIsolatedScopeDirective', function() {
return {
restrict: 'E',
transclude: true,
controllerAs: 'app1',
bindToController: {
something: '='
},
template: '<div ng-transclude></div>',
controller: function(){
}
};
})
.directive('myWrapperDirective', function() {
return {
restrict: 'E',
transclude: false,
controllerAs: 'app2',
bindToController: {
myDirectiveVar: '='
},
template: '<div><my-isolated-scope-directive>'+
'<my-input ng-model="app2.myDirectiveVar"></my-input>'+
'</my-isolated-scope-directive></div>',
controller: function(){
}
};
});
index:
<!doctype html>
<html ng-app="plunker" >
<head>
<meta charset="utf-8">
<title>AngularJS Plunker</title>
<link rel="stylesheet" href="style.css">
<script>document.write("<base href=\"" + document.location + "\" />");</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl as main">
This scope value
<input ng-model="main.model.name">
<my-isolated-scope-directive>
<my-input ng-model="main.model.name"></my-input>
</my-isolated-scope-directive>
<my-wrapper-directive my-directive-var="main.model.name">
</my-wrapper-directive>
</body>
</html>
See the plunker:
http://plnkr.co/edit/VD0wXO1jivQc3JvfQFTh?p=preview
UPDATE
yes, good point, so if you want to use controllerAs, you need angular 1.2 as minimum, for bindToController you need angular 1.3

How can i change array in directive, and then reflect that change in my controller?

I made directive with isolated scope with "=" method, in that directive i pass empty array, then i push data on that array.... How that change can be reflected on original array in my controller?
Here is the example:
angular.module('myModule').controller('MyController', ['$scope', function($scope) {
$scope.test = [];
}]);
angular.module('myModule').directive('mydirective', function() {
return {
scope: {
test: "=",
bread: "="
},
restrict: 'E',
link: function(scope, element, attribute) {
scope.test.push('one more')
},
replace: true,
templateUrl: 'some template'
};
});
HTML
<div ng-controller='MyController'>
<mydirective test='test'></mydirective>
<div ng-bind='test'> </div>
</div>
When i push something on array i dont have a reflection of that in my controller.
How can i fix that?
Here's how to do what you are trying to achieve.
HTML
<!-- myCtrl contains the original array, which we push here to the scope as 'ctrl' -->
<div ng-controller='myCtrl as ctrl'>
<!-- we pass your directive 'ctrl.test', which tells angular to two-way bind to the
test property on the 'ctrl' object on the current scope -->
<mydirective test='ctrl.test'>
<!-- we're inside the isolate scope: test here refers to mydirective's idea of test,
which is the two-way bound array -->
<div ng-bind='test'></div>
</mydirective>
</div>
JS
angular.module('app', [])
.directive('mydirective', function() {
scope: {
test: '='
},
link: function($scope) {
$scope.test.push('one more');
}
})
.controller('myCtrl', function() {
this.test = [];
});
Any alterations to the array will now be reflected in the ng-bind. Please note that it is bad practice to place primitives on $scope without being part of an object (due to the mechanics of prototypical inheritance) so you'd want to change $scope.test to something else.

Angular Directive needs access to variable in ng-repeat

<div ng-repeat="campaign in campaigns">
<div progressbar="value"></div>
</div>
Each campaign has it's own unique property called "percent" that I want the directive to be able to access.
myApp.directive('progressbar', function(){
return {
restrict: 'A',
scope: {
'progress': '=progressbar'
},
controller: function($scope, $element, $attrs) {
$element.progressbar({
value: *** I want this to refer to the correct campaign.percent ***
})
}
}
})
I have a feeling I am severely overthinking this one. Any help would be greatly appreciated. Thanks!
You should change HTML in order to pass current campaign object to directive:
<div ng-repeat="campaign in campaigns">
<div progressbar="campaign"></div>
</div>
and then in directive you will be able to access campaign object as $scope.progress:
controller: function($scope, $element, $attrs) {
$element.progressbar({
value: $scope.progress.percent
});
}
Of course you can pass in percent value directly as <div progressbar="campaign.percent"></div> but I think that it makes sense to provide complete campaign object in case you will need some other properties other then percent.

ng-repeat inside custom directive

So I'm struggling with passing the scope to my custom directives.
Here's my html
<li ng-repeat="post in posts | filter:search | orderBy:sortField:reverse" class="archives-listing" ng-class="{'last-border':$last}">
<archive-notes></archive-notes>
</li>
Here's my directives
app.directive('archiveNotes', function(){
return {
restrict: 'E',
transclude: true,
scope: {
notes: '#',
paths: '#'
},
controller: function($scope) {
},
templateUrl: '/wp-content/themes/twentythirteen/js/angular/templates/notes.html'
}
})
app.directive('archiveFolders', function(){
return {
require: '^archiveNotes',
restrict: 'E',
transclude: true,
scope: {
path: '#'
},
link: function(scope, element, attrs) {
},
templateUrl: '/wp-content/themes/twentythirteen/js/angular/templates/folders.html'
}
});
here are my templates.
notes.html
<div ng-class="{'found' : find(post.paths[$index], search)}" class="arch-notes">
<div ng-bind-html="is_NotesEmpty(post.notes)">{{post.notes}}</div>
<archive-folders></archive-folders>
</div>
folders.html
<div ng-repeat="path in post.paths | filter:search track by $index" ng-transclude>
<span class="arch-paths">{{path}}</span>
</div>
I left several things blank i.e controller and link because at this point i was just trying to figure how to make everything show up first before i start manipulating the DOM
I followed the example in the angularjs documentation, and it got me this far. I guess cant seem to figure out how to access the scope?
Any help is appreciated.
Based on your templates, it seems that your archiveNotes directive definition should actually look like this:
app.directive('archiveNotes', function(){
return {
...
scope: {
post: '='
},
...
}
})
To get the post variable passed in from ng-repeat's scope, you also need to set the post attribute on the directive's element:
<archive-notes post="post"></archive-notes>
Similarly, you need to set it on the child directive:
app.directive('archiveFolders', function(){
return {
...
scope: {
post: '='
},
...
}
});
... and change your notes.html template:
<archive-folders post="post"></archive-folders>
Isolate scope is sort of like a firewall, where you can set exceptions, in this case on specific scope variables. All we're doing here is setting those exceptions in the directive definitions, then passing them through using the attribute on the elements.
These videos by John Lindquist really shed some light on isolate scope for me:
Understanding Isolate Scope
Isolate Scope '#'
Isolate Scope '='
Isolate Scope '&'

Categories