Menu and different submenu on click - angularjs

I am trying to build a menu and a submenu in angular.
What I want to do is to have two arrays of objects
Menu
menu = [{name: 'Name1', link: '/link1'}, {name: 'Name2', link: '/link2'}]
submenu = [[{name: 'SubName1', link: '/Sublink1'}, {name: 'SubName1', link: '/sublink1'}],
[[{name: 'SubName2', link: '/Sublink2'}, {name: 'SubName2', link: '/sublink2'}]]
So when I click Name1 the first array of SubMenu will be selected and when clicking Name2 the second array will be selected.
How I can create two Directives one for the main menu and one for the second and be able to communicate between them on click. I have tried building this in a controller, I was able to select the submenu by using the $index, but the submenu can't be moved around as I like because it needs to be under the controller.
I finally managed to solve my problem here is the solution: http://jsfiddle.net/4kjjyL4s/4/
How can I improve my solution?

Don't reinvent the wheel :) UI router is a prepackaged solution that handles nested routing for you.
If you have a menu of items and you want to display another menu of items when one of the items is selected UI router does exactly that. https://github.com/angular-ui/ui-router

Can't give you the exact answer because information is lacking, but for example if you're using the directives with different menu items at other places in your app, I'd recommend to pass the menu array from controller (ng-controller, not directive's controller) through directive's scope.
Also, you're looking for kinda standard way for directives to communicate directly (in your case, communication between menu and submenu directive to notify the item selection change), use directive's controller. Here's a good tutorial.
https://thinkster.io/egghead/directive-to-directive-communication/

