Create Isolated Scope using Directive - javascript

I have a set of clients, displayed on a form, which must have individual scopes (per client):
View plunker here.
Naturally, I would expect that creating a new directive, with isolate scope, would not allow elements to be bound to oustide $scope, using a custom directive like this:
<fieldset client="156510">
<legend>Client 156510</legend>
<!-- Form elements -->
</section>
</fieldset>
And likewise:
angular.module("plunker", [])
.controller("ClientCtrl", function($scope) {})
.directive("client", function() {
return {
restrict: "A",
scope: {
name: "#name",
client: "=client"
}
};
});
Given that ng-repeat is not an option, how can I isolate scope of any contained HTML using a directive? The angular docs seem to suggest this is possible, but my implementation does not seem to work as intended.
Thanks in advance.

Any directive on an element with isolated scope WITHOUT template/templateUrl actually does NOT get a new scope.
Here is the proof
http://plnkr.co/edit/jXwrtG?p=preview
.directive("client", function() {
return {
restrict: "A",
template: ' ',//notice extra spaces
replace: true,//notice this
scope: {
name: "#name",
client: "=client"
}
};
});
Also scope=true will solve your problem.
http://plnkr.co/edit/JdiCVV?p=preview
.directive("client", function() {
return {
restrict: "A",
scope: true
};
});
Also as pointed by #imscrb
transclude = true also works but you must add ng-transclude to the element
<fieldset client="156510" ng-transclude>
http://plnkr.co/edit/b0hX5h?p=preview
.directive("client", function() {
return {
restrict: "A",
transclude: true,
scope: {
name: "#name",
client: "=client"
}
};
});

