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.
Related
I currently have a bootstrap accordion with an ng-click event set on the my panel headings that passes in a string like so:
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" ng-click="setActiveAccordion('accordionOne')">
In the panel content, on the element with the directive, I have
<div my-named-directive active-accordion="activeAccordion">
My controller to control the active accordion functionality looks like this:
.controller('accordionPanelController', ['$scope', function(scope){
$scope.activeAccordion = 'null';
//no accordion open by default
$scope.setActiveAccordion = function(accordionName){
$scope.activeAccordion = accordionName;
};
}]);
In the child directive, my-named-directive:
.directive('myNamedDirective', function(){
return {
scope: {
activeAccordion: "="
},
controller: function ($scope) {
//a bunch of irrelevant stuff
$scope.$watch('activeAccordion', function (newValue){
if(newValue === "accordionOne"){
//do stuff
}
}
}
}
});
I am aware of a couple issues here. First, it seems like the $watch is expensive and unnecessary. More importantly, however, I take an issue with the child wanting/caring about a property on a parent object, rather than the parent dictating what the child does.
Can someone help me understand what a better approach to this in angular is?
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.
Was told to create service/factory and use it on global controller. It's my first time hearing global controller. Anyways, I created a factory called Scroll and injected it on our main.controller.js. The function in the factory is isScrollingEnabled which returns true or false. The other function is setScrolling which will set the variable to true or false. The default value is set to true.
Inside the main controller, I have this code
$scope.scrollEnabled = Scroll.isScrollingEnabled();
console.log('value of $scope.scrollEnabled', $scope.scrollEnabled);
That code spits out true in the console which is good.
on my template, I'm using it this way. I have button that sets scrolling to false. The value in the console spits out false which is good.
<body ng-class="{ 'scrolling' : scrollEnabled }">
However, it's not working. If I change it to the code written below, it works
<body ng-class="{ 'scrolling' : false }">
So I guess, it's not in scope especially ui-view is in index.html and main.controller.js and main.html will be loaded in the ui-view. The < body > is before this which tells me, any scope inside main.controller.js will not work outside of ui-view.
So what's the solution for this?
Sorry for not posting the factory. Here it is
.factory('Scroll', function() {
var scrollEnabled = true; // i then changed it to false hoping it will work, it didn't
var ScrollEvent = {
isScrollingEnabled: function () {
return scrollEnabled;
},
disablePageScrolling: function() {
scrollEnabled = false;
}
};
return ScrollEvent;
});
The $scope of the controller you're attaching the value to doesn't extend to the <body> element. Instead, you can whip together a directive:
.directive('shouldScroll', function (Scroll) {
return {
restrict: 'A',
link: function ($scope, elem) {
$scope.$watch(Scroll.isScrollingEnabled, function (n) {
if (n) {
elem.addClass('scrolling');
} else if (n === false) {
elem.removeClass('scrolling');
}
});
}
};
});
You'd attach it to body like so:
<body should-scroll>
Another solution that works in some cases is just to use class rather than ng-class:
<body class="{{ scrollEnabled ? 'scrolling' : '' }}">
I have an AngularJS Application with a scroll directive implemented as the following:
http://jsfiddle.net/un6r4wts/
app = angular.module('myApp', []);
app.run(function ($rootScope) {
$rootScope.var1 = 'Var1';
$rootScope.var2 = function () { return Math.random(); };
});
app.directive("scroll", function ($window) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= 100) {
scope.scrolled = true;
} else {
scope.scrolled = false;
}
scope.$apply();
});
};
});
The HTML looks the following:
<div ng-app="myApp" scroll ng-class="{scrolled:scrolled}">
<header></header>
<section>
<div class="vars">
{{var1}}<br/><br/>
{{var2()}}
</div>
</section>
</div>
I only want the class scrolled to be added to the div once the page is scrolled more than 100px. Which is working just fine, but I only want that to happen! I don't want the whole scope to be re-rendered. So the function var2() should not be executed while scrolling. Unfortunately it is though.
Is there any way to have angular only execute the function which is bound to the window element without re-rendering the whole scope, or am I misunderstanding here something fundamentally to AngularJS?
See this fiddle:
http://jsfiddle.net/un6r4wts/
Edit:
This seems to be a topic about a similar problem:
Angularjs scope.$apply in directive's on scroll listener
If you want to calculate an expression only once, you can prefix it with '::', which does exactly that. See it in docs under One-time binding:
https://docs.angularjs.org/guide/expression
Note, this requires angular 1.3+.
The reason that the expressions are calculated is because when you change a property value on your scope, then dirty check starts and evaluates all the watches for dirty check. When the view uses {{ }} on some scope variable, it creates a binding (which comes along with a watch).
To set the stage - this is not happening within a single scope, where I can bind a simple attribute. The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope). Further, the button that's clicked is in a child scope about 3 children deep from root.
Here is how I'm currently solving this:
HTML (sitting in root scope):
<ul class="nav-secondary actions"
darthFader fadeDuration="200"
fadeEvent="darthFader:secondaryNav">
Where darthFader is my directive.
Directive:
directive('darthFader',
function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.$on(attrs.fadeevent, function(event,options) {
$(element)["fade" + options.fade || "In"](attrs.fadeduration || 200);
});
}
}
})
So here I'm creating an event handler, specific to a given element, that is calling fadeIn or fadeOut, depending on an option being passed through the event bus (or defaulting to fadeIn/200ms).
I am then broadcasting an event from $rootScope to trigger this event:
$rootScope.$broadcast('darthFader:secondaryNav', { fade: "Out"});
While this works, I'm not crazy about creating an event listener for every instance of this directive (while I don't anticipate having too many darthFader's on a screen, it's more for the pattern I would establish). I'm also not crazy about coupling my attribute in my view with an event handler in both my controller & directive, but I don't currently have a controller wrapping the secondary-nav, so I'd have to bind the secondaryNav to $rootScope, which I don't love either. So my questions:
Is there a way to do this without creating an event handler every time I instantiate my directive? (maybe a service to store a stateful list of elements?)
How should I decouple my view, controller & directive?
Any other obvious questions I'm missing?
Cheers!
You mention in your question
The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope).
I believe if I were to write this same functionality, I would put the element in its own controller--controllers are responsible for managing the intersection of the view and the model, which is exactly what you're trying to do.
myApp.controller('NavController', function($scope) {
$scope.fadedIn = false;
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="fadedIn">
myApp.directive('darthFader', function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
var duration = attrs.fadeDuration || 200;
$scope.$watch(attrs.fadeShown, function(value) {
if (value)
$(element).fadeIn(duration);
else
$(element).fadeOut(duration);
});
}
};
});
If you're worried about sharing the fade in/out state between multiple controllers, you should create a service to share this state. (You could also use $rootScope and event handlers, but I generally find shared services easier to debug and test.)
myApp.value('NavigationState', {
shown: false
});
myApp.controller('NavController', function($scope, NavigationState) {
$scope.nav = NavigationState;
});
myApp.controller('OtherController', function($scope, NavigationState) {
$scope.showNav = function() {
NavigationState.shown = true;
};
$scope.hideNav = function() {
NavigationState.shown = false;
};
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="nav.shown">
<!-- ..... -->
<div ng-controller="OtherController">
<button ng-click="showNav()">Show Nav</button>
<button ng-click="hideNav()">Hide Nav</button>
</div>
Create a custom service, inject it in the controller. Call a method on that service that will do the fade-in/fade-out etc. Pass a parameter to convey additional information.