To communicate between controllers or directives, you should use services.
From the angular guide ( https://docs.angularjs.org/guide/services ):
Angular services are substitutable objects that are wired together using dependency injection (DI). You can use services to organize and share code across your app.
I checked the code you posted on jsfiddle ( http://jsfiddle.net/4kjjyL4s/4/ )and I tried to keep the most of it. Below are my changes in the JavaScript file ( please, read the comments in the code ).
var app = angular.module("app",[]);
app.controller('main', function(){});
// The service will be responsible for the shared objects and logic
app.service('MenuService', function () {
var list = [
{
name: "Menu1", link: "#menu1",
submenu: [
{ name: "Menu1Sub1", link: "#submenu1" },
{ name: "Menu1Sub2", link: "#submenu2" }
]
},
{
name: "Menu2", link: "#menu2",
submenu: [
{ name: "Menu2Sub1", link: "#submenu1" },
{ name: "Menu2Sub2", link: "#submenu2" }
]
}
];
var selected = [];
// methods and attributes published under the **this**
// keyword will be publicly available
this.getMenu = function () {
return list;
};
this.getSubmenu = function () {
return selected;
};
this.select = function ( menuItem ) {
// this does the *trick*!
// if we use the assignment operator here, we would replace the
// reference returned in the getSubmenu() method, so as the original
// reference did not change, angular's dirty checking would not detect it.
// using angular.copy() method, we are copying the contents of the
// selected submenu over the same reference returned by getSubmenu()
angular.copy( menuItem.submenu, selected );
};
});
// No $rootScope injection results in better re-usability. When you were
// relying in $rootScope sharing, both directives should live in the
// $rootScope, so if you add them inside a ng-controller created scope
// they would not work anymore
app.directive("menu", function() {
return {
restrict: "E",
// no need to isolate scope here, *scope:true* creates a new scope
// which inherits from the current scope
scope: true,
// with controllerAs (introduced in angular 1.2), you can skip
// polluting the scope injection.
controllerAs: "ctrl",
controller: function( MenuService ) {
this.list = MenuService.getMenu();
this.changeSub = function ( menuItem ) { MenuService.select( menuItem ); };
},
template: "<div ng-repeat='menu in ctrl.list'><button ng-click='ctrl.changeSub(menu)'>{{menu.name}}</button></div>"
};
});
app.directive("submenu", function() {
return {
restrict: "E",
scope: true,
controllerAs: "ctrl",
controller: function( MenuService ) {
this.sublist = MenuService.getSubmenu();
},
template: "<span ng-repeat='menu in ctrl.sublist'>{{menu.name}} | </span>aa"
};
});
And here is the updated HTML file, just to show both directives work now not directly inserted in the $rootScope
<div ng-app="app">
<div ng-controller="main">
<menu></menu>
<h1>Hello World!</h1>
<div class="main-content">
<submenu></submenu>
</div>
</div>
</div>
Hope it helps!

Try this Code:
function MyCtrl ($scope) {
$scope.subMenu = []; // default is false
$scope.toggleSubMenu = function (index) {
$scope.subMenu[index] = !$scope.subMenu[index];
};
}
HTML
<ul>
<li ng-class="{active: subMenu[0]}"> Name1
<ul>
<li>test</li>
<li>test</li>
<li>test</li>
</ul>
</li>
<li ng-class="{active: subMenu[1]}"> Name2
<ul>
<li>bar</li>
<li>bar</li>
<li>bar</li>
</ul>
</li>
</ul>
Also Check this

Related

How can I use interpolation to specify element directives?

I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.

Read existing data from DOM into model, ng-model for static data?

I'm currently playing around with rewriting the functionality of an existing page using Angular. The gist of it is that I have a plain HTML page with a list of stuff, like this:
<ul>
<li class="item">
<h1>Foo</h1>
<ul class="categories">
<li class="category">Bar</li>
</ul>
</li>
...
</ul>
This is augmented by some Javascript which parses this data once and adds a dynamic category filter menu to the page. I.e. it extracts all li.category elements and displays a menu with them, and clicking on one of these categories filters the item list to display only items with the chosen category.
I've replicated the basics of that in Angular with a lot less code than I had before. However, I'm still doing a lot of jQuery traversing of the .item elements to build that initial list of categories:
myApp.controller('MyController', function ($scope) {
$scope.categories = [];
angular.element('.item').each(function () {
angular.element(this).find('.categories .category').each(function () {
var category = this.textContent;
for (var i = 0, length = $scope.categories.length; i < length; i++) {
if ($scope.categories[i].name == category) {
$scope.categories[i].count++;
$scope.categories[i].items.push(this);
return;
}
}
$scope.categories.push({ name : category, count : 1, items : [this] });
});
});
});
This does not seem to be in the spirit of Angular, and I'd like to replace it with something like:
<ul>
<li class="item" ng-item>
<h1>Foo</h1>
<ul class="categories">
<li class="category" ng-category>Bar</li>
</ul>
</li>
...
</ul>
A directive should then be able to parse all ng-item/ng-category elements and add them to the model/scope once. Something like ng-model, but for static data.
I have virtually no experience with Angular, how can I accomplish this; or shouldn't I want to do something entirely different in the first place?
For creating your own ng-item and ng-category directives, I suggest that you can go through Creating Custom Directives part in Angular Offical Develop Guide:
http://docs.angularjs.org/guide/directive
It will tell you how to begin creating your directive from add directive to module like this:
.directive('myCustomer', function() {
return {
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
});
Edit:
This two is also useful tutorial:
http://www.ng-newsletter.com/posts/directives.html
http://www.befundoo.com/university/tutorials/angularjs-directives-tutorial/
Edit2:
To answer your comment:
Do you have a concrete sample of how I'd write a directive that reads data from its element and modifies the controller's scope?
I thought that it has clear explanation in Angular Official Guide:
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-iso.html'
};
});
In this example:
restrict: 'E' : directive name match element name
So directive look like this:
<my-customer></my-customer>
scope: { customerInfo: '=info'}
<my-customer info="myInfo"></my-customer>
this will bind myInfo to scope just like this expression:
$scope.customerInfo = myInfo;
this is a concrete sample of how to read data from its element and modify the controller's scope.