If you want each client to be totally isolate then you need to put all of the HTML being used within the template of the directive. this is because the current html you have, and the model's your binding are controlled by the controller and not the directive
I.E... (haven't tested with your code, but its what you need to do) - it's likely you need to change the ng-model in your template to the model you will push into the directive though - this is just as an example
return{
....
template: '<legend>Client 156510</legend>'
+ '<section>'
+ '<div class="horizontal-field">'
...........
+ '</section>'
}
then your html would simply be something like (again, psuedo-code):
<fieldset client="156510" ng-model="yourModel"></fieldset>

If i understand your question correctly then enabling transclusion should solve your problem:
angular.module("plunker", [])
.controller("ClientCtrl", function($scope) {})
.directive("client", function() {
return {
restrict: "A",
scope: {
name: "#name",
client: "=client"
},
transclude: true,
template: '<div ng-transclude></div>'
};
});

I believe that you do things in wrong way since uou broke the DRY principle. Instead of repeating HTML for each client in view, move it into directive's template and pass into directive isolated scope client themselve. Of course, you can pass into directive only client's id and get client from some service just in directive if you want.
And don't specify all Angular service classes manually like ng-pristine, ng-dirty etc.
Controller and directive
app = angular.module("plunker", []).controller("ClientCtrl", function($scope) {
$scope.clients = [{
id: 12345,
firstName: 'First',
lastName: 'First',
middleInitial: 'A'
}, {
id: 123456,
firstName: 'Second',
lastName: 'Second',
middleInitial: 'B'
}];
})
.directive("client", function() {
return {
restrict: "A",
templateUrl: 'client-form.html',
scope: {
client: "=client"
}
};
});
Client form template
<fieldset ng-form='clientForm'>
<legend>Client {{client.id}}</legend>
<section>
<div class="horizontal-field">
<label for="title">Title</label>
<select ng-model="client.title" name="title">
<option value=""></option>
<option value="mr">Mr.</option>
<option value="mrs">Mrs.</option>
<option value="ms">Ms.</option>
<option value="miss">Miss</option>
<option value="dr">Dr.</option>
</select>
</div>
<div class="horizontal-field">
<label for="first-name">First Name</label>
<input placeholder="First Name" required="" autofocus="" ng-model="client.firstName" name='first_name' type="text" >
</div>
<div class="horizontal-field">
<label for="middle-initial">Middle Initial</label>
<input maxlength="1" ng-model="client.middleInitial" type="text" name="middle_initial" value="" id="middle-initial">
</div>
<div class="horizontal-field">
<label for="last-name">Last Name</label>
<input placeholder="First Name" ng-model="client.lastName" type="text" name="last_name">
</div>
</section>
</fieldset>
View
<div ng-repeat='client in clients' client='client' ></div>
Plunker

Related

AngularJS Bound Ng-Model Inside Directive Has Wrong Attribute Name

This is a followup to a previous question. I have some questions on the rendering of a custom directive and the actual output in the DOM when the page is rendered.
Here is the definition of my directive:
angular.module('moduleName)
.directive('selectValue', ['$timeout', function($timeout) {
const directive = {
restrict: 'E',
replace: true,
scope: {
controlId: '#',
model: '=?'
},
controller: 'selectValueCtrl',
templateUrl: 'template.html'
};
return directive;
}
Here is the externalized template:
<!-- template.html -->
<input id="{{controlId}}" name="{{controlId}}" placeholder="Enter Value"
type="text" ng-model="model" />
Given the following use of the directive:
<select-value controlId="selectValue" model="data.value"></selectValue>
Why does it render as the following:
<input id="selectValue" ng-model="model" />
Instead of:
<input id="selectValue" ng-model="data.value" />
Did I make a mistake in my code, or is this expected behavior?
{{controlId}} - this is an interpolation and Angular should calculate the expression behind the braces. That's why you get id="selectValue" instead of id="{{controlId}}"
ng-model="model" it's just a two-way binding which is handled by Angular under the hood, but Angular doesn't change the template in this case. Angular should know what model is binded, values are transferred behind the scenes.

Pass directives through another directive

We created some directives to speed up our coding and clean the htmls, for example:
<div class="form-group">
<label class="form-label">{{label}}
<input ng-disabled="ngDisabled" ng-model="ngModel" type="text" class="form-control"/>
</label>
<div ng-if="invalidMessage" class="error">{{invalidMessage}}</div>
</div>
and the js
angular.module('ui.default-input', [])
.directive('defaultInput', function(){
return {
restrict: 'E',
templateUrl: '_component.html',
scope: {
label: '#',
invalidMessage: '#',
ngModel: '=',
ngDisabled: '='
}
}
});
So we can use it anywhere like
<default-input label="im a label" ng-model="vm.model"></default-input>
The problem is, whenever we want to add another directive to the input, we have to defined it in the directive own scope and manually apply it. (Like we did with ngDisabled and ngModel).
Is there a smarter solution? Is it wrong to use directives in that way?
Thanks in advance.

Angular: custom directive not updating controller

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

How to pass JSON data to AngularJS directive

I am learning AngularJS. I try to create a reusable component called .
Unfortunately I cannot prefill the fields inside element with the data obtained from JSON.
I looked around SO and the web but could not solve it. Could you please let me know what am I doing wrong?
I have two controllers. One gets a list of all countries:
app.controller('MainController', ['$scope', 'Countries',
function ($scope, Countries) {
$scope.countries = Countries.query();
}]);
The other gathers a specific address:
app.controller('AddressesController', ['$scope', '$routeParams', 'Address',
function($scope, $routeParams, Address) {
if ($routeParams.addressId) {
$scope.senderAddress = Address.get({addressId: $routeParams.addressId});
} else {
$scope.senderAddress = {"id":null, "country":null, "city":null, "street":null};
}
$scope.adData = {"id": 1, "country": "Poland", "city": "Warsaw", "street": "Nullowska 15"};
}]);
The services are defined as follows, they seem to work correctly and provide correct JSONs:
myServices.factory('Countries', ['$resource',
function($resource) {
return $resource('data/countries.json', {}, {
query: {method:'GET'}
})
}]);
myServices.factory('Address', ['$resource',
function($resource) {
return $resource('data/:addressId.json', {}, {
query: {method:'GET', params:{addressId:'addressId'}}
})
}])
I have routing set so that it directs to AddressesController:
app.config(function ($routeProvider) {
$routeProvider
.when('/address', {
templateUrl: 'partials/addresses.html',
controller: 'AddressesController'
})
.when('/address/:addressId', {
templateUrl: 'partials/addresses2.html',
controller: 'AddressesController'
})
});
The partial view is simple, I create 2 elements
<label> Sender </label>
<address address-data='{{senderAddress}}'></address> <!-- I tried all combinations of passing this as argument -->
<label> Receiver </label>
<address></address>
Now the directive is declared as:
app.directive("address", function () {
return {
restrict: "E",
templateUrl: "/directives/address.html",
scope: {addrData: '#senderAddress'},
link: function(scope, element, attributes) {
scope.adData = attributes["addressData"];
}
}
});
and template for it is:
<div>
<label> {{senderAddress}} </label> <!-- senderAddress from Addresses Controller is filled correctly -->
<div>
<label>Country</label>
<select>
<option value=""></option>
<option ng-repeat="country in countries.countries" value="{{country}}">{{country}}</option>
</select>
</div>
<div>
<label>City {{dto.adData.city}}</label>
<input type="text" data-ng-model="dto.adData.city" /> <!-- this I cannot pre-fill -->
</div>
<div>
<label>Street{{data.adData.city}}</label>
<input type="text" data-ng-model="dto.adData.street"> <!-- this I cannot pre-fill -->
</div>
</div>
It all works well outside of directive. But I miss something regarding how to handle the scope inside a directive with data being obtained from JSON service. Is it because JSON data is a promise object when the links to the directive are created? How to handle it?
PS
I also tried observing the attributes:
link: function(scope, element, attributes) {
//scope.dto.adData = attributes["addressData"];
attrs.$observe('addressData', function(data) {
if (!data)
return;
scope.dto.adData = data;
})
}
Even for statically defined data it doesn't work:
app.directive("address", function () {
return {
controller: function($scope) {
$scope.dto = {};
$scope.dto.data = {"id": 1, "country": "Poland", "city": "Warsaw", "street": "Nullowska 15"};
},
Passing in the JSON like this isn't how I'd do it as it's kind of hacking in the data binding and you probably don't get two-way binding. I'd use an isolate scope.
Your directive would be used without handlebars, to link up the scope variable:
<address address-data='senderAddress'></address>
And then you'd include a scope option in the directive definition:
app.directive("address", function () {
return {
restrict: "E",
templateUrl: "/directives/address.html",
scope: {
addressData: '='
}
}
});
The bare equals-sign '=' tells Angular to double-bind the parent scope variable referenced in the address-data attribute to the child scope variable addressData. This is done automatically by normalizing the name "address-data" into the JS-style "addressData." If you wanted to name the two scope variables differently, you could do innerAddressData: '=addressData' instead.
If you do it like this, you don't need a linking function at all and the binding still should work.
OK, I solved it, in case anyone has similar issues, it may help to check if the scope is set to true and to check if JSON is parsed from string ;-).
app.directive("address", function () {
return {
restrict: "E",
templateUrl: "/directives/address.html",
scope: true, // creates its own local scope
link: function(scope, element, attributes) {
attributes.$observe('addressData', function(data) {
if (!data)
return;
scope.dto = {};
// works almost fine but in 2nd case data is also filled
scope.dto.adData = angular.fromJson(data);
})
}
}
});

Testing angularjs directive (template not updated in tests after changing an isolated scope var)

I'm creating an angularjs component that (will) provides a checkbox list directive with filtering, sorting, toggling options, scrolling, etc... once finished. It should help people to deal with long checkbox lists.
I'm trying to test the order by label or id feature but the template does not reflect the model changes even after $digest or $apply call. I tried to solve but no way.
Here it is the directive definition:
angular.module('angularjsSmartcheckboxApp')
.directive('smartCheckbox', [function () {
return {
templateUrl: 'views/smartcheckbox.html',
restrict: 'E',
replace: true,
scope: {model: '='},
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
// TODO
}]
};
}]);
Usage:
<smart-checkbox model="smartList" />
where smartList is:
$scope.smartList = [
{id: '001', label: 'First item'},
{id: '002', label: 'Second item'},
{id: 'Z01', label: 'Another item'}
];
Here is the template:
...
<div class="input-group ordercontrols">
Order options:
<label class="radio-inline">
<input type="radio" value="label" ng-model="orderby"> Label
</label>
<label class="radio-inline">
<input type="radio" value="id" ng-model="orderby"> Id
</label>
<label>
<input type="checkbox" ng-model="reverse" />
Reverse
</label>
</div>
<div>
<div class="checkbox widgetcontrols" ng-repeat="elem in model | filter:{$:filter} | orderBy: orderby:reverse">
<label>
<input class="smart" type="checkbox" ng-model="elem.value" value="{{elem.value || false}}" />
<span class="itemid">[{{elem.id}}]</span> {{elem.label}}
</label>
</div>
</div>
Here you can find the failing test 'Labels order (reverse)':
describe('Directive: smartCheckbox', function () {
// load the directive's module
beforeEach(module('angularjsSmartcheckboxApp', 'views/smartcheckbox.html'));
var element,
$rootScope,
$compile,
scope;
beforeEach(inject(function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$compile = _$compile_;
$rootScope.model = [
{id: '001', label:'First item'},
{id: '002', label:'Second item'}
];
element = $compile(angular.element('<smart-checkbox model="model"></smart-checkbox>'))(scope);
$rootScope.$apply();
}));
it('Labels order (reverse)', function () {
scope.reverse = true;
scope.orderby = 'label';
scope.$apply();
expect(element.children().eq(3).find('label').eq(0).find('span').text()).toBe('[002]');
});
});
Repository link:
https://github.com/davidemoro/angularjs-smartcheckbox
PS: if you open the browser the reordering works fine, so I suppose there is something of wrong in the test method.
What is the problem?
Thank you in advance
How I solved it:
- element = $compile(angular.element('<smart-checkbox model="model"></smart-checkbox>'))(scope);
+ $element = angular.element('<smart-checkbox model="model"></smart-checkbox>');
+ element = $compile($element)(scope);
...
it('Labels order (reverse)', function () {
- scope.reverse = true;
- scope.orderby = 'label';
- scope.$apply();
+ $element.isolateScope().reverse = true;
+ $element.isolateScope().orderby = 'label';
+ $element.isolateScope().$apply();
- expect(element.children().eq(3).find('label').eq(0).find('span').text()).toBe('[002]');
+ expect($element.children().eq(3).find('label').eq(0).find('span').text()).toBe('[002]');
});
Since the directive uses an isolated scope, I need to use the .isolateScope method.
Hope it might help other people :)
UPDATE: I've writed a blog post based on this answer. See http://davidemoro.blogspot.com/2014/02/testing-angularjs-directives-with_13.html

Categories