Dynamic menu bar with angularjs - javascript

I'm trying to create a menu bar using Angularjs. I've done similar things before with Backbonejs, but I have a hard time getting my head around how to do this with angular.
In my html file, I have the following menu placeholder.
<div id='menu1'></div>
<div id='menu2'></div>
<div id='menu3'></div>
<div id='menu4'></div>
<div id='menu5'></div>
A number of my angular modules add a menu when they are loaded (in run). Each of them only reserves a particular slot (i.e. menu1..5), so they don't clash. When some modules aren't loaded, their menu would not show in the menu bar.
An angular module would conceptually look like:
angular.module('myModule3', [])
.service('someService', function($http) {
// get some data to populate menu (use $http)
this.menuItems = ['orange', 'apple', 'banana']
})
.run(['someService', function(someService) {
// create a rendered menu item
...
// insert it at id="menu3"
})
For sake of simplicity, the rendered menu item should look like:
<ul>
<li>organge</li>
<li>apple</li>
<li>banana</li>
</ul>
I'm fairly new to angular, so I don't really know where to begin. I've been reading up on directives, but don't see how they fit in here, as they require some custom markup (maybe a custom menu tag containing the DOM target (i.e. menu..5). Also, how to connect this to a controller is not clear to me.
Update
In addition to the above base template (containing arbitrary anchor points in the DOM) and the directive (which will produce a DOM element which will be inserted at these anchor points), a template will facilitate the creation of the DOM element. This template will be located in a separate file containing the position the directive's DOM element will be inserted to (as opposed to the usual case of directives in which an already existing tag will be replaced/inserted into specific markup that matches the directive's definition:
<menu ng-model="Model3DataService" target="#menu3">
<ul>
<li ng-repeat="for item in items"></li>
</ul>
</menu>
Again, coming from a Backbone/jquery background this makes sense, but this may not be the right thing to do in angular. If so, please let me know how I could keep the base template free of any knowledge about the modules and assumptions of where they put their menu (i.e. which slot of the menu bar they allocate). I'm happy to hear about other solutions...

Each module should have its menu loader defined:
angular.module('module1', []).
factory('module1.menuLoader', function() {
return function(callback) {
callback(['oranges', 'bananas'])
}
});
Your application should contain menu directive which can load menu items for any module only if exists.
angular.module('app', ['module1']).
directive('menu', ['$injector', function($injector) {
return {
restrict: 'A',
template:
'<ul><li ng-repeat="item in items">{{item}}</li></ul>',
scope: {},
link: function($scope, $element, $attrs) {
var menuLoaderName = $attrs.menu+'.menuLoader';
if ($injector.has(menuLoaderName)) {
var loaderFn = $injector.get(menuLoaderName);
loaderFn(function(menuItems) {
$scope.items = menuItems;
});
}
}
};
}]);
Final html:
<div class="content">
<div menu="module1"></div>
<div menu="module2"></div>
<div menu="module3"></div>
</div>
After running the application only module1 menu will be loaded. Other menu placeholders remain empty.
Live demo: http://plnkr.co/edit/4tZQGSkJToGCirQ1cmb6
Updated: If you want to generate markup on the module side the best way is to put the template to the $templateCache in the module where it's defined and then pass the templateName to the application.
angular.module('module1', []).
factory('module1.menuLoader', ['$templateCache', function($templateCache) {
$templateCache.put('module1Menu', '<ul><li ng-repeat="item in items">{{item}}</li></ul>');
return function(callback) {
callback('module1Menu', ['oranges', 'bananas'])
}
}]);
angular.module('app', ['module1'])
.directive('menu', ['$injector', function($injector) {
return {
restrict: 'A',
template:
'<div ng-include="menuTemplate"></div>',
scope: {},
link: function($scope, $element, $attrs) {
var menuLoaderName = $attrs.menu+'.menuLoader';
if ($injector.has(menuLoaderName)) {
var loaderFn = $injector.get(menuLoaderName);
loaderFn(function(menuTemplate, menuItems) {
$scope.menuTemplate = menuTemplate;
$scope.items = menuItems;
});
}
}
};
}]);

Related

AngularJs Directive update

I want to update my directive content only at some desired places, but not at others. I have simulated my problem here:
http://jsfiddle.net/Lvc0u55v/2945/
The problem is, I have an 'editor' directive which is applied in two places:
<span class="editor1" editor ></span>
<span class="editor2" editor ></span>
I want to update the content of span class="editor1" on button click.
How do I do it?
Why not go with a relatively Angularesque approach by isolating the scope of the directive and to a maximum extent, avoid jQuery in your logic.
So you could have your directive defined as such:
.directive('editor', function() {
return {
scope: {
upd : '=',
editordata : '=data'
},
template: '<div>{{editordata}}</div>',
controller: function($scope, $rootScope, $element) {
$rootScope.$on('update', function(evt, data) {
if(data.upd === $scope.upd){
$scope.editordata = data.txt;
}
})
},
link: function(scope, el, attr) {}
}
})
Here, you are passing the required information which the editor directive depends upon through its scope, namely upd (which I suppose is how you want to uniquely identify the items by) and the text data.
Meanwhile, you can define a list of the editor items in the common parent controller MyCtrl and iterate over them in the DOM with ng-repeat.
// MyCtrl controller
$scope.list = [
{upd: 'editor1', data: 'original data for editor1'},
{upd: 'editor1', data: 'original data for editor2'}
]
<!-- HTML -->
<div ng-repeat="item in list" upd="item.upd" data="item.data"></div>
Demo
You can check if the current directory has the "editor1" class and if so complete your logic.
You can look at this example :
element[0].querySelector('.editor1') !== undefined'

Repeating a directive with transclude that is different each time

In my angular application I have several panels which have a shared structure, identical html.
Inside the panel the content and behavior changes, basically each one is a separate directive, let's call them panel-content.
This is the close to the solution I think is optimal but I have some architectural doubts.
Since I have directive (which has transclude true set):
<panel></panel>
It's template looks like this:
<div>
Content
<ng-transclude></ng-transclude>
</div>
I have to repeat the panels
<ul>
<li ng-repeat="panel in panels">
<panel>
<panel-content></panel-content>
</panel>
</li>
</ul
This is all fine, but what would be a reasonably good way of "choosing" on each iteration which <panel-content> should I show?
Lets say I have a panel.id I can use.
I notice I can achieve it in several ways, I could do an ng-switch inside the <panel-content> view using the panel id, I could set up that the templateUrl of has a dynamic part and links to different URLs depending on the panel.id.
But for some reason, I am convinced I am missing something nicer more straightforward?
Please not that this architecture is not set in stone, if there is another structure that would better fit my needs please let me know.
So, the question again is, how do I choose? Or rather, who is responsible for choosing which <panel-content> should be displayed.
If I understand you right, I would use ng-include in directive that changes each time the template by id:
Something like:
<ul>
<li ng-repeat="panel in panels">
<panel type-id="panel.id">
</panel>
</li>
</ul>
and Directive:
app.directive('panel',
function() {
return {
restrict: 'E',
replace: true,
template: '<ng-include src="getTemplateUrl()"/>',
link: function(scope,elem,attrs) {
scope.getTemplateUrl = function() {
var url;
if(attrs.typeId !== undefined){
var url = $rootScope.list_templates[attrs.typeId].value; // TODO: add validation
}
return url;
};
}
});
app
$rootScope.list_templates = [
{ id: 0, value: 'partials/upcoming_0.html'},
{ id: 1, value: 'partials/upcoming_1.html'},
{ id: 2, value: 'partials/upcoming_2.html'}
];

AngularJS update controller variable and render the view again?

I have a webpage with a very simple structure, imagine left navigation, header bar and content area. If I have to describe it with code it looks like this
<body>
<aside ng-controller="SideNavCtrl">Left Nav data</aside>
<header ng-controller="HeaderCtrl">Page Title</header>
<section>
<article ng-view>Page Content</article>
</section>
I'm using ngRoute and change my routes left and right. What I want is to update "Page Title" from the controller that accepted the route. I've tried hundreds of different ways but changing variable never forces header data update. Here's my latest variable share between controllers (which doesn't work)
app.controller("HeaderCtrl", ["$scope", "HeaderData", function($scope, HeaderData) {
$scope.title = HeaderData.title();
}]);
app.factory("HeaderData", function() {
var title = 'Default';
return {
title: function() { return title; },
setTitle: function(newTitle) { title = newTitle; }
}
});
Later on in the routed controller I go like this
app.controller("PreviewBuildCtrl", ["$scope", "$routeParams", "$location", "BuildsAPIService", "HeaderData", function($scope, $routeParams, $location, BuildsAPIService, HeaderData) {
$scope.build = BuildsAPIService.getBuildById($routeParams.buildId);
HeaderData.setTitle("Previewing Build");
console.log(HeaderData);
if (!$scope.build) {
$location.url('/builds/' + $routeParams.profession);
}
}]);
The problem is that using HeaderData.setTitle() doesn't update the header contents. I really don't want to write jQuery code for this task. There must be a smarter, more angular way of doing this.
All I want to do is change the header title to "Page B" when you move to page b from page a and I consider adding breadcrumbs plugins an overkill for the task at hand :(
You break the data binding via title = newTitle. The controllers are pointing to the old title, but HeaderData is not pointing to it anymore so it is lost. You need another dot.
this.title = newTitle;
The title method should also return this.title, of course.

Getting ng-repeat to work inside of AngularJS's $interpolate service

I am using the AngularJs-UI components for Bootstrap. I would like to insert a filled out template into one of the data elements for the popover feature. This works find for all elements not inside of a ng-repeat. How do I get the ng-repeat elements to work inside of a interpolated template?
I have a plunker at http://plnkr.co/edit/Cuku7qaUTL1lxRkafKzv Its not working because I don't know how to get Angular-UI-bootstrap to in plunker.
<div data-popover="{{createHTML()}}">some content</div>
My local scope has the function createHTML() that looks something like this.
angular.module('myApp', ['ngSanitize'])
.controller("myController", function(myService){
$scope.createHTML = function() {
var thingy = { blah: "foobar", collection: [ "1", "2", "3" ] };
return myService.html_for(thingy);
}
});
And the service is
angular.module('myApp')
.service('myService', function($templateCache, $interpolate, $sanitize, $log) {
"use strict";
function html_for(thingy) {
var template = $templateCache.get('thingyTemplate.html'),
link = $interpolate(template),
html = link(thingy),
unsafe = $sanitize(html);
return unsafe;
}
return {
html_for: html_for
}
});
Templates:
<script type="text/ng-template" id="thingyTemplate.html">
<div>
<div><strong>Blah:</strong> {{blah}}</div>
<div data-ng-repeat="foo in collection"><strong>Collection:</strong> {{foo}}</div>
<div><strong>Collection[0]:</strong> {{collection[0]}}</div>
<div><strong>Collection[1]:</strong> {{collection[1]}}</div>
<div><strong>Collection[2]:</strong> {{collection[2]}}</div>
</div>
</script>
<script type="text/ng-template" id="template/popover/popover.html">
<div class="popover {{placement}}" data-ng-class="{ in: isOpen(), fade: animation() }">
<div class="arrow"></div>
<div class="popover-inner">
<h3 class="popover-title" data-ng-bind="title" data-ng-show="title"></h3>
<div class="popover-content" data-ng-bind-html="content"></div>
</div>
</div>
</script>
$interpolate doesn't handle directives like ngRepeat (
Difference between parse, interpolate and compile ). $interpolate:
Compiles a string with markup into an interpolation function. This
service is used by the HTML $compile service for data binding.
To handle ngRepeat and other directives you want $compile. But for your use case $compile is going to result, unfortunately, in a number of changes because:
It needs a scope to compile against rather than just a context like $interpolate. Moreover it needs the scope thingy is on.
This means we'll need to reference your properties like so {{thingy.blah}} instead of {{blah}} inside your template.
The compile needs to happen when the popup is on the dom.
The popup is only on the dom when it's open.
So we can't just replace $interpolate with $compile inside your service.
One approach is to replace data-ng-bind-html with the following directive that acts like an ng-bind-html that has a built in $compile (clearly you should only use this with html that you know is safe).
.directive('compile', function($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
var result = element.html(value);
$compile(element.contents())(scope.$parent.$parent);
}
);
};
});
Used like so (with compile replacing ng-bind-html:
<div class="popover-content" compile="content"></div>
One issue is that we need thingy to be in scope. There's a few of ways of handling that- but for demonstration purposes I've manually gone back up to the scope the popover is called from - which is 2 scopes up thus the scope.$parent.$parent.
Using this compile directive you no longer $interpolate or $sanitizeso the function in your service can shrink down to just returning the appropriate template:
function html_for() {
var template = $templateCache.get('thingyTemplate.html');
return template;
}
demo fiddle

Angularjs/Jquery data()

How to attach arbitrary data to an html element declaratively, and retrieve it.
Please see the code. http://plnkr.co/edit/sePv7Y?p=preview
Angular has the jQuery data() support.
So, I want to attach data to each li element (say _data = node ) in the template, and later on to retrieve it using
var li = elm[0]....
console.log('li-', li.data('_data'))
li - {id:1}
Code:
'use strict';
var app = angular.module('Directives', []);
app.controller('MainCtrl', function ($scope) {
$scope.data = [
{id:1}, {id:2}, {id:3}
];
});
app.directive('test', function ($timeout) {
return {
template: '<li class="ch" ng-repeat="node in data">' +
'<span class="span2">' + 'id - {{node.id}}' + '</span>' +
'</li>',
restrict: 'A',
link: function (scope, elm, attrs) {
console.log(elm[0].children);
}
};
});
Edit:
Updated the code with how I like to set data.
template: '<li class="ch" ng-repeat="node in data" data-node="node">' +
couldn't select the li element properly now to see whether it is working
tried,
elm[0].children[0].data()
elm.children[0].data()
etc..
First of all, if it were some third party lib that you are trying to integrate with angular, that might be ok, but now you're generating DOM with angular and embedding data in the DOM. This is very strange.
Second, your test directive template uses ngRepeat, which creates isolate scope and you won't be able to access li items declaratively. You will have to use DOM traversal, which is also not very angular-way-ish.
Third, your view should be bound to model by angulars two-way bindings. Do not try to simulate opposite behaviour on top of that. Either you should not use angular or you should change your approach to your problem, because it will be pain to develop and maintain otherwise.
I would provide a real answer if you could describe what are you trying to achieve and why exactly do you need that model in data. Now the easiest solution would be ditching test directive and rewriting it as such:
controller's template:
<ul>
<li ng-repeat="node in data" model-in-data="node">
<span class="span2">id - {{node.id}}</span>
</li>
</ul>
directive modelInData
.directive('modelInData', function($parse) {
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
var model = $parse($attrs.modelInData)($scope);
$attrs.$set('data', model);
}
}
});
Here each li element adds it's model to the data attribute.

Categories