AngularJS component binding a function instead of emitting an event - angularjs

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

Related

Trouble understanding Angular components

I am trying to learn about Angular components but am having trouble getting them to work in a nested component configuration. Basically what I want to do is to have a parent component that periodically updates a value. I then want the inner child component to bind to the value in the parent and trigger a call to $onChanges when the value changes.
I made a jsFiddle demonstrating what I'm trying to accomplish. The parent component seems to be working and displaying the updated value, but for some reason the child component doesn't render at all. Here is the code I am using to accomplish this:
let app = angular.module('app', []);
class ParentController {
constructor($interval) {
this.value = 0;
$interval(() => this.value++, 1000);
}
}
let ParentComponent = {
controller: ParentController,
template: `<div>parent value: {{$ctrl.value}}</div>`
};
app.component('parent', ParentComponent);
class ChildController {
$onChanges(changesObj) {
console.log(changesObj);
}
}
let ChildComponent = {
bindings: {
value: '='
},
controller: ChildController,
require: {
parent: '^^parent'
},
template: `<div>child value: {{$ctrl.value}}</div>`
};
app.component('child', ChildComponent);
And the HTML:
<div ng-app="app">
<parent>
<child value="$ctrl.parent.value"></child>
</parent>
</div>
Am I doing something wrong or is what I'm trying to accomplish not possible?
The thing about components is that anything inside of them typically gets replaced unless you use the 'ng-transclude' directive.
To get your child element to appear, you need the following changes:
let ParentComponent = {
controller: ParentController,
transclude: true,
template: `<div>parent value: {{$ctrl.value}}</div><div ng-transclude></div>`
};
To clarify, you add transclude: true to the component object as well as an element with the ng-transclude directive within the template. Whatever element has the ng-transclude directive will have its contents replaced with whatever is in the <parent> tag.
This will only get your child component to render, you still have another error in your HTML being that you're trying to use
<child value="$ctrl.parent.value"></child>
Which, in the JavaScript, is equivalent to $scope.$ctrl.parent.value which is undefined.
Just change your HTML to:
<div ng-app="app">
<parent>
<child></child>
</parent>
</div>
And the child component to:
let ChildComponent = {
controller: ChildController,
require: {
parent: '^^parent'
},
template: `<div>child value: {{$ctrl.parent.value}}</div>`
};
And then you're all good!
EDIT (from comment):
In order to trigger a update call whenever the parent value changes, you would need to replace your ChildController with the following:
let ChildController = function (scope) {
scope.$watch('$ctrl.parent.value', function (newValue, oldValue) {
console.log(newValue);
});
};
This adds a watch on the child controller that will call the function each time the parent.value changes.
i have made som modifications to your code.
there is a different way of achieving what you want to do.
here is my code I will try to explain how it should be done.
let app = angular.module('app', []);
class ParentController {
constructor($interval) {
this.value = 0;
$interval(() => this.value++, 1000);
}
}
let ParentComponent = {
controller: ParentController,
//here is the important edit. loading child component inside the parent component
template: `<div>
parent value: {{$ctrl.value}}
<child value="$ctrl.value"></child>
</div>`
};
app.component('parent', ParentComponent);
class ChildController {
$onChanges(changesObj) {
if(changesObj.hasOwnProperty('value')){
this.value=changesObj.value.currentValue //this referes to current scope.
}
}
}
let ChildComponent = {
bindings: {
value: '<'
},
controller: ChildController,
require: {
//parent: '^^parent' no need for this. we will use $onChanges
},
template: `<div>child value: {{$ctrl.value}}</div>`
};
app.component('child', ChildComponent);
<div ng-app="app"><parent></parent></div>
things I have done in this code sample :
I have included component inside the parent component template
because what you want to do is load child inside parent. so added child inside the parent template.
Removed Require in child component definition because it would make your child component dependent on the parent component. there is a different use case where you must use the parent. it's best you use bindings and $onChanges to update data in a child from a parent,
added some validations to the $onChanges data.
if(changesObj.hasOwnProperty('value')){
this.value=changesObj.value.currentValue
}
you need to understand that the change obj is a simple change Object. it has multiple properties like is first change(), current value, OldValue etc.
you must always validate what you want from here.
also i am setting it to this.value which is the current scope.
we are not directly using the parent scope or binding scope variable. i am creating a new scope object which you will use to display the data. it will be populated by the binding object.
overall this is happening
1.parent interval updates parents scope
2.this.value is passed to child as Bindings
3.$onChanges validates bindings and assigns it to local scope this.value
4.child template renders this.value in the UI

Angular 1.5 Component Bindings: Check if Callback is Present

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.

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.

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

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

AngularJS change attribute model from inside directive

