Understand Bindings in Components - angularjs

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.

Related

AngularJS 1.5 - How to Set Two Way Bindings on Component

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.

Angular 1.5 component attribute presence

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

Directive with transclude, data binding not working in templateUrl

I have a directive with transclude:true. However, the data binding works when i use a template:"" but not when i use templateUrl:""
Below you can find my directive. The rsCarousel.html template contains the same code as the template"" property.
When using the template property i get the vm.carouselId on screen but not when using the templateUrl property.
Why is this?
Thx,
(function () {
'use strict';
angular.module('skynetDashboard').directive('rsCarouseli', carouseli);
function carouseli(){
var directive = {
restrict:"EA",
scope:{
carouselData:"=",
carouselId:"#",
carouselOptions:"#"
},
transclude:true,
templateUrl:"js/directive/rsCarousel.html",
//template:"<strong>ID: {{vm.carouselId}}</strong><ul ng-transclude></ul>",
bindToController:true,
controllerAs:"vm",
link:link,
controller:controller
}
return directive
function link(scope){
console.log(scope.vm)
}
controller.$inject = [""]
function controller(){
}
}
})();
That is strange, scope binding should work in both cases.
Be sure that your template file is not cached by the browser (and using an old version of it).

Angular.js: Data binding not working with directives controller option

Why is angular's data binding not working when I specify a controller in the directives controller option? $scope.emailInvalid.text normally should get mapped to type.text but in my case, nothing get's displayed.
JS:
.directive('alert', function () {
return {
template: '<div>{{type.text}}</div>',
restrict: 'E',
scope: {
type: '='
},
controller: function ($scope) {
$scope.emailInvalid = {
text: 'Text Alert Two'
};
}
};
});
HTML:
<alert type="emailInvalid"></alert>
When I define a separate controller and pass it with ng-controller in the HTML, everything works like expected.
Here is a plunker.
Since you want to display type.text you need to define
$scope.type = {
text: 'Text Alert Two'
};
in your directive controller. By doing so, you don't event have to pass the object to the directive.
PLUNKR
OK, I found a solution:
The problem was that angular fails at mapping $scope.emailInvalid to $scope.type. What I have done in my example with <alert type="emailInvalid"></alert>, is passing an object emailInvalid to the directive. Angular is looking for this object in the scopes model of where I used the directive. Obviously this object doesn't exist and angular can't map anything.
The part I was missing is, that the controller I defined with the directives controller option is defined in a different scope than the controller I used with ng-controller.
To work around this problem I'm now passing a string instead of an object and use switch/case to map the alert type.
plunker

AngularJS: Multiple ways to pass function from controller to 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.

Resources