Directives in Angular 1.X are set to have two way binding by default. Components have isolated scopes by default. I have a view that looks like:
<div class="my-view">
{{controllerVariable}}
</div>
If I have the above set up as a directive, the controllerVariable loads correctly in the following situation:
<div ng-controller="myController">
<my-view></my-view>
</div>
But if I have it set up as a component using the following:
myApp.component('myView', {
templateUrl: '/path/to/view',
bindings: '='
});
then the variable value isn't displayed. I have tried adding $ctrl to the variable:
<div class="my-view">
{{$ctrl.controllerVariable}}
</div>
but this doesn't display the value either.
What am I missing here?
You need to pass the value from the directive into the component:
<my-view passed-var='ctrl.passedVar'></my-view>
and in the component:
myApp.component('myView', {
templateUrl: '/path/to/view',
bindings: {
passedVar: '='
},
controller: function () {
var vm = this;
console.log(vm.passedVar);
}
});
then you will be able to access in the component as in the example
There are a few other ways to do it, such as using a service to handle the information or using require which would give your component access to the controller of the directive. You can find the above method and others here: https://docs.angularjs.org/guide/component.
I had to explicitly state the variable I wanted to bind:
myApp.component('myView', {
templateUrl: '/path/to/view',
bindings: {
controllerVariable: '#'
}
});
Also, since controllerVariable is a string, I had to use the # sign binding.
Related
angular.module('RatingModule').component('starRating', {
templateUrl: 'rating.html',
controller: RatingCtrl,
bindings: {
maxStars: '=',
ngModel : '='
//above is the component where i want to use ngModel inside bindings of coponents
function RatingCtrl() { i want to use ngModel here }
// below is the index.html where ng-model is binding a value from the app controller
<body ng-app="RatingModule" ng-controller="appCtrl as ctrl">
<star-rating ng-model="ctrl.rating.number" max-stars="5"></star-rating>
the angular ngModel directive binds an input,select, textarea (or
custom form control) to a property on the scope using
NgModelController, which is created and exposed by this directive.
This sould not work, plus you should not use a angular name for a custom property.
you can do
angular.module('RatingModule').component('starRating', {
templateUrl: 'rating.html',
controller: RatingCtrl,
bindings: {
maxStars: '=',
myModel : '='
then
function RatingCtrl() { $scope.myModel }
and
<star-rating my-model="ctrl.rating.number" max-stars="5"></star-rating>
Well, actually it's possible to use ngModel, but it's 100% worthless.
You can set ng-model on component, then require: 'ngModel' and manually set ngModelCtrl.$setViewValue on 'click' for example. But as I said before, it's ill wrong approach.
Here is a Plunker, basically, you can set model as you want
element.on('click', function(){
ngModelCtrl.$setViewValue(scope.index);
});
$setViewValue(value, trigger); Update the view value.
This method should be called when a control wants to change the view
value; typically, this is done from within a DOM event handler. For
example, the input directive calls it when the value of the input
changes and select calls it when an option is selected.
DMCISSOKHO raised a good point, you couldn't use ng-model here. Try this instead:
angular.module('RatingModule').component('starRating', {
templateUrl: 'rating.html',
controller: RatingCtrl,
bindings: {
maxStars: '=',
ratingNumber : '='
and in your view use
<star-rating rating-number="ctrl.rating.number" max-stars="5"></star-rating>
I'm refactoring some angular directives to angular 1.5-style components.
Some of my directives have behavior that depends on a certain attribute being present, so without the attribute having a specific boolean value. With my directives, I accomplish this using the link function:
link: function(scope,elem,attrs, controller){
controller.sortable = attrs.hasOwnProperty('sortable');
},
How would I do this with the angular 1.5-style component syntax?
One thing I could do is add a binding, but then I'd need to specify the boolean value. I'd like to keep my templates as-is.
Use bindings instead of the direct reference to the DOM attribute:
angular.module('example').component('exampleComponent', {
bindings: {
sortable: '<'
},
controller: function() {
var vm = this;
var isSortable = vm.sortable;
},
templateUrl: 'your-template.html'
});
Template:
<example-component sortable="true"></example-component>
Using a one-way-binding (indicated by the '<') the value of the variable 'sortable' on the controller instance (named vm for view model here) will be a boolean true if set as shown in the example. If your sortable attribute currently contains a string in your template an '#' binding may be a suitable choice as well. The value of vm.sortable would be a string (or undefined if the attribute is not defined on the component markup) in that case as well.
Checking for the mere presence of the sortable attribute works like this:
bindings: { sortable: '#' }
// within the controller:
var isSortable = vm.sortable !== undefined;
Using bindings may work but not if you are trying to check for the existence of an attribute without value. If you don't care about the value you can just check for it's existence injecting the $element on the controller.
angular
.module('yourModule')
.component('yourComponent', {
templateUrl: 'your-component.component.html',
controller: yourComponentsController
});
function yourComponentController($element) {
var sortable = $element[0].hasAttribute("sortable");
}
There is a built-in way to do this by injecting $attrs into the controller.
JS
function MyComponentController($attrs) {
this.$onInit = function $onInit() {
this.sortable = !!$attrs.$attr.hasOwnProperty("sortable");
}
}
angular
.module("myApp", [])
.component("myComponent", {
controller: [
"$attrs",
MyComponentController
],
template: "Sortable is {{ ::$ctrl.sortable }}"
});
HTML
<my-component sortable>
</my-component>
<my-component>
</my-component>
Example
JSFiddle
I don't get the binding in angular components. I have reworked this material FAB demo to a component. So there is no ng-controller directive anymore. However I cannot make the bindings of bindings: {isOpen: '='} to work. I get the following error:
Expression 'undefined' in attribute 'isOpen' used with directive 'tsButton' is non-assignable!
The code looks like this:
<div ng-cloak>
<md-fab-speed-dial
md-open="$ctrl.isOpen"
ng-mouseenter="$ctrl.isOpen=true"
ng-mouseleave="$ctrl.isOpen=false">
<!-- buttons and trigger -->
</md-fab-speed-dial>
(function () {
'use strict';
angular
.module('trip')
.component('tsButton', {
templateUrl: "app/component/button.component.html",
controller: ButtonController,
});
function ButtonController() {
var vm = this;
vm.isOpen = false;
};
}
})();
If I omit the bindings: {isOpen: '='} then md-open="$ctrl.isOpen" is not propagated.
A workaround is to define methods for ng-mouseenter="$ctrl.open()" and ng-mouseleave="$ctrl.close()" that in controller assign the correct boolean to vm.isOpen. But as I say it is just a workaround that makes the code longer, among other things.
isOpen: '=' was not working because I was giving it a primitive value. For this to work it had to be a reference of course.
In parts of my application I have used a directive in this format:
<child-directive ng-repeat="item in vm.items"></child-directive>
This one has access to {{item}} from within child-directive without having to do anything.
Now I want to use the same directive along side other directives that all work with the same context data:
<div ng-repeat="item in vm.items">
<child-directive></child-directive>
<other-directive></other-directive>
</div>
The child directive does not need to alter the context data, it only needs the information from inside to display a widget.
I've tried using scope in the directive in this format:
angular
.module('myapp.dashboard')
.directive('childDirective', childDirective);
function childDirective() {
var directive = {
restrict: 'E',
templateUrl: 'client/components/child-directive.ng.html',
controller: 'ChildDirectiveController',
controllerAs: 'vm',
scope: {
item: '='
}
};
return directive;
}
and
<child-directive ng-attr-item="{{item}}"></child-directive>
Within the ng-repeat section. However that just throws an error.
I've also tried ng-bind with no luck.
Any suggestions?
You're using bi-directional scope binding rather than an interpolated property. You can read up on this more in the angular docs
Change your scope object to be:
scope: {
item: '#'
}
OR, change your template to:
<child-directive ng-attr-item="item"></child-directive>
I am trying to write component-style AngularJS, similar to the practice put forward by this article.
However, I have come to realize there are various ways to pass functions to directives from an associated controller. The directive I'm working on is quite complex and I was passing each function in by binding to the directive in the template, but I now see I could just implicitly inherit the $scope object or reference the Controller object directly.
Here is an example of what I mean:
app.js
var app = angular.module('plunker', [])
app
.controller('myCtrl', function($scope) {
$scope.output = '';
// fn foo is passed into directive as an argument
$scope.foo = function () {
$scope.output = 'foo';
}
// fn inherited from controller
$scope.bar = function () {
$scope.output = 'bar';
}
// fn attached to ctrl object and referenced directly
this.baz = function () {
$scope.output = 'baz';
}
})
.directive('myDirective', function() {
return {
scope: {
output: '=',
foo: '&',
},
templateUrl: 'template.html',
replace: true,
controller: 'myCtrl',
controllerAs: 'ctrl'
};
})
index.html
<body ng-controller="myCtrl">
<my-directive
output="output"
foo="foo()">
</my-directive>
</body>
template.html
<div>
<button ng-click="foo()">Click Foo</button>
<button ng-click="bar()">Click Bar</button>
<button ng-click="ctrl.baz()">Click Baz</button>
<p>You clicked: <span style="color:red">{{output}}</span></p>
</div>
Plunkr: http://plnkr.co/edit/1JzakaxL3D2L6wpPXz3v?p=preview
So there are three functions here and they all work, yet are passed to the directive in different ways. My question is what are the merits of each and which is the best from a code and testability perspective?
You're not really passing anything to the directive, as it's using the same controller as the file containing it...
For instance, if you delete the following:
scope: {
output: '=',
foo: '&',
}
from your directive, everything still works the same. I can't think of a reason to use the same controller for a directive and and the containing application like this. I would never recommend this approach.
If you also remove
controller: 'myCtrl',
controllerAs: 'ctrl'
only foo and bar work. This is because the directive inherits the scope it's contained in. This is only recommended if your directive is pretty simple and tightly coupled to the view using it. Usually this approach is OK when you're just doing some visual modifications that repeat themselves in the page. Just notice that when you change something in the controller, the directive will probably break, and that goes against the encapsulation principle.
Finally, the correct way to pass a function to a directive is indeed using '&' modifier. This lets your directive keep an isolated scope, which means it won't break if some code on the containing controller changes. This makes your directive truly an encapsulated, independent module that you can "drag and drop" anywhere.
Here's a fork of your plunkr.