I have setup a directive as below, which is to control the opening and closing of a Flyout panel, I want the state(whether its open/closed) of the flyout to be publicly accessible by the parent scope, I see some use a service for this but it seems verbose use a service, my questions is I'm wondering is there an elegant way to set the variable attached to the attribute on the Close Event? or do I have to access the parent scope? Fiddle Here http://jsfiddle.net/sjmcpherso/EbDRR/
<div class="page">
<button ng-click="openFlyout()">Bhttp://jsfiddle.net/sjmcpherso/EbDRR/#baseutton {{fly.flyoutOpen}}</button>
<flyout foopen={{fly.flyoutOpen}}>
<button ng-click="close()">X</button>
</flyout>
</div>
angular.module('myApp', [])
.controller('MyController', function($scope) {
$scope.fly = {flyoutOpen:false};
$scope.openFlyout = function(){
$scope.fly.flyoutOpen = !$scope.fly.flyoutOpen;
}
}).directive('flyout', function() {
return {
restrict: 'AE',
link: function(scope, el, attr) {
close.bind('click', function() {
//Set fly.flyoutOpen via attr.foopen to false
});
attr.$observe('foopen', function(value) {
console.log(typeof value);
if (value == "true") {
el.css('right', '0');
console.log("left:"+value);
} else {
el.css('right', '-150px');
console.log("right:"+value);
}
});
}
};
});
In my opinion, it is not angular-way to change parent scope in directives.
Although I also have faced the situations that I could not find other solutions, I think we should avoid this way as much as possible.
In your situation, I believe you could avoid to change the parent scope in your directive.
Therefore what I suggest you is ...
Remove close.bind('click'... in your directive.
Add close function of the controller in which you could change the scope value.(as you know this way would not break the angular-principles at all.)
jsfiddle is here.
I hope this would help you.
It is simple.
You can can use directive scope that can use for two way binding.
<http://jsfiddle.net/EbDRR/14/>
I have added "openclose" attribute in your code.
Have a look at these two examples; sorry they're both one-pagers...
I'm trying to keep the code self-contained, so that you can see the all of the pieces in one place. Normally, there'd be build-processes keeping code and templates and the page separate; not so much, here.
The good news is that these pages should work without so much as a server (as the only external file is angular), so you can save each one as an .html page, and open it from the drive.
Shared Config Between Parent and Child
<!doctype html>
<html ng-app="myApp" ng-controller="MyController">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<style>
page-flyout { display: block; }
page-flyout.expanded { background-color : grey; }
</style>
</head>
<body>
<main ng-controller="page-controller as page">
<button ng-click="page.toggle()"
>{{ page.flyout.expanded ? "Hide":"Show" }} Flyout</button>
<page-flyout
view="page.flyout"
class="ng-class: {'expanded':page.flyout.expanded,'contracted':!page.flyout.expanded }"
></page-flyout>
</main>
<script type="text/ng-template" id="components/page-flyout/page-flyout.html">
<h4>I am {{ view.expanded ? "OPEN" : "CLOSED" }}</h4>
<button ng-click="flyout.close()" ng-hide="!view.expanded">×</button>
</script>
<script >
var app = initApp();
initAngular(app);
function initAngular (MyApp) {
angular.module("myApp", ["pageFlyout"]);
angular.module("pageFlyout", []);
angular.module("myApp")
.controller("MyController", [MyApp.MyController])
.controller("page-controller", [MyApp.PageController]);
angular.module("pageFlyout")
.controller("page-flyout-controller", ["$scope", MyApp.PageFlyoutController])
.directive("pageFlyout", [function () {
return {
scope: { view: "=" },
restrict: "AE",
replace: false,
controller: "page-flyout-controller",
controllerAs: "flyout",
templateUrl: "components/page-flyout/page-flyout.html"
};
}]);
};
function initApp () {
var MyApp = {
MyController: function MyController () {
},
PageController: function PageController () {
var page = extend(this, { toggle: toggle, flyout: { expanded: false } });
function toggle () {
var currentState = page.flyout.expanded;
page.flyout.expanded = !currentState;
}
},
PageFlyoutController: function PageFlyoutController ($scope) {
var flyout = extend(this, { close: close });
function close () { $scope.view.expanded = false; }
}
};
function extend () {
var args = [].slice.call(arguments);
return angular.extend.apply(angular, args);
}
return MyApp;
}
</script>
</body>
</html>
I'm using a "PageController" as an outer controller; this outer element has a toggle method, and a flyout object.
The flyout object has an expanded property, and nothing more.
This is a config / state object that will be shared between the parent and child. The parent won't know anything about the child, the child won't know anything about the parent...
But both need to know about the structure of this one object; basically, the parent has to agree to give the child a config object which meets the child's needs, if it's going to use the child's services.
In my child scope, I'm referring to this object as the view, which is provided in my directive declaration <page-flyout view="page.flyout">.
The flyout has a controller, with a close method, which sets view.expanded = false;
Because the parent and the child both share this config object, and because a UI event triggers a check for digest, this works and everybody's happy...
Try it out, and see what happens: the flyout html is in a completely different universe, and yet the button still does what it should, and all of the text inside and outside of the flyout's scope keeps itself up to date...
...except that this isn't quite as clean as could be.
Sharing some sort of readable state would be one thing, but two people directly writing to it, and reading from it, might be a bit much.
Parent Controls State, Child Defines Event, Parent Implements Event-Handler
<!doctype html>
<html ng-app="myApp" ng-controller="MyController">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<style>
page-flyout { display: block; }
page-flyout.expanded { background-color : grey; }
</style>
</head>
<body>
<main ng-controller="page-controller as page">
<button ng-click="page.toggle()"
>{{ page.flyout.expanded ? "Hide":"Show" }} Flyout</button>
<page-flyout
onclose="page.close()"
class="ng-class: {'expanded':page.flyout.expanded,'contracted':!page.flyout.expanded }"
></page-flyout>
</main>
<script type="text/ng-template" id="components/page-flyout/page-flyout.html">
<h4>I am {{ view.expanded ? "OPEN" : "CLOSED" }}</h4>
<button ng-click="flyout.close()">×</button>
</script>
<script >
var app = initApp();
initAngular(app);
function initAngular (MyApp) {
angular.module("myApp", ["pageFlyout"]);
angular.module("pageFlyout", []);
angular.module("myApp")
.controller("MyController", [MyApp.MyController])
.controller("page-controller", [MyApp.PageController]);
angular.module("pageFlyout")
.controller("page-flyout-controller", ["$scope", MyApp.PageFlyoutController])
.directive("pageFlyout", [function () {
return {
scope: { onclose: "&" },
restrict: "AE",
replace: false,
controller: "page-flyout-controller",
controllerAs: "flyout",
templateUrl: "components/page-flyout/page-flyout.html"
};
}]);
};
function initApp () {
var MyApp = {
MyController: function MyController () {
},
PageController: function PageController () {
var page = extend(this, {
toggle: toggle,
close: close,
flyout: { expanded: false }
});
function toggle () {
var currentState = page.flyout.expanded;
page.flyout.expanded = !currentState;
}
function close () {
page.flyout.expanded = false;
}
},
PageFlyoutController: function PageFlyoutController ($scope) {
var flyout = extend(this, { close: close });
function close () { $scope.onclose(); }
}
};
function extend () {
var args = [].slice.call(arguments);
return angular.extend.apply(angular, args);
}
return MyApp;
}
</script>
</body>
</html>
I'll save you some hunting; almost every single line here is 100% the same.
The important ones are in the directive:
return {
scope: { onclose: "&" }
}
...the ampersand means that the property $scope.onclose is a method, which is defined on declaration of the directive.
Sure enough, when you look at the declaration of page-flyout, you'll notice <page-flyout onclose="page.close()">
Now $scope.onclose doesn't equal page.close(); so much as it equals function () { page.close(); }.
It could, instead be a simple expression, inside of the scope of the parent:
<page-flyout onclose="page.flyout.expanded = false">
$scope.onclose = function () { page.flyout.expanded = false; };
You can even pass named params back into your parent expression/method (unnecessary here; read up on it in tutorials, but basically you call $scope.method( paramsObj ) where the keys of the object are the same as the argument names in the implementation. Really handy stuff.
So in my case, my flyout button calls flyout.close(), a method on my controller. My controller method calls $scope.onclose(); (a separation between my viewmodel stuff, and the angular-specific glue that ties everything together), $scope.onclose then calls page.close() in the parent, which sets page.flyout.expanded = false;.
Nice. Neat. Clean.
The child knows absolutely nothing about the parent, and yet, I created an event that the parent could subscribe to; in HTML, no less.
One big problem, though.
If you load this page up, you'll notice that the flyout's text never changes, and the close button for the flyout never hides itself, based on whether the flyout is "open" or "closed".
The reason for that is dirt-simple.
The child knows absolutely nothing about the parent or the parent's data.
That's fine, but in this case, that means despite being able to close itself just fine, it can never know if it's open in the first place (if I used ng-hide in the parent scope, that would solve the problem, if you didn't want to leave it half-open).
The solution, of course, is to give the child a shared object, representing the state of a flyout...
We've been there before...
...except that this time, it's a read-only object (no guarantees, just by practice), and you're using events to talk up, and using properties to talk down.
The real solution involves doing both: having both the read-only properties for the child to use in its view, and to create events that the parent can hook into, to update its model (and what it gives back to the child on the next $digest).
Then aside from the contract the child defines and the parent keeps, neither one knows anything about the other.
I assume what you want to achieve is as follows.
encapsulate the features inside directive.
don't share the scope values with parent controller.
don't use services.
My idea is something like this.
Prepare the function which is the wrapper for updating the element of your flyout panel.(This function is completely encapsulated only in the directive.)
Instanciate the wrapper and use it in your directive code independently.
After instanciating the wrapper, the directive send it via oninit function to a parent controller.
After recieveing the wrapper, the controller could use it without sharing the scope.
jsfiddle is here.

Resources