Angular: Evaluate Expression Passed into Component Attribute - angularjs

How can I pass in the value of an Angular expression to a component's attribute? I'm getting the value back from an API.
app.controller.js
$http.get(apiUrl).then(function(resp) {
$scope.name = resp.data.name;
})
...
person.component.js
export const Person = {
templateUrl: 'person.html',
bindings: {
firstName: '#'
},
controller: function() {
console.log(this.firstName);
}
}
app.html
...
<person first-name="name">
For some reason it's not evaluating name and it's logging undefined in the console.
Is there a way around this so it logs Tom inside the controller?
Any help is appreciated. Thanks in advance!
I've setup a jsFiddle here

& is for expressions, and # is for interpolated strings, so try using
firstName: '&'
and then this.firstName() should evaluate the expression passed in.
Also, firstName is not guaranteed to have been initialized until $onInit, so if you do
bindings: {
firstName: '&'
},
controller: function() {
this.$onInit = function() {
console.log(this.firstName());
}
}
you should get your expected result.
For reference: https://docs.angularjs.org/guide/component
$onInit() - Called on each controller after all the controllers on an element have been constructed and had their bindings initialized
Edit:
After the extra information you provided, you should probably use a one-way binding (<) instead for this case, because it appears you are just passing in a single value (instead of an expression), and then you can detect changes in $onChanges. I forked your jsfiddle to show a potential solution: http://jsfiddle.net/35xzeo94/.

Related

What is wrong with angular component "bindings" parameter?

I have an angular component that I want to include in my page.
everything works if I comment/remove bindings parameter in this component... Why?
(function () {
angular.module('app').component('detailsComponent1', {
templateUrl: '/RealSuiteApps/RealForm/-1/Details/Component1',
restrict: 'E',
bindings: {
value: "Component1"
},
controllerAs: 'cm',
controller: function () {
this.message = 'Hello from component1';
}
});
})();
If I want to use binding parameter, I get an error:
that links to: https://docs.angularjs.org/error/$compile/iscp?p0=detailsComponent1&p1=value&p2=Component1&p3=controller%20bindings%20definition
What does it mean ??
Thanks.
Well, The error is coming because of Invalid Isolate scope
When declaring isolate scope the scope definition object must be in specific format which starts with mode character (#&=<), after which comes an optional ?, and it ends with an optional local name.
Please find the reference link here: https://docs.angularjs.org/error/$compile/iscp
Plus, you have some discarded directive property inside component.
restrict (restricted to elements for component)
Please find documentation for component here: https://docs.angularjs.org/guide/component
Hope it helps you!
Cheers

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

Optional parameter on Angular Directive

I created a directive on Angular that receives 5 parameters and one of them is an optional array. The way I'm trying to deal with it is like follows:
app.directive('fooDirective', function() {
return {
restrict: 'AE',
scope: {
param1: '=',
param2: '=' // optional array
},
template: //...
"<div ng-class='defineClass()'> Message </div>"
//...
controller: function($scope) {
if (typeof $scope.param2 === 'undefined')
$scope.param2 = [];
console.log($scope.param2);
$scope.defineClass = function() {
if ($scope.param2.length == 0) return 'gray-text';
return 'red-text';
};
// ......
}
}
});
At some point in my code I check for the .length of param2 and if it is undefined it throws a lot of errors. What is driving me nuts is that the console.log() you can see there outputs a [], indicating that the .length of my param should be 0 and the errors on the console are shown after the output of the console.log().
So I guess I am missing something about either the way Angular binds the scopes or the flow that the directive is constructed. I have tried verifing my param on both link and compile phases and got the same problem.
So, what am I missing here? Thanks in advance.
From the angular documentation (see the section for scope bi-directional binding):
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value of the attr attribute....
If the parent scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You can avoid this behavior using =? or =?attr in order to flag the property as optional.
So the solution to make the parameter optional is to change the binding to be an optional two-way binding with the additional ?.
...
scope: {
param1: '=',
param2: '=?' // notice the ? makes this parameter optional.
},
...

Difference between & and = for passing functions to isolate scope

& is always described as the way to call a function on the parent scope inside a directive's isolated scope.
However, since = creates two-way binding and a function is just another value, shouldn't it be just as effective for this purpose?
The only difference I see is that using &, you can modify the passed function without affecting the parent, since it's one-way binding.
So why is & usually recommended over = for this use case?
There is also some weird behavior that I've come across. Using & gives you a function wrapper. If you unwrap it in the controller and call it, it will resolve differently than if you unwrap it as the result of an ng-click inside the directive.
I've set up an experiment in this fiddle:
app.directive('myDir', function() {
return {
restrict: 'E',
template: '<button ng-click="parentFunc1()(1); parentFunc2(1)">Directive</button>',
scope: {
parentFunc1: '&func1',
parentFunc2: '=func2',
},
controller: Ctrl2,
}
});
function Ctrl2($scope) {
//Step 1
console.log($scope.parentFunc1);
$scope.parentFunc1()(1);
$scope.parentFunc2(1);
//Step 2
$scope.oldParent1 = $scope.parentFunc1;
$scope.parentFunc1 = function (value) {
console.log(value+1);
};
$scope.parentFunc1(1);
$scope.parentFunc2(1);
//Step 3
$scope.parentFunc1 = $scope.oldParent1;
$scope.parentFunc2 = function (value) {
console.log(value+2);
};
console.log($scope.parentFunc1);
$scope.parentFunc1()(1);
$scope.parentFunc2(1);
//Step 4 -> Click the directive button
}
function Ctrl($scope){
$scope.foo = function (value) {
console.log(value);
};
}
This logs "1,1; 2,1; 1,2; 2,2". The last two pairs of values leave me puzzled because they seem to execute the same code.
Very good question!
See the difference between & and = is simple.
When you are declaring a directive scope, and you add to it & it means that you are declaring a function within the scope rather if it was = it was for a regular property.
WAIT WAIT, those two examples above just worked and they are both functions!
Well that's true but hold on,
You just used them incorrectly.
Using the :"&func" means that you are adding a function that will be evaluated soon.
Confused?
I'll type a perfect example:
<script type="text/javascript">
angular.module("exampleApp", [])
.directive("scopeDemo", function (){
return {
template: "<div><p>Name: {{local}}, City: {{cityFn()}}</p></div>",
scope:{
local: "=nameprop",
cityFn: "&city"
}
}
}
}).controller("scopeCtrl, function($scope){
$scope.data = {
name: "Shahar",
defaultCity: "London"
};
$scope.getCity = function(name){
return name == 'Shahar' ? $scope.data.defaultCity : "unknown";
}
});
</script>
<body ng-controller="scopeCtrl">
<div>
Direct Binding: <input ng-model="data.name" />
</div>
<div scope-demo city="getCity(data.name)" nameprop="data.name"></div> //Reference 1.
</body>
As you can see I've written two attributes to my scope's directive.
one accepts a PROPERTY and one accepts a FUNCTION.
As you can see the result of this directive is rather dull, but it explains the whole point.
You will not succeed doing so if you try to make a function with the '=' since Angular will just ignore that.
I hope it clears it up.
Good luck!
The difference between & and = binding strategies takes place when you want to call function on parent scope with parameters also passed from parent scope.
Let's say you have following controller:
angular.module('myApp').controller('myCtrl', function() {
$scope.mans = [{name: 'Peter'}, {name: 'Alex'}]
$scope.someMethod = function(par) {
console.log(par);
}
});
And HTML:
<div ng-repeat="man in mans">
<button my-dir="someMethod(man.name)">Click me</button>
</div>
In this case myDir directive should only use & binding strategy, because the directive knows nothing aboout passed parameters.

