I am using angular 1.5's new component feature to compartmentalize various things; in particular I have a sidenav slide-out menu.
The sidenav needs to run its initialization code after other components are finished loading. So far I cannot find anything that helps me break this logic apart. At the moment, I am accomplishing this with a messy hack, like this.
assume html body such as this;
<body>
<container>
<navigation></navigation>
<sidenav></sidenav>
</container>
</body>
navigation needs to finish rendering before the sidenav can execute correctly. So in my component files, I am doing this (pseudo code);
SideNav Component
bindings = {};
require = { Container: '^container' };
SideNav Controller
$postLink = function() {
Container['Ready']();
}
Navigation Component
bindings = {};
require = { Container: '^container' };
Navigation Controller
$postLink = function() {
if(Container['Ready'])
Container['Ready']();
}
Container Component
transclude = true;
Container Controller
pending = 2; // controls that must finish loading
Ready = function() {
pending--;
if( pending > 0 )
return;
// code to initialize sidenav via jQuery
}
so basically, there is a counter on the container, and each component that I need to be loaded calls a function on the parent that decrements the counter. If that causes the counter to be 0, then the sidenav is initialized.
This feels really caddywhompus, though. Is there any other way to get some form of notification or behavior that can allow me to initialize the sidenav when it is truly the right time?
You can probably think of a better place to hang these items, but the idea I had with the watch is to do something like this:
<html ng-app="myApp">
<body ng-controller="appController as AppCtrl">
<div>
<component1/></component1>
<component2></component2>
<component3></component3>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script>
angular.module("myApp",[])
.controller("appController", function($rootScope,$scope) {
var ctrl=this;
ctrl.readyForAction = readyForAction;
ctrl.letsParty = letsParty;
$scope.$watch("$rootScope.gotBeer && $rootScope.gotPizza && $rootScope.gotHockey",ctrl.readyForAction)
function readyForAction() {
if ($rootScope.gotBeer && $rootScope.gotPizza && $rootScope.gotHockey) {
ctrl.letsParty()
}
else
{
console.log("Not yet!")
}
};
function letsParty() {
alert("Let's go Red Wings!")
};
})
.component("component1", {
template:"<h1>Beer</h2>",
controller: function($rootScope) {
$rootScope.gotBeer=true;
}
})
.component("component2", {
template: "<h1>Pizza</h1>",
controller: function($rootScope) {
$rootScope.gotPizza = true;
}
})
.component("component3", {
template: "<h1>Hockey</h1>",
controller: function($rootScope) {
$rootScope.gotHockey = true;
}
})
</script>
</body>
</html>
I'm just setting the flags when the controllers are created, but obviously you could set them anywhere. So then you just watch an expression that consists of all of your flags and then when they all evaluate to true you go about your business.
Related
I have an AngualrJS app which is currently single page. It will display either a Leaflet map or two Ag-grid, using ng-show/hide on a boolean value, to show only map or grids at a time.
I was thinking that it would be better to add routing, using ui-router, and have 2 views, one for the Leaflet map & one for the two ag-grid.
I had some problems with the grids, probably because it is necessary to do something like
// wait for the document to be loaded, otherwise
// ag-Grid will not find the div in the document.
document.addEventListener("DOMContentLoaded", function() {
// lookup the container we want the Grid to use
var eGridDiv = document.querySelector('#myGrid');
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(eGridDiv, gridOptions);
I am not asking to solve my coding problem, which I hope to figure out by myself.
Instead, I am asking you to help me understand how AngularJs ui-router views work.
Are they always bound to the DOM, and hidden until the appropriate state is entered, or are they added to/removed from the DOM as the stata changes?
Is there anything else that I need to know, in order to understand how it works under the hood?
If I understand the requirements correctly, you could first define some conditions and then make the transition to the appropriate view.
In the example code, you can change the checked attribute for inputs for changing view displayed.
var myApp = angular.module('helloworld', ['ui.router'])
.config(function($stateProvider) {
var helloState = {
name: 'hello',
url: '/hello',
template: '<h3>hello world!</h3>'
}
var aboutState = {
name: 'about',
url: '/about',
template: '<h3>Its the UI-Router hello world app!</h3>'
}
$stateProvider.state(helloState);
$stateProvider.state(aboutState);
})
.directive('selectView', selectView);
let state;
let transitions;
function selectView($state, $transitions) {
state = $state;
transitions = $transitions;
return {
restrict: 'A',
link: selectViewLinkFn
}
}
function selectViewLinkFn($scope, $element) {
const triggers = document.querySelectorAll('input[type="radio"]');
transitions.onSuccess({}, () => {
console.log('onSuccess: ', document.querySelector('h3').innerHTML);
});
transitions.onStart({}, () => {
const findedInView = document.querySelector('h3');
console.log(`onStart: ${findedInView ? findedInView.innerHTML : 'nothing found'}`);
});
setView($scope);
for (const trigger of triggers) {
trigger.addEventListener('change', () => setView($scope, true))
}
function setView(scope, needDigest) {
// Check some conditions
let currentRoute;
for (const trigger of triggers) {
if (trigger.checked) {
currentRoute = trigger.value;
}
}
state.go(currentRoute);
if (needDigest) {
scope.$digest();
}
}
}
selectView.$inject = ['$state', '$transitions'];
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<script src="//unpkg.com/#uirouter/angularjs/release/angular-ui-router.min.js"></script>
<body ng-app="helloworld" select-view>
<label for id="selectHello">Hello</label>
<input name="selectorForView" type="radio" id="selectHello" value="hello" checked>
<label for id="selectAbout">About</label>
<input name="selectorForView" type="radio" id="selectAbout" value="about">
<h2 ng-bind="selectorForView">
<h2>
<ui-view></ui-view>
</body>
The release of AngularJS V1.7.1* introduces the new ng-ref directive. While this new directive enables users to easily do certain things, I see great potential for abuse and problems.
The ng-ref attribute tells AngularJS to publish the controller of a component on the current scope. This is useful for having a component such as an audio player expose its API to sibling components. Its play and stop controls can be easily accessed.
The first problem is the player controls are undefined inside the $onInit function of the controller.
Initial vm.pl = undefined <<<< UNDEFINED
Sample = [true,false]
For code that depends on data being available, how do we fix this?
The DEMO
angular.module("app",[])
.controller("ctrl", class ctrl {
constructor() {
console.log("construct")
}
$onInit() {
console.log("onInit", this.pl);
this.initPL = this.pl || 'undefined';
this.sample = this.pl || 'undefined';
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
}
})
.component("player", {
template: `
<fieldset>
$ctrl.box1={{$ctrl.box1}}<br>
$ctrl.box2={{$ctrl.box2}}<br>
<h3>Player</h3>
</fieldset>
`,
controller: class player {
constructor() {
console.log("player",this);
}
$onInit() {
console.log("pl.init", this)
this.box1 = true;
this.box2 = false;
}
},
})
<script src="//unpkg.com/angular#1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
Initial vm.pl = {{vm.initPL}}<br>
Sample = {{vm.sample}}<br>
<button ng-click="vm.getSample()">Get Sample</button>
<br>
<input type="checkbox" ng-model="vm.pl.box1" />
Box1 pl.box1={{vm.pl.box1}}<br>
<input type="checkbox" ng-model="vm.pl.box2" />
Box2 pl.box2={{vm.pl.box2}}<br>
<br>
<player ng-ref="vm.pl"></player>
</body>
Getting ref to components controller isn't new, directives back in the day allowed it and that wasn't a problem at all, it's necessary to have such feature, ng-ref is just a helper for you to do this from the template side (the same way angular 2+ does).
Nevertheless, if you need the child components ready you should use $postLink() instead of $onInit. $postLink is called after the component is linked with his children, which means, the ng-ref will be ready when it gets called.
So all you have to do is change your onInit like so:
̶$̶o̶n̶I̶n̶i̶t̶(̶)̶ ̶{̶
$postLink() {
console.log("onInit", this.pl);
this.initPL = this.pl || 'undefined';
this.sample = this.pl || 'undefined';
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
}
$postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs. This hook can be considered analogous to the ngAfterViewInit and ngAfterContentInit hooks in Angular. Since the compilation process is rather different in AngularJS there is no direct mapping and care should be taken when upgrading.
Ref.: Understanding Components
The full working snippet can be found bellow (I removed all console.log to make it clearer):
angular.module("app",[])
.controller("ctrl", class ctrl {
constructor() {
//console.log("construct")
}
$postLink() {
//console.log("onInit", this.pl);
this.initPL = this.pl || 'undefined';
this.sample = this.pl || 'undefined';
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
}
})
.component("player", {
template: `
<fieldset>
$ctrl.box1={{$ctrl.box1}}<br>
$ctrl.box2={{$ctrl.box2}}<br>
</fieldset>
`,
controller: class player {
constructor() {
//console.log("player",this);
}
$onInit() {
//console.log("pl.init", this)
this.box1 = true;
this.box2 = false;
}
},
})
<script src="//unpkg.com/angular#1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
Initial vm.pl = {{vm.initPL}}<br>
Sample = {{vm.sample}}<br>
<button ng-click="vm.getSample()">Get Sample</button>
<br>
<input type="checkbox" ng-model="vm.pl.box1" />
Box1 pl.box1={{vm.pl.box1}}<br>
<input type="checkbox" ng-model="vm.pl.box2" />
Box2 pl.box2={{vm.pl.box2}}<br>
<player ng-ref="vm.pl"></player>
</body>
Parent controller initialization happens before the initialization of the player controller, so that is why we have initPL as undefined in the first $onInit.
Personally, I would prefer to define and load the data that should be passed down to the nested components on the parent controller initialization instead of setting the parent's initial data from its child's. But still if we will need this we can do it on the child's component initialization using bindings and callbacks. Probably it looks more like a dirty workaround, but it could work in such scenarios, here is the code:
angular.module("app",[])
.controller("ctrl", class ctrl {
constructor() {
console.log("construct")
}
$onInit() {
console.log("onInit", this.pl);
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
this.onPlayerInit = (pl) => {
console.log("onPlayerInit", pl);
this.initPL = pl || 'undefined';
this.sample = `[${pl.box1},${pl.box2}]`;
}
}
})
.component("player", {
bindings: {
onInit: '&'
},
template: `
<fieldset>
$ctrl.box1={{$ctrl.box1}}<br>
$ctrl.box2={{$ctrl.box2}}<br>
<h3>Player</h3>
</fieldset>
`,
controller: class player {
constructor() {
console.log("player",this);
}
$onInit() {
console.log("pl.init", this)
this.box1 = true;
this.box2 = false;
if (angular.isFunction( this.onInit() )) {
this.onInit()(this);
}
}
},
})
<script src="//unpkg.com/angular#1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
Initial vm.pl = {{vm.initPL}}<br>
Sample = {{vm.sample}}<br>
<button ng-click="vm.getSample()">Get Sample</button>
<br>
<input type="checkbox" ng-model="vm.pl.box1" />
Box1 pl.box1={{vm.pl.box1}}<br>
<input type="checkbox" ng-model="vm.pl.box2" />
Box2 pl.box2={{vm.pl.box2}}<br>
<br>
<player ng-ref="vm.pl" on-init="vm.onPlayerInit"></player>
</body>
I am in the process of writing some unit tests following the conventions in this blog post but I am having trouble accessing my directive's controller functions. I have a directive written with ES6 classes for the directive and the controller. I am using controllerAs to bind my controller class to my directive class. The directive I am trying to write unit tests for looks like so:
// Why is this file included? | Refer to : http://www.michaelbromley.co.uk/blog/350/exploring-es6-classes-in-angularjs-1-x#_section-directives
import directiveFactory from '../../../directivefactory.js';
// ##Directive Definition
class sideNav {
constructor() {
this.template =
`
<!-- SIDENAV -->
<!-- hamburger menu toggle visible when the sidenav menu is toggled shut -->
<span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger dark-hamburger" set-class-when-at-top="fix-to-top" ng-click='vm.test(); vm.toggle();'></span>
<!-- wraps all sidenav menu content -->
<div ng-class='{ show: vm.open }' class="collapsible">
<!-- hamburger menu toggle visible when the sidenav menu is toggled open -->
<span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger light-hamburger" ng-click='vm.test(); vm.toggle();'></span>
<!-- brand-image -->
<div class="side-nav-head" transclude-id="head"></div> <!-- component user content insertion point 1 -->
<!-- navigation links -->
<div class="side-nav-body" transclude-id="body"></div> <!-- component user content insertion point 2 -->
<!-- footer -->
<footer>
</footer>
</div><!-- end collapsible -->
<!-- END SIDENAV -->
`;
this.restrict = 'E';
this.scope = {};
this.bindToController = {
};
this.transclude = true;
this.controller = SideNavController;
this.controllerAs = 'vm';
}
// ###Optional Link Function
link (scope, elem, attrs, ctrl, transclude) {
transclude ((clone) => {
angular.forEach(clone, (cloneEl, value) => {
// If the cloned element has attributes...
if(cloneEl.attributes) {
// Get desired target ID...
var tId = cloneEl.attributes["transclude-to"].value;
// Then find target element with that ID...
var target = elem.find('[transclude-id="' + tId + '"]');
// Append the element to the target
target.append(cloneEl);
}
});
});
}
}
// ###Directive Controller
class SideNavController {
constructor($rootScope) {
this.$rootScope = $rootScope;
// Initiate the menu as closed
this.open = false;
// Upon instantiation setup necessary $rootScope listeners
this.listen();
}
// ####listen()
// *function*
// Setup directive listeners on the $rootScope
listen () {
// Receives an event from the ng-click within the directive template
// for the side-nav-item component
this.$rootScope.$on('navigation-complete', (event) => {
// Upon receiving event, toggle the menu to closed
this.toggle();
});
}
// ####toggle()
// *function*
// Toggle menu open or shut
toggle() {
this.open = !this.open;
}
// ####test()
// *function*
test() { // DEBUG
console.log('tester'); // DEBUG
console.log(this.visible); // DEBUG
console.log(this.open); // DEBUG
}
}
SideNavController.$inject = ['$rootScope'];
export default ['sideNav', directiveFactory(sideNav)];
I take this file and import it along with one other directive component to create a module like this:
import { default as sideNav } from './side-nav/side-nav.js';
import { default as sideNavItem } from './side-nav-item/side-nav-item.js';
let moduleName = 'sideNav';
let module = angular.module(moduleName, [])
// #### Sidebar Nav Components
.directive(...sideNav)
.directive(...sideNavItem);
export default moduleName;
In my unit test I try to mock the controller in beforeEach but no matter if I use the controller name as vm or SideNavController (the former being the controllerAs name and the latter being the actual class name—not truly sure which is the one I want) I still receive the error: Error: [ng:areq] Argument 'vm/SideNavController' is not a function, got undefined.
This is my unit test:
describe('Side Nav Directive', () => {
let elem, scope, ctrl;
// Mock our side-nav directive
beforeEach(angular.mock.module('sideNav'));
beforeEach(angular.mock.inject(($rootScope, $compile, $controller) => {
// Define the directive markup to test with
elem = angular.element(
`
<div>
<!-- side-nav directive component -->
<side-nav>
<!-- content insertion point 1 -->
<div transclude-to="head">
<img src alt="test_image">
</div>
<!-- content insertion point 2 -->
<div transclude-to="body">
Test Link
</div>
</side-nav>
</div>
`
);
scope = $rootScope.$new();
$compile(elem)(scope);
scope.$digest();
ctrl = $controller('vm', scope);
}));
it("should toggle shut when angular view navigation completes", () => {
expect(ctrl).toBeDefined(); // <----- this errors
});
});
I am truly stumped after referring to many tutorials and blog posts and could really use some insight!
I would actually suggest a slightly different approach to the testing. If the goal is to test the the logic in SideNavController I would consider moving that class to it's own file. That way you can import it in both the directive and the test. Structuring it that way gives you a lot easier access to it since you can test it in complete isolation from the directive itself.
Testing this by compiling markup and creating the entire directive basically turns this into an integration test and is a bit more complex to manage.
In general I find that this makes for more maintainable and useful tests - especially if the goal is to test the controller.
It would be similar to my example here: http://www.syntaxsuccess.com/viewarticle/writing-jasmine-unit-tests-in-es6
The solution I implemented is based on the response TGH made to my original question. I now have the controller class stored in a separate file, which creates a separation of concerns between the directive transclusion functionality and the directive controller functionality. The directive controller file looks like this:
const ROOTSCOPE = new WeakMap();
// ###Directive Controller
class SideNavController {
constructor($rootScope) {
ROOTSCOPE.set(this, $rootScope);
// Initiate the menu as closed
this.open = false;
// Upon instantiation setup necessary $rootScope listeners
this.listen();
}
// ####listen()
// *function*
// Setup directive listeners on the $rootScope
listen () {
// Receives an event from the ng-click within the directive template
// for the side-nav-item component
ROOTSCOPE.get(this).$on('navigation-complete', (event) => {
// Upon receiving event, toggle the menu to closed
this.toggle();
});
}
// ####toggle()
// *function*
// Toggle menu open or shut
toggle() {
this.open = !this.open;
}
}
SideNavController.$inject = ['$rootScope'];
export default SideNavController;
The test file imports the class from the appropriate file and mocks the controller for the test in a beforeEach block. The controller's variables and functions are all available:
// Import the controller to be tested
import SideNavController from './SideNavController.js';
describe('SideNavController', () => {
let $rootScope, vm;
beforeEach(angular.mock.inject((_$rootScope_, _$controller_) => {
$rootScope = _$rootScope_.$new();
vm = _$controller_(SideNavController, {} );
}));
it('should be initialize the open variable to false', () => {
expect(vm.open).toBeDefined();
expect(vm.open).toBeFalsy();
});
it('should listen for a navigation event and call the toggle function when the event is caught', () => {
// Create Jasmine spy to watch toggle function
spyOn(vm, 'toggle');
// Simulate navigation event propagation
$rootScope.$emit('navigation-complete');
expect(vm.toggle).toHaveBeenCalled();
});
});
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.
I want to have element where i can have 2 views using their own controller but only one at a time.
I can't use a ng-view and use the routeProvider because in the future I need to include more ng-includes that need to change their content depending on the possible actions.
I created a fiddle http://jsfiddle.net/EvHyT/29/.
So I used a ng-include and then I set the src for it from a main controller. At that point I want to use controller 1 or controller 2.
function MainCtrl($rootScope, $scope, navService){
$scope.template = {};
$scope.loadCtrl1=function(param){
navService.loadCtrl1(param);
}
$scope.loadCtrl2=function(param){
navService.loadCtrl2(param);
}
$rootScope.$on('loadCtrl1', function(e, args){
$scope.template = {'url': 'temp1'};
});
$rootScope.$on('loadCtrl2', function(e, args){
$scope.template = {'url': 'temp2'};
});
}
I use a service for communication because i want to move the load controller functions in a child controller.
var myApp = angular.module('myApp',[]);
myApp.factory('navService', function($rootScope) {
return {
loadCtrl1:function(param){
$rootScope.$broadcast('loadCtrl1', {'id':param});
},
loadCtrl2:function(param){
$rootScope.$broadcast('loadCtrl2', {'id':param});
}
};
});
I know this solution is bad because the controllers are not yet created when a different template is inserted so my event listener will not fire. Also can I destroy the previous instances of the controller because switching between the two controllers makes my event fire multiple times.
function Child1Ctrl($scope, $rootScope){
$rootScope.$on('loadCtrl1', function(e, args){
alert(args.id);
});
}
function Child2Ctrl($scope, $rootScope){
$rootScope.$on('loadCtrl2', function(e, args){
alert(args.id);
});
}
You don't need to broadcast (and shouldn't be broadcasting) to make this happen.
In my experience, if you're broadcasting on the rootScope, you're probably working too hard.
A simpler way of loading the template is very similar to what you're doing:
my.NavService = function() {
this.template = 'index.html';
this.param = null;
};
my.NavService.prototype.setTemplate(t, p) {
this.template = t;
this.param = p;
};
my.ctrl = function($scope, nav) {
$scope.nav = nav;
$scope.load = function(t, p) {
nav.setTemplate(t, p);
};
};
my.ctrl1 = function($scope, nav) {
$scope.param = nav.param;
};
my.ctrl2 = function($scope, nav) {
$scope.param = nav.param;
};
module.
service('nav', my.NavService).
controller('mainCtrl', my.ctrl).
controller('ctrl1', my.ctrl1).
controller('ctrl2', my.ctrl2);
<script type="text/ng-template" id="temp1.html">
<div ng-controller="ctrl1">Foo {{param}}.</div>
</script>
<script type="text/ng-template" id="temp2.html">
<div ng-controller="ctrl2">Bar {{param}}.</div>
</script>
<div ng-controller="mainCtrl">
<a ng-click="load('temp1.html', 16)">Load 1</a>
<a ng-click="load('temp2.html', 32)">Load 2</a>
<div ng-include src="nav.template"></div>
</div>
A setup like this would work much better for you.
Alternatively, you should look into selectively showing elements with ng-switch. Unlike ng-show/hide, ng-switch does not simply add "display:none" to the CSS. It removes it from the DOM.
Some notes:
The above example may not be a working example, it's a demonstration of the idea.
A working example can be seen here: http://jsbin.com/ofakes/1 It modifies your original code.
JSFiddle had some issues with loading the include from the on page script
tag.
JSBin was a little better.
I didn't really get it to work as expected until I broke out the templates
into their own files.