I've got a simple contactList component, which has 2 bindings: contacts and onRemove.
contacts is just an array of contacts to display
onRemove is a callback function
app
.component('contactList', {
template:
`<div ng-repeat="c in $ctrl.contacts">
{{c.name}}
<div ng-click="$ctrl.onRemove({contact: c})">Remove</div>
</div>`,
bindings: {
contacts: '<',
onRemove: '&'
},
controller: function() {}
})
I use this component multiple times within my app. And the onRemove callback can behave differently, depending on where the contactList component is getting used. Example:
contactMainView (= component) displays a search bar and the resulting list of contacts using the contactList component. onRemove will delete the contact from the database.
groupMembersView displays all contacts belonging to the given group using the contactList component. Here it shouldn't be possible to remove a contact, though onRemove will not be set.
Ideas:
First I thought, that I could use an ng-if="$ctrl.onRemove" but that doesn't work, because onRemove is never undefined within contactList component. console.log(this.onRemove) always prints: function(locals) { return parentGet(scope, locals); }
Of course I could use another showRemove: '#' binding, but that doesn't look DRY to me.
Do you have any idea or some better solution to get things done?
The simplest way would to check if the attribute is defined. In your controller inject $attrs and then you can do:
if( $attrs.onRemove ) { //Do something }
Using the & binding angular will wrap the function in order to keep references to the original $scope of the passed method, even if is not defined.
Execute the function onRemove on component allow to get if a function was passed in parameter. So you can use ng-if="$ctrl.onRemove()"
component('contactList', {
template:
`<div ng-repeat="c in $ctrl.contacts">
{{c.name}}
<div ng-click="$ctrl.onRemove()({contact: c})" ng-if="$ctrl.onRemove()">Remove</div>
</div>`,
bindings: {
contacts: '<',
onRemove: '&'
},
controller: function() {
console.log(this.onRemove);
console.log(this.onRemove());
}
})
another option is to define the callback as optional by adding a question mark in the binding definition:
onRemove: '&?'
text from the documentation: All 4 kinds of bindings (#, =, <, and &) can be made optional by adding ? to the expression. The marker must come after the mode and before the attribute name. See the Invalid Isolate Scope Definition error for definition examples.
Related
Is there any way to specify the default value for an # binding of a component.
I've seen instruction on how to do it with directive: How to set a default value in an Angular Directive Scope?
But component does not support the compile function.
So, I have component like this:
{
name: 'myPad',
bindings : {layout: '#'}
}
I want to free users of my component from having to specify the value of the 'layout' attribute. So..., this:
<my-pad>...</my-pad>
instead of this:
<my-pad layout="column">...</my-pad>
And... this 'layout' attribute is supposed to be consumed by angular-material JS that 'm using, so it needs to be bound before the DOM is rendered (so the material JS can pick it up & add the corresponding classes to the element).
update, some screenshots to clarify the situation:
Component definition:
{
name : 'workspacePad',
config : {
templateUrl: 'src/workspace/components/pad/template.html',
controller : controller,
bindings : {
actions: '<', actionTriggered: '&', workspaces: '<', title: '#',
flex: '#', layout: '#'
},
transclude: {
'workspaceContent': '?workspaceContent'
}
}
}
Component usage:
<workspace-pad flex layout="column" title="Survey List" actions="$ctrl.actions"
action-triggered="$ctrl.performAction(action)">
<workspace-content>
<div flex style="padding-left: 20px; padding-right: 20px; ">
<p>test test</p>
</div>
</workspace-content>
</workspace-pad>
I want to make that "flex" and "layout" in the second screenshot (usage) optionals.
UPDATE
My "solution" to have this in the constructor of my component:
this.$postLink = function() {
$element.attr("flex", "100");
$element.attr("layout", "column");
$element.addClass("layout-column");
$element.addClass("flex-100");
}
I wish I didn't have to write those last 2 lines (addClass)... but well, since we don't have link and compile in component.... I think I should be happy with it for now.
First of there is great documentation for components Angularjs Components`. Also what you are doing I have done before and you can make it optional by either using it or checking it in the controller itself.
For example you keep the binding there, but in your controller you have something like.
var self = this;
// self.layout will be the value set by the binding.
self.$onInit = function() {
// here you can do a check for your self.layout and set a value if there is none
self.layout = self.layout || 'default value';
}
This should do the trick. If not there are other lifecycle hooks. But I have done this with my components and even used it in $onChanges which runs before $onInit and you can actually do a check for isFirstChange() in the $onChanges function, which I am pretty sure will only run once on the load. But have not tested that myself.
There other Lifecycle hooks you can take a look at.
Edit
That is interesting, since I have used it in this way before. You could be facing some other issue. Although here is an idea. What if you set the value saved to a var in the parent controller and pass it to the component with '<' instead of '#'. This way you are passing by reference instead of value and you could set a watch on something and change the var if there is nothing set for that var making it a default.
With angularjs components '#' are not watched by the component but with '<' any changes in the parent to this component will pass down to the component and be seen because of '<'. If you were to change '#' in the parent controller your component would not see this change because it is not apart of the onChanges object, only the '<' values are.
To set the value if the bound value is not set ask if the value is undefined or null in $onInit().
const ctrl = this;
ctrl.$onInit = $onInit;
function $onInit() {
if (angular.isUndefined(ctrl.layout) || ctrl.layout=== null)
ctrl.layout = 'column';
}
This works even if the value for layout would be false.
Defining the binding vars in constructor will just initiate the vars with your desired default values and after initialization the values are update with the binding.
//ES6
constructor(){
this.layout = 'column';
}
$onInit() {
// nothing here
}
you can use
$onChanges({layout}) {
if (! layout) { return; }
this.setupLayout(); ---> or do whatever you want to do
}
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/.
I am building a simple view:
<tabulation tabulation-data="vm.tabs"></tabulation>
<div ng-switch="vm.activeTab.id">
<account-details ng-switch-when="details"></account-details>
<account-history ng-switch-when="history"></account-history>
<account-summary ng-switch-when="summary"></account-summary>
<account-dashboard ng-switch-when="dashboard"></account-dashboard>
</div>
Essentially, as I have it working now, tabulation will $emit an event to the parent account controller, which will update the vm.activeTab property to toggle through the different tab content.
A colleague of mine told me it may be more elegant to use bindings (&) on the tabulation component, which will use a function passed by the parent account component...
Unfortunately, I don't seam to understand how it functions:
Parent account controller:
function PocDemoContainerController($scope) {
var vm = this;
vm.tabs = [{
label: 'Details',
id: 'details'
},
{
label: 'History',
id: 'history'
},
{
label: 'Summary',
id: 'summary'
},
{
label: 'Dashboard',
id: 'dashboard'
}];
vm.activeTab = vm.tabs[0];
// this is the function that I want to pass to the tabulate component
vm.onClickTab = function (tab) {
vm.activeTab = tab;
};
...
}
Tabulate component html:
<tabulation tabulation-data="vm.tabs" on-click-tab="vm.onClickTab(tab)">
<div class="tabulation">
<nav class="container">
<button class="tabulation__mobile-toggle"
ng-class="{'tabulation__mobile-toggle--is-open': vm.mobileTabulationIsOpen}"
ng-click="vm.toggleMobileTabulation()">{{vm.activeTab.label}}</button>
<ul class="tabulation__container"
ng-class="{'tabulation__container--is-open': vm.mobileTabulationIsOpen}">
<li class="tabulation__item"
ng-repeat="tab in vm.tabs"
ng-class="{'tabulation--is-active': vm.isTabActive(tab)}">
<a id={{tab.id}}
class="tabulation__link"
ng-click="vm.onClick(tab)">{{tab.label}}</a>
</li>
</ul>
</nav>
</div>
</tabulation>
Tabulate controller:
...
module.exports = {
template: require('./tabulation.html'),
controller: TabulationController,
controllerAs: 'vm',
bindings: {
tabulationData: '<',
onClickTab: '&' // this should send data up, right?
}
};
Tabulation controller:
function TabulationController($scope) {
var vm = this;
...
vm.onClick = function (tab) {
vm.onClickTab(tab); // This is the function from the parent I want to call
};
...
}
TabulationController.$inject = [
'$scope'
];
module.exports = TabulationController;
So, the tabulation controller can see and call vm.onClickTab but the parameter value that is being passed is not passed to the parent account component controller...
How do I achieve this? (is it even possible that way?)
Alright! I finally found out how to do it, and it was not at all intuitive (the angular docs don't tell you this).
Based on the example above, my tabulation needs to tell the parent component which tab is now active so that it can update the view.
Here is how:
Declare a function on your parent component:
vm.onClickTab = function (tab) { ... };
Put the function on your child component.
<tabulation on-click-tab="vm.onClickTab(tab)"
IMPORTANT: If you are passing an argument, use the same name as the one you will define in your child component.
Inside your child component, declare a new function, and it is that function that should call the parent's callback.
vm.onClick = function (tab) { ... };
Now, here comes the part that is not mentioned anywhere: You have to call the parent's callback function using an object, with a property that uses the same name defined when passing that callback to the child component:
.
function TabulationController($scope) {
...
vm.onClick = function (tab) {
// The onClickTab() function requires an object with a property
// of "tab" as it was defined above.
vm.onClickTab({tab: tab});
};
...
}
Now, when vm.onClick() is called, it calls the parent's callback with an object, passing it the argument, and the parent can now use that data to update the view.
Looking at your Tabulate component html I wonder what is the tab parameter you are sending. Is it a local property of your controller? it seems not because there is no vm prefix to it (or any other name you've defined). Your code seems legit, it is the parameter origin that is not clear, therefore undefined.
Give us a hint on its origin for further analysis.
I had a similar problem and found this solution. I'm not sure if it's the best one but it works.
In the parent controller
I call my component
<contact button-action="vm.select(targetContact)"/>
And define my function
function select(contact) {...}
In my contact component
I define the binding:
bindings: { buttonAction: '&' }
And call the function
<button type="button" ng-click="$ctrl.buttonAction()">Click me</button>
When I click on my component button, select function is called passing the targetContact
I am making a component that contains 3 child components in this way:
<header-component>
<side-component>
<main-component>
The main component contains list of heroes.
The header component contains two buttons that are suppose to switch the view on the main component to list or grid view.
The problem I have now is passing data from the header-component to the main component. So when I click grid button the view on the main content should change to grid view , same for the row view.
How can the data be passed between child components in angular 1.5 ?
Component approach
I would suggest you to align with Angular 2 component approach and use inputs/outputs approach. If you do so, you will be able to easily migrate to Angular 2, because components will be conceptually identical (with difference only in syntax). So here is the way you do it.
So we basically want header and main components to share piece of state with header to be able to change it. There are several approaches we can use to make it work, but the simplest is to make use of intermediate parent controller property. So let's assume parent controller (or component) defines this view property you want to be used by both header (can read and modify) and main (can read) components.
Header component: input and output.
Here is how simple header component could look like:
.component('headerComponent', {
template: `
<h3>Header component</h3>
<a ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
<a ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
`,
controller: function() {
this.setView = function(view) {
this.view = view
this.onViewChange({$event: {view: view}})
}
},
bindings: {
view: '<',
onViewChange: '&'
}
})
The most important part here is bindings. With view: '<' we specify that header component will be able to read outer something and bind it as view property of the own controller. With onViewChange: '&' components defined outputs: the channel for notifying/updating outer world with whatever it needs. Header component will push some data through this channel, but it doesn't know what parent component will do with it, and it should not care.
So it means that header controller can be used something like
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
Main component: input.
Main component is simpler, it only needs to define input it accepts:
.component('mainComponent', {
template: `
<h4>Main component</h4>
Main view: {{ $ctrl.view }}
`,
bindings: {
view: '<'
}
})
Parent view
And finally it all wired together:
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>
Take a look and play with simple demo.
angular.module('demo', [])
.controller('RootController', function() {
this.view = 'table'
})
.component('headerComponent', {
template: `
<h3>Header component</h3>
<a class="btn btn-default btn-sm" ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
<a class="btn btn-default btn-sm" ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
`,
controller: function() {
this.setView = function(view) {
this.view = view
this.onViewChange({$event: {view: view}})
}
},
bindings: {
view: '<',
onViewChange: '&'
}
})
.component('mainComponent', {
template: `
<h4>Main component</h4>
Main view: {{ $ctrl.view }}
`,
bindings: {
view: '<'
}
})
<script src="https://code.angularjs.org/1.5.0/angular.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css" />
<div class="container" ng-app="demo" ng-controller="RootController as root">
<pre>Root view: {{ root.view }}</pre>
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>
</div>
Demo: http://plnkr.co/edit/ODuY5Mp9HhbqA31G4w3t?p=info
Here is a blog post I wrote covering component-based design in details: http://dfsq.info/site/read/angular-components-communication
Although the parent component approach (passing down data via attributes) is a perfect valid and yet good implementation, we can achieve the same thing in a simpler way using a store factory.
Basically, data is hold by the Store, which is referenced in both components scope, enabling reactive updates of the UI when the state changes.
Example:
angular
.module('YourApp')
// declare the "Store" or whatever name that make sense
// for you to call it (Model, State, etc.)
.factory('Store', () => {
// hold a local copy of the state, setting its defaults
const state = {
data: {
heroes: [],
viewType: 'grid'
}
};
// expose basic getter and setter methods
return {
get() {
return state.data;
},
set(data) {
Object.assign(state.data, data);
},
};
});
Then, in your components you should have something like:
angular
.module('YourApp')
.component('headerComponent', {
// inject the Store dependency
controller(Store) {
// get the store reference and bind it to the scope:
// now, every change made to the store data will
// automatically update your component UI
this.state = Store.get();
// ... your code
},
template: `
<div ng-show="$ctrl.state.viewType === 'grid'">...</div>
<div ng-show="$ctrl.state.viewType === 'row'">...</div>
...
`
})
.component('mainComponent', {
// same here, we need to inject the Store
controller(Store) {
// callback for the switch view button
this.switchViewType = (type) => {
// change the Store data:
// no need to notify or anything
Store.set({ viewType: type });
};
// ... your code
},
template: `
<button ng-click="$ctrl.switchViewType('grid')">Switch to grid</button>
<button ng-click="$ctrl.switchViewType('row')">Switch to row</button>
...
`
If you want to see a working example, check out this CodePen.
Doing so you can also enable the communication between 2 or N components. You just only have to:
inject the store dependency
make sure you link the store data to your component scope
like in the example above (<header-component>).
In the real world, a typical application needs to manage a lot of data so make more sense to logically split the data domains in some way. Following the same approach you can add more Store factories. For example, to manage the current logged user information plus an external resource (i.e. catalog) you can build a UserStore plus a CatalogStore -- alternatively UserModel and CatalogModel; those entities would also be good places to centralize things like communication with the back-end, add custom business logic, etc. Data management will be then sole responsibility of the Store factories.
Keep in mind that we're mutating the store data. Whilst this approach is dead simple and clear, it might not scale well because will produce side effects. If you want something more advanced (immutability, pure functions, single state tree, etc.) check out Redux, or if you finally want to switch to Angular 2 take a look at ngrx/store.
Hope this helps! :)
You don't have to do it the Angular 2 way because just in case
you would migrate sometimes... Do it if it make sense for you to do it.
Use custom events to achieve this.
you can pass message across your application using event dispatchers $emit(name, args); or $broadcast(name, args);
And you can listen for this events using method $on(name, listener);
Hope it helps
Ref:
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$emit
Example:
you can notify change like below from your header-component
$rootScope.$emit("menu-changed", "list");
And you can listen for the change in your main-component directive like
$rootScope.$on("menu-changed", function(evt, arg){
console.log(arg);
});
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>