AngularJS: How create a new directive joining existents directives?

I would like improve my current solution.
I have a big menu using <ul> and <li> tags. I need show to the user only the <li> tags that they have permission to access.
I have resolved this problem using two directives: ng-init and ng-show
...
<li ng-init="ok=hasPemission('item1')" ng-show="ok">
Item 1
</li>
...
I needed to use 'ng-init' to get a stable hasPermission result. I can't use ng-show="hasPemission('item1')" because 'hasPemission' returns a new 'promise' object and the ng-show has problem with not stable expressions ([$rootScope:infdig] infinit $digest loop).
Now, I wanted create a new directive that join this both directives. I created this one but I think that there is a way to reuse these two existents directives.
This is my new directive:
.directive('myPermissionShow',['$q','$animate','AccessControl',
function ($q, $animate, AccessControl) {
return {
restrict:'A',
scope: {
resourceName: '#myPermissionShow'
},
link: function ($scope, $element) {
var user = AccessControl.getLoggedUser();
AccessControl.hasPermission(user,$scope.resourceName).then(
function (value) {
// I copied the line below from ngShow directive:
$animate[value ? 'removeClass' : 'addClass']($element, 'ng-hide');
}
);
}
}
}])
So, my html changed to:
...
<li my-permission-show="item1">
Item 1
</li>
<li my-permission-show="item2">
Item 2
</li>
...
Is there a way to create this new directive reusing the directives 'ng-init' and 'ng-show'?
Something like this:
!!!NOT WORKING CODE!!!
.directive('myPermissionShow',['AccessControl',
function (AccessControl) {
return {
restrict:'A',
replaceAttribute: true, /* this property does not exists... */
template: 'ng-init="val=hasPermission(user,resourceName)" ng-show="val"',
scope: {
resourceName: '#myPermissionShow'
},
controller: function ($scope, $element) {
$scope.hasPemission = AccessControl.hasPermission;
$scope.user = AccessControl.getLoggedUser();
}
}
}])
!!!NOT WORKING CODE!!!

How do I use an Angular directive to show a dialog?

Using Angular, I'm trying to create a directive that will be placed on a button that will launch a search dialog. There are multiple instances of the search button, but obviously I only want a single instance of the dialog. The dialog should be built from a template URL and have it's own controller, but when the user selects an item, the directive will be used to set the value.
Any ideas on how to create the dialog with it's own controller from the directive?
Here's what I've go so far (basically just the directive)...
http://plnkr.co/edit/W9CHO7pfIo9d7KDe3wH6?p=preview
Here is the html from the above plkr...
Find
Here is the code from the above plkr...
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var person = {};
person.name = 'World';
$scope.person = person;
$scope.setPerson = function(newPerson) {
person = newPerson;
$scope.person = person;
}
});
app.directive('myFind', function () {
var $dlg; // holds the reference to the dialog. Only 1 per app.
return {
restrict: 'A',
link: function (scope, el, attrs) {
if (!$dlg) {
//todo: create the dialog from a template.
$dlg = true;
}
el.bind('click', function () {
//todo: use the dialog box to search.
// This is just test data to show what I'm trying to accomplish.
alert('Find Person');
var foundPerson = {};
foundPerson.name = 'Brian';
scope.$apply(function () {
scope[attrs.myFind](foundPerson);
});
});
}
}
})
This is as far as I've gotten. I can't quite figure out how to create the dialog using a template inside the directive so it only occurs once and then assign it a controller. I think I can assign the controller inside the template, but first I need to figure out how to load the template and call our custom jQuery plugin to generate the dialog (we have our own look & feel for dialogs).
So I believe the question is, how do I load a template inside of a directive? However, if there is a different way of thinking about this problem, I would be interested in that as well.
I will show you how to do it using bootstrap-ui. (you can modify it easily, if it does not suit your needs).
Here is a skeleton of the template. You can normally bound to any properties and functions that are on directive's scope:
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
... // e.g. <div class="button" ng-click=cancel()></div>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
...
</div>
</div>
</div>
Here is how to create/declare directive in your module:
.directive("searchDialog", function ($modal) {
return {
controller: SearchDialogCtrl,
scope : {
searchDialog: '=' // here you will set two-way data bind with a property from the parent scope
},
link: function (scope, element, attrs) {
element.on("click", function (event) { // when button is clicked we show the dialog
scope.modalInstance = $modal.open({
templateUrl: 'views/search.dialog.tpl.html',
scope: scope // this will pass the isoleted scope of search-dialog to the angular-ui modal
});
scope.$apply();
});
}
}
});
Then controller may look something like that:
function SearchDialogCtrl(dep1, dep2) {
$scope.cancel = function() {
$scope.modalInstance.close(); // the same instance that was created in element.on('click',...)
}
// you can call it from the template: search.dialog.tpl.html
$scope.someFunction = function () { ... }
// it can bind to it in the search.dialog.tpl.html
$scope.someProperty;
...
// this will be two-way bound with some property from the parent field (look below)
// if you want to perform some action on it just use $scope.$watch
$scope.searchDialog;
}
Then it your mark-up you can just use it like that:
<div class="buttonClass" search-dialog="myFieldFromScope">search</div>
I recommend this plugin:
https://github.com/trees4/ng-modal
Demo here:
https://trees4.github.io/ngModal/demo.html
Create a dialog declaratively; and it works with existing controllers. The content of the dialog can be styled however you like.