Call method on Directive to pass data to Controller

So basically I have a controller, which lists a bunch of items.
Each item is rendering a directive.
Each directive has the ability to make a selection.
What I want to achieve is once the selection has been made, I want to call a method on the controller to pass in the selection.
What I have so far is along the lines of...
app.directive('searchFilterLookup', ['SearchFilterService', function (SearchFilterService) {
return {
restrict: 'A',
templateUrl: '/Areas/Library/Content/js/views/search-filter-lookup.html',
replace: true,
scope: {
model: '=',
setCriteria: '&'
},
controller: function($scope) {
$scope.showOptions = false;
$scope.selection = [];
$scope.options = [];
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
};
}]);
The directive is used like this:
<div search-filter-lookup model="customField" criteria="updateCriteria(criteria)"></div>
Then the controller has a function defined:
$scope.updateCriteria = function(criteria) {
console.log("Weeeee");
console.log(criteria);
};
The function gets called fine. But I'm unable to pass data to it :(
Try this:
$scope.setCriteria({criteria: option});
When you declare an isolated scope "&" property, angular parses the expression to a function that would be evaluated against the parent scope.
when invoking this function you can pass a locals object which extends the parent scope.
It's a common mistake to think that $scope.setCriteria is the same as the function inside the attribute. If you log it you'll see it's just an angular parsed expression function which have the parent scope saved at it's closure.
So when you run $scope.setCriteria() you actually evaluate an expression against the parent scope.
In your case this expression happens to be a function but it could be any expression.
But you don't have a criteria property on the parent scope, that's why angular let you pass a locals object to extend the parent scope. e.g. {criteria: option}
Extends the parent scope
you wrote in a comment that it requires the directive to have knowledge of the parameter name defined in the controller. No it doesn't, it just extends the parent scope with a criteria option, you can still use any expression you want though you are provided with an extra property you may use.
A good example would be ngEvents, take ng-click="doSomething($event)":
ngClick provides you with a local property $event, you don't have to use but you may if you need.
the directive doesn't know anything about the controller, it's up to you to decide which expression you write, cheers.
You can pass the function in using =...
scope: {
model: '=',
setCriteria: '='
},
controller: function($scope) {
// ...
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
<div search-filter-lookup model="customField" criteria="updateCriteria"></div>

Resources