How to pass data between sibling components without using $scope? - angularjs

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);
});

Related

Send a data to another controller in AngularJS

How I can to send array.length to another controller?
First controller: Code below
function uploader_OnAfterAddingFile(item) {
var doc = {item: {file: item.file}};
if (doc.item.file.size > 10240) {
doc.item.file.sizeShort = (Math.round((doc.item.file.size / 1024 / 1024) * 100) / 100) + 'MB';
} else {
doc.item.file.sizeShort = (Math.round((doc.item.file.size / 1024) * 100) / 100) + 'KB';
}
doc.item.showCancel = true;
if ($scope.documentStatus) {
item.formData.push({status: $scope.documentStatus});
}
if ($scope.tenderDraftId) {
item.formData.push({tenderDraftId: $scope.tenderDraftId});
}
item.getDoc = function () { return doc; };
doc.item.getUploadItem = function () { return item; };
$scope.documents.push(doc);
//I need send $scope.documents.length
}
send to this function on other controller
Second Controller:
They are in one page.
First Controller it is a component which release upload files.
Second controller it is a modal window where we have 2 input of text and element with first controller.
All I need it to now array.length of files which were upload in submit function on modal window. I tried with $rootScope but it didn`t help me.
I think what you really want to do here is to $emit or $broadcast an event. This will allow you to write less code and be able to pass this data effortlessly to anyplace in the application that you wish! Using event listeners, $on, would also provide the same effect.
Please give this article a good read to understand which option is best for your use case.
https://medium.com/#shihab1511/communication-between-controllers-in-angularjs-using-broadcast-emit-and-on-6f3ff2b0239d
TLDR:
$rootScope.$broadcast vs. $scope.$emit
You could create a custom service that stores and returns the value that you need:
see more information under the title 'Create Your Own Service'.
Or you could inject routeParams to the second controller: see more information
I came across a similar problem the other day. I would use data binding along with a $ctrl method. Here is a really good article with an example that you can replicate with your use case: http://dfsq.info/site/read/angular-components-communication Hope this helps. This form of communication makes it a lot easier to share data between two components on the same page. Article example:
Header component: input and output
.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: '&'
}
})
So it means that header component can be used something like this
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
Main component: input
.component('mainComponent', {
template: `
<h4>Main component</h4>
Main view: {{ $ctrl.view }}
`,
bindings: {
view: '<'
}
})
Parent component
<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>
I used the method explained above to pass data between two controllers to hide one component, when a button is clicked in a different component. The data that was being passed was a boolean. I would expect you would be able to do the same thing with array.length.

Expose element of parent component to child

I have a main component that handles the toolbar and sidnav of my angular application. I would like to make a div inside the toolbar available to child components (and controllers) to customize so that they can do things like change the toolbar title text and add contextual buttons. This feels sort of like the opposite of transposition where a parent component can customize part of a child component (e.g. a menu component customizing the content of a button). One option would be to have the toolbar managed by a service, but even then I can't think of a great way to customize the content of the toolbar without doing a decent amount of javascript that builds up dom elements (one of the things I always try to avoid in angular).
In Angular 1.6.x components use isolate scope only:
Components only control their own View and Data: Components should
never modify any data or DOM that is out of their own scope. Normally,
in AngularJS it is possible to modify data anywhere in the application
through scope inheritance and watches. This is practical, but can also
lead to problems when it is not clear which part of the application is
responsible for modifying the data. That is why component directives
use an isolate scope, so a whole class of scope manipulation is not
possible.
So, to make this work you would need to use a directive instead of a component. The div you want to include would need to be a directive itself to be able to alter the parent scope of the toolbar directive. What you would be doing is transcluding one directive inside another, and using shared scope to alter the parent scope.
This article is a really good resource for what you are trying to accomplish. I would start here: https://www.airpair.com/angularjs/posts/transclusion-template-scope-in-angular-directives
I have altered the example Codepen from that article to show you how it might work: http://codepen.io/jdoyle/pen/aJQpYo
If you select items in the list you can see that the header name changes to whatever is selected.
angular.module("ot-components", [])
.controller("AppController",($scope)=> {
//Normally, this data would be wrapped in a service. For example only.
$scope.header = "Marketing";
$scope.areas = {
list: [
"Floorplan",
"Combinations",
"Schedule",
"Publish"
],
current: "Floorplan"
};
})
.directive("otList", ()=> {
return {
scope: false, // this is one of the major changes
template:
`<ul class="ot-list">
<li class="ot-list--item"
ng-repeat="item in items"
ng-bind="item"
ng-class="{'ot-selected': item === selected}"
ng-click="selectItem(item)">
</li>
</ul>`,
link: (scope, elem, attrs) => {
scope.items = JSON.parse(attrs.items);
scope.selected = attrs.selected;
scope.selectItem = (item) => {
scope.selected = item;
scope.$parent.header = item; // this is the other major change
};
}
};
})
.directive("otSite", ()=> {
return {
scope: true, // another major change
transclude: true,
template:
`<div class="ot-site">
<div class="ot-site--head">
<img class="ot-site--logo" src="//guestcenter.opentable.com/Content/img/icons/icon/2x/ot-logo-2x.png">
<h1>{{header}}</h1>
</div>
<div class="ot-site--menu">
</div>
<div class="ot-site--body" ng-transclude>
</div>
<div class="ot-site--foot">
© 2015 OpenTable, Inc.
</div>
</div>`
};
});

Angular 1.5, Calling a function in a component from Parent controller

Angular 1.5 components easily allow creating a call back to the parent from the component. Is there a way i can call a function in a component from a function in parent's controller ?
Lets say my component is called task-runner and below is the HTML for it in the parent container.
<task-runner taskcategogyid=5></task-runner>
<button type="button" ng-click="doSomethingInParent()">ParentToChildButton</button>
The plunkr is here. I want that when ParentToChildButton is clicked, the function doSomethingInParent() calls the remotefunc in component.
A few different ways:
Pass an object as an attribute with two-way binding (scope:{myattr:'='}) to the task-item-header directive which the directive could then add a function to for the parent controller to call.
Set an attribute that has either one-way binding (scope:{myattr:'#'}) on it and then attrs.$observe changes to it to trigger the action, or two-way binding (scope:{myattr:'='}) and then $scope.$watch changes to it to trigger the action.
Have the directive raise an event (scope:{raiseLoaded:'&onLoaded'}) that passes an object that represents a remote control object with a method on it that triggers the action you want. To raise the event, you'd call something like raiseLoaded({remoteControl: remoteControlObj}) within the directive, and then to listen to the event, you'd use <task-item-header on-loaded="setRemote(remoteControl)"> assuming you have a setRemote() method on your parent controller.
Update I just realized your question was for a newer version of AngularJS, so I'm not sure if my answer still applies. I'll leave it here for now, but if you find it is not helpful I can delete it.
I needed something like this previously so I thought I would share how I solved this problem.
Similar to the OP, I needed to freely trigger methods in child components from a parent component. I wanted to be able to trigger this method in the parent freely/separately without the use of the $onChanges lifecycle hook.
Instead I created a notification-registration mechanism to allow a child component to 'register' a method with the parent when it is loaded. This method can then be freely triggered by the parent outside of the $onChanges cycle.
I created a codepen to demonstrate this. It can be easily extended to handle different types of notifications from the parent that aren't related to the data changes.
Index.html
<div ng-app="tester">
<parent></parent>
</div>
Script.js
angular.module('tester', []);
angular.module('tester').component('parent', {
controller: parentController,
template: `
<div class="tester-style">
<button ng-click="$ctrl.notifyChild()">Notify child</button>
<child parent-to-child-notification-registration="$ctrl.childComponentNotificationRegistration(handler)">
</div>
`
});
function parentController() {
let childComponentEventHandler = null;
this.$onInit = function() {
this.value = 0;
};
this.childComponentNotificationRegistration = function(handler) {
childComponentEventHandler = handler;
console.log('Child component registered.');
};
this.notifyChild = function() {
if (childComponentEventHandler) {
childComponentEventHandler(this.value++);
}
};
}
angular.module('tester').component('child', {
bindings: {
parentToChildNotificationRegistration: '&',
},
controller: childController,
template: `
<div class="tester-style">
<h4>Child Component</h4>
</div>
`
});
function childController() {
this.$onInit = function() {
this.parentToChildNotificationRegistration({
handler: this.processParentNotification
});
};
this.processParentNotification= function(parentValue) {
console.log('Parent triggered child notification handler!!!');
console.log('Value passed to handler:', parentValue);
};
};
}
Also for something similar to #adam0101's #3 answer see codepen.

Angular multiple view not getting data

I'm having issues getting a simple nested view to display my data I'm trying to pass to it.
The parent view loads the nested...
Note that if I do {{opc.org.address}} before this, it does spill out the data.
<div ui-view="address" addressData="opc.org.address"></div>
The address template...
Note if I change to {{opc.org.address.(whatever)}} I can see the data fine.
<div class="addressWrapper" ui-view>
<span class="address">{{ addressData.address1 }}</span><br />
<span class="address2" ng-if="addressData.address2">{{ addressData.address2 }}<br />></span>
<span class="city">{{ addressData.city}}</span>,
<span class="state">{{ addressData.state}}</span>
<span class="zip">{{ addressData.postalCode}}</span>
<span class="country" ng-if="addressData.countryCode"><br />{{ addressData.countryCode }}</span>
We're running the site via Typescript and ui-router. Here's the portion of the router file that loads the parent and address view.
.state('search.orgProfile', {
url: '/orgs/:externalOrgId',
views: {
'': {
templateUrl: 'app/orgs/org-profile.tmpl.html',
controller: 'OrgProfileController',
controllerAs: 'opc',
resolve: {
org: function (orgProfileService: IOrgProfileService, $stateParams: any) {
return orgProfileService.getOrgProfile(<string>$stateParams.externalOrgId)
.then(function (orgDetails: IOrg) {
return orgDetails;
});
},
relationships: function (relationshipsService: IRelationshipsService, org: IOrg) {
return relationshipsService.getRelationships(org.id, 0, Constants.PAGE_SIZE)
.then(function (relationships: RelationshipModel[]) {
return relationships;
});
},
notes: function (notesService: INotesService, org: IOrg) {
return notesService.getNotes(Constants.NOTES_TYPE_ORG, org.id)
.then(function (theNotes: NoteModel[]) {
return theNotes;
});
}
}
},
'address#search.orgProfile': {
templateUrl: 'app/components/address/address.tmpl.html'
}
}
});
When running the page, I only see ",". So it is running as angular, but its not seeing addressData that I passed in. Am I missing something? Do I need to define addressData somewhere else?
Scope are inherited betweend inherited state/views.
You should be able to use {{opc.org.address}} in your view's template and $scope.opc in your views controller. It will search for the parent scope. Just be aware to initialize your fields on parent scope. For instance if you don't set the value to null before doing an asynchrnous loading but do it in your view's controller. You will have 2 distinct opc object.
https://github.com/angular-ui/ui-router/wiki/multiple-named-views
There is nothing suggesting you can pass parameters to views, but i don't think you need to.
Since view are internal to a state just use parent's scope.
EDIT : Following the comment : i suggest to use a directive to represent the addressData in a generic way and be able to bind them to a scope. Views are for different part of a state/ Either they represent totally different data (ex : header/content/footer views) or they are used to represent some data inherited from the scope. So they are strongly bind to the parent's state (if any) and you can't detach them. If you need to detach something and bind it to different state/scope, this is what directive are for.
The solution was to add a controller to the nested view and set the addressData to the orgs address that was resolved in the main view. Took a bit of scope spelunking to find it, but once I plugged it in all was well!
'address#search.orgProfileHome': {
templateUrl: 'app/components/address/address.tmpl.html',
controller: function ($scope: any) {
$scope.addressData = $scope.$parent.$parent.opc.org.address;
}
}

Angular Controller organization and ui-router

I am trying to make a simple list display, but I am having some concerns about Controllers organization.
In my application, I have 2 states, items and state2. In items, I want to display a list of Items, and "something else" in state2.
But I also have a + button at the top of my application that can add an item to my list. And I want that button to be displayed in both states. Here is an illustration:
Now, I would like to put my items related functions, in a specific controller ItemsCtrl. So this would be my routes:
myApp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/items");
$stateProvider
.state('items', {
url: "/items",
templateUrl: "partials/items.html",
controller: "ItemsCtrl"
})
.state('state2', {
url: "/state2",
templateUrl: "partials/state2.html",
controller: "State2Ctrl"
});
});
And this would be my ItemsCtrl:
myApp.controller('ItemsCtrl',function($scope){
$scope.items = ["One",'Two'];
})
And now, I make a new MainCtrl to handle the + button that should be present on any page:
myApp.controller('MainCtrl',function($scope){
$scope.promptItem = function(){
var result = prompt('Add Item', 'New Item', ['ok'], 'Zero');
$scope.items.push(result.input1); //This line doesn't work
}
})
What is the best organization for this kind of interface? Do I really need to put my $scope.items in my MainCtrl?
I'd rather not, and the best thing I think would even be to put the promptItem function in ItemsCtrl, what do you think?
Thanks a lot for your answers, I am completely new to this world :)
EDIT: Here is my HTML structure, my + button is in the root file:
<button ng-click="promptProduct()">Add Item</button>
<a ui-sref="state1">State 1</a>
<a ui-sref="state2">State 2</a>
<div ui-view></div>
This is probably a great place to leverage an Angular service. The problem from what I understand is that you are struggling to share state between different parts of your application. I would recommend creating, e.g. a ListService, as such:
myApp.service('ListService', ListService);
function ListService () {
this.list = ['One', 'Two'];
}
ListService.prototype = {
addItemToList: function (newThing) {
var item = // some initialization of an item from the passed value
this.list.push(item);
}
};
You can then inject ListService anywhere you need access to the data itself. Both the list view and the state2 need it -- the former to actually render the list, and the latter to modify its contents. The method I described above lets you separate the data (the list itself) from the presentation / UI interaction.
[edit]
Likewise, if you want a button that lets you add a new item in both of your views, you could create a directive that receives ListService and prompts the user with a modal when clicked.

Resources