Angular, ng-repeat to build other directives

I'm building a complex layout, that takes a JSON document and then formats it into multiple rows, with each row then having more rows and/or combinations of rows/columns inside them.
I'm new to Angular and am just trying to get to grips with Directives. They are easy to use for very simple things, but quickly become very difficult once you need to anything more complicated.
I guess I'm doing this the wrong way around, but is there a way to simply add the name of a directive (in the example below, I've used ) and get that directive to be rendered on an ng-repeat?
Maybe the same way that you can use {{{html}}} instead of {{html}} inside of mustache to get a partial to render as HTML and not text.
As expected, the example below simply writes the name of the directive into the dom. I need Angluar to take the name of the directive, understand it, and then render before before it is written. Due to the complex layout of the page I need to design, I could be rendering many different directives, all inside each other, all from 1 JSON document (which has been structured into different rows and then row / column combinations).
Example code that renders the name of the directive to the page, but gives you an idea of how I'd like to write a solution the problem...
<div app-pages></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script>
var app = angular.module("app", ['main']);
angular.module('main', [])
.controller("appPageController", ['$scope', function( $scope ){
$scope.pages = [];
var page1 = {
title: 'Page 1',
directive: '<app-page-type-1>'
};
var page2 = {
title: 'Page 2',
directive: '<app-page-type-2>'
};
$scope.pages.push(page1);
$scope.pages.push(page2);
}])
.directive("appPageType2", function factory() {
console.log('into page type 2');
return {
replace: true,
template: 'This is the second page type'
};
})
.directive("appPageType1", function factory() {
console.log('into page type 1');
return {
replace: true,
template: 'This is the first page type'
};
})
.directive("appPages", function factory() {
console.log('into pages');
return {
replace: true,
template: '<ul><li ng-repeat="page in pages">{{page.directive}}</li></ul>'
};
});
</script>
This is one possible alternative to your idea. The idea is to append the directive you defined in page object for each html element inside the ng-repeat. Please take a look at the demo. Hope it helps.
<div ng-app="myApp" ng-controller="appPageController">
<ul>
<li ng-repeat="page in pages" app-pages></li>
</ul>
</div>
.directive("appPages", function ($compile) {
console.log('into pages');
return {
replace: true,
link: function (scope, elements, attrs) {
var html = '<div ' + scope.page.directive + '></div>';
var e = angular.element(html);
elements.append(e);
$compile(e)(scope);
}
};
});
Demo

Resources