Elegant burger menu directive - angularjs

I have started to develop a burger module, consisting essentially in 2 parts :
a "burger-opener" button which opens the menu, most probably an attribute directive including a click event listener, dom and css agnostic
a "burger menu" element, most probably a directive benefiting from transclusion, letting the client decide what the menu contains for the sake of reusability. This basically provides a close button at the top of it, before the ng-transclude element.
There must be a tight relationship between those 2 elements in terms of functionality, i.e the button element will call "open" into the burger menu element.
The thing is, I have a constraint which is that the button and the menu do not have to be contained within each other. For example, one must be able to use the module like so
<ul burger-menu>
<li>Save</li>
<li>Load</li>
</ul>
<section id="container">
<a href="" burger-opener class="burgerOpen"><a>
</section>
This constraint seems to be auto-excluding directive to directive communication using the "require" syntax because this angularjs functionality supposes directives are self-contained. So unless I create a top level DOM controller containing my 2 elements... I'm stuck.
I have been using a brute force approach, that is to use a broadcast from the rootscope for the button to send the "open" message to the menu directive. It works like a charm but I am not satisfied with it.
One other approach would be to set an even on the button but I would take this as a failure for some weird reason. I'm probably wrong but I'm quite sure there is a more elegant way to connect those two elements using the AngularJS paradigm without using broadcast nor events.
Do you know it ? I guess basically I am asking how components such as ui bootstrap modal service actually work.

Here is what I came up to. This seems quick and reusable enough to me, let me know if you can create something better !
Basically, the burgerMenu directive shares its parent scope (scope:false or nothing, it's false by default) and sets an api within it using the 'controller as' syntax. Thus the button whose role is to open the menu has a clear click handler with burgerCtrl.openBurger().
Here is the burgerMenu directive :
angular.module('app')
.directive("burgerMenu", [function () {
return {
scope: false,
controller: function () {
var self = this;
this.openBurger = function () {
self.isOpen = true;
};
this.closeBurger = function () {
self.isOpen = false;
};
this.isOpen = false;
},
controllerAs: 'burgerCtrl',
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'js/app/burgerMenu/_burger.tpl.html'
}
}
]);
The template :
<section class="nav_bar" ng-class="{open:burgerCtrl.isOpen}">
<div class="nav_content" ng-show="burgerCtrl.isOpen">
<h1 ng-click="burgerCtrl.closeBurger();">X</h1>
<ng-transclude></ng-transclude>
</div>
</section>
Css (main idea) :
.nav_bar { position:fixed; }
.nav_bar.open { width: 240px; }
Usage :
<section id="header">
<div class="burger" ng-click="burgerCtrl.openBurger()"></div>
<h1>App title</h1>
</section>
<section data-burger-menu>
<ul id="menu">
<li>Save</li>
<li>Share</li>
<li>Load n°1</li>
</ul>
</section>

Related

AngularJS component nesting

I created a component search-context, which works well. It's configurable and it does what it's supposed to do.
<search-context context-name="Groups"
compare-columns="['displayName']"
search-manager="$ctrl"
query-url="/group/search/{{contextId}}"
icon="fa fa-users"
on-resolve-item-url="resolveItemUrl(row)"></search-context>
Here it is in action, standalone.
There are various other search contexts, and I'd like to create a search-manager component such that I can write markup like this:
<search-manager>
<search-context context-name="Devices"
compare-columns="['displayName']"
search-manager="$ctrl"
query-url="/device/search/{{contextId}}"
icon="fa fa-laptop"></search-context>
<search-context context-name="Groups"
compare-columns="['displayName']"
search-manager="$ctrl"
query-url="/group/search/{{contextId}}"
icon="fa fa-users"
on-resolve-item-url="resolveGroupEditUrl(row)"></search-context>
</search-manager>
The general plan is for search-context to check whether it has a search-manager and if so suppress its own input/button controls, and the search-manager will supply input controls and supply the search term to the search contexts.
The examples in the AngularJS component documentation demonstrate dynamic child controls using ng-repeat in the control template, but it's not clear how to set things up to handle explicit markup such as I propose. If at all possible I'd prefer not to need to explicitly specify the search-manager="$ctrl" parent reference.
How does one go about this and what are the supporting topics one must research and understand? Just the key concept names would be a big help but an overview and a further-reading list would be awesome.
My first attempt at the template for search-manager looks like this
<div>
<div class="panel-heading">
<h3 class="panel-title">Search</h3>
<div class="input-group">
<input class="form-control" ng-model="$ctrl.term" />
<span class="input-group-btn" ng-click="$ctrl.search()">
<button class="btn btn-default">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="panel-body">
<ng-transclude></ng-transclude>
</div>
</div>
The code looks like this
function SearchManagerController($scope, $element, $attrs, $http) {
var ctrl = this;
ctrl.searchContext = [];
ctrl.registerSearchContext = function (searchContext) {
ctrl.searchContext.push(searchContext);
}
ctrl.search = function () {
ctrl.searchContext.forEach(function (searchContext) {
searchContext.search(ctrl.term);
});
};
}
angular.module("app").component("searchManager", {
templateUrl: "/app/components/search-manager.html",
controller: SearchManagerController,
transclude: true,
bindings: {
term: "#"
}
});
The child components are transcluded but they need a reference to the search-manager component, and $ctrl is not in scope.
How do we get a reference to the parent?
To obtain a reference to the parent all you need to do is require the parent in the search-context declaration. The double caret prefix means to search the parents. Single caret starts with the current object which will work but is slightly less efficient. The question mark means don't barf if you can't find it, just return undefined. This is necessary when the component may not always be parented by a search manager.
angular.module("app").component("searchContext", {
templateUrl: "/app/components/search-context.html",
controller: SearchContextController,
require: {
searchManagerCtrl: "?^^searchManager"
},
bindings: {
...
}
});
But what if you need the parent to have references to the children?
In the SearchContextController we implement the $onInit lifecycle event handler.
ctrl.$onInit = function () {
if (ctrl.searchManagerCtrl) {
ctrl.searchManagerCtrl.registerSearchContext(ctrl);
}
};
registerSearchContext is a method defined in the parent's controller for this purpose. The implementation essentially pushes each registered control into an array property we define on its scope, and then methods of the parent can enumerate the children.
For a directive this require trick is expressed slightly differently. You must declare the property searchManagerCtrl in the directive scope, and supply the expression directly as the value of require.
require: "?^^searchManager",
You must also supply a link function. One of the parameters of a link function is controller and a reference to searchManager will be passed in this parameter, at which point you can assign it to a property of the directive scope. The $onInit lifecycle event is still available for registering with the search manager, but refers to $scope rather than ctrl.

Expose element of parent component to child

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

How to watch bootstrap accordion with angular in the least expensive manner

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?

angularjs: click on a tag to invoke a function and pass the current tag to it

I need to handle a click on a tag that enables the opening of a popover.
I try to figure out the best way to do this with angularjs and naturally used hg-click.
<div ng-repeat="photo in stage.photos"
ng-click="openPopoverImageViewer($(this))"
>
$scope.openPopoverImageViewer = function (source) {
alert("openPopoverImageViewer "+source);
}
The issue is that I cannot manage to pass the $(this) to it.
Q1) How to pass the jQuery element?
Q2) In addition, ng-click sounds
to require the function being part of the controller: is it possible
to invoke a function in the partial instead?
You need to stop "thinking in jQuery" :)
Like #oori says, you can pass in photo.
Or better yet, create a custom directive. Directives is the way to go when you need new functionality in your dom, like an element that you can click to open an overlay. For example:
app.directive('popOver', function() {
return {
restrict: 'AE',
transclude: true,
templateUrl: 'popOverTemplate.html',
link: function (scope) {
scope.openOverlay = function () {
alert("Open overlay image!");
}
}
};
});
You can then use this as a custom elemen <pop-over> or as an attribute on regular HTML elements. Here is a plunker to demonstrate:
http://plnkr.co/edit/P1evI7xSMGb1f7aunh3G?p=preview
Update: Just to explain transclusion: When you say that the directive should allow transclusion (transclude:true), you say that the contents of the tag should be sent on to the directive.
So, say you write <pop-over><span>This will be passed on</span></pop-over>, then the span with "This will be passed on" is sent to the directive, and inserted wherever you put your ng-transclude element in your template. So if your pop-over template looks something like this:
<div>
<ng-transclude/>
</div>
Then your resulting DOM after the template has compiled will look like this:
<div>
<span>This will be passed on</span>
</div>
Pass it "photo"
<div ng-repeat="photo in stage.photos" ng-click="openPopoverImageViewer(photo)">
or the current $index
<div ng-repeat="photo in stage.photos" ng-click="openPopoverImageViewer($index)">

parent directive erasing child ng-repeat

I am learning angularjs through the process of taking an existing site that was built primarily with JQuery and trying to "angularize" it. I am having trouble reproducing the same functionality in angular.
Please see the following plunker.
http://plnkr.co/edit/n4cbcRviuzNsieVvr4Im?p=preview
I have a ul element with an angularjs directive called "scroller" as seen below.
<ul class="dropdown-menu-list scroller" scroller style="height: 250px">
<li data-ng-repeat="n in notifications">
<a href="#">
<span class="label label-success"><i class="icon-plus"></i></span>
{{n.summary}}
<span class="time">{{n.time}}</span>
</a>
</li>
</ul>
The scroller directive looks like this:
.directive('scroller', function () {
return {
priority: 0,
restrict: 'A',
scope: {
done: '&',
progress: '&'
},
link: function (scope, element, attrs) {
$('.scroller').each(function () {
var height;
if ($(this).attr("data-height")) {
height = $(this).attr("data-height");
} else {
height = $(this).css('height');
}
$(this).slimScroll({
size: '7px',
color: '#a1b2bd',
height: height,
disableFadeOut: true
});
});
}
};
What i want to happen is that the ng-repeat executes on the notifications array in the controller, producing a collection of li elements that exceed 250px therefore a slimscrollbar would be added. What actually happens is the result of the ng-repeat is not included in the final DOM. I believe the call in the parent scroller directive of $(this).slimScroll() is called after the ng-repeat executes and replaces the DOM. If i remove the scroller attribute, the li elements show up.
I am sure there is a strategy for this and am hoping the community can educate me on a better approach or alternate approach. thoughts? again the plunker is here.
http://plnkr.co/edit/n4cbcRviuzNsieVvr4Im?p=preview
Thanks,
Dan
The issue is actually your directive scope. You are using an explicit object as the scope, which means you are isolating the scope, which means the directive scope isn't inheriting from its parent anymore. So notifications from the parent controller is no longer reachable from the directive scope (and therefore any elements inside of its element).
If you remove this from your directive it should work:
scope: {
done: '&',
progress: '&'
}
I notice that you aren't using those attributes anyway so it shouldn't break any other functionality.
Look at the API docs http://docs.angularjs.org/guide/directive and look for isolate scope for more details.
An alternative to what you're trying to do would just be something like this
scope.$watch(attr.done, function(val) { //do something when the value changes })
Since I don't know your use case I can't say what the best solution would be.

Resources