AngularJs decoupling page segments' nav links but maintaining one navigation system - angularjs

I have a side menu directive that populates itself from a Sidebar Controller but the routing for all click events on the entire website comes from the page's parent Route Controller.
The Container main page
<body data-ng-controller ="RouteCtrl as vm">
<div data-header=""></div>
<div data-ng-controller="SidebarCtrl">
<div data-side-bar=""></div>
</div>
<div data-ng-view=""></div>
......
</body>
The SidebarCtrl
(function () {
'use strict';
var controllerId = 'SidebarCtrl';
angular.module('app').controller(controllerId,
['$scope', SidebarCtrl]);
function SidebarCtrl($scope) {
var vm = this;
vm.title = 'SidebarCtrl';
vm.toggleMenu = function () {
vm.minifyMe = !vm.minifyMe;
};
vm.expandNav = function (item) {
if (item !== vm.expandedItem) {
vm.expandedItem = item;
} else {
//vm.expandedItem = null;
}
};
vm.menuItems = [
{
name: "Parent", subMenu:
[
{ name: "Sub 1", module: "Mod1", url: "rootPage" },
{ name: "Sub 2", module: "Mod2", url: "mod2Sub1/somePage" },
{ name: "Sub 3", module: "Mod3", url: "folder2/mod3Sub2/somePage" },
{ name: "Sub 4", module: "Mod4", url: "folder3/subFoler/somePage" }
]
}
];
}
angular.module('app')
.directive('sideBar', function () {
return {
restrict: "A",
replace: true,
templateUrl: '/app/html/common/sidebar.html'
};
});
})();
The sidebar html
<aside id="left-panel" data-ng-controller="SidebarCtrl as vm">
....
<li data-ng-repeat="menuItem in vm.menuItems" ....>
<span class="menu-item-parent">{{menuItem.name}}</span>
<ul ....>
<li data-ng-repeat="subItem in menuItem.subMenu">
<a style="cursor: pointer;" data-ng-click="vm.changeView('{{subItem.module}}','{{subItem.url}}')">{{subItem.name}}</a>
</li>
</ul>
</li>
....
</aside>
The Route Controller
// Route navigation for entire app
vm.changeView = function (moduleView, dashboardView) {
$rootScope.event = { viewUrl: dashboardView, moduleUrl: moduleView };
console.log(dashboardView);
$location.path(dashboardView);
};
var newRoute = $routeParams.primaryNav;
if ($routeParams.secondaryNav != "" && $routeParams.secondaryNav != undefined) {
newRoute = newRoute + '/' + $routeParams.secondaryNav;
}
if ($routeParams.tertiaryNav != "" && $routeParams.tertiaryNav != undefined) {
newRoute = newRoute + '/' + $routeParams.tertiaryNav;
}
$scope.templateUrl = '/app/html/' + newRoute + '.html';
As show above, the side bar populates but the routing does not work
If I made the sidebar nav static [which I do not want], and the sidebar page's controller the RouteCtrl the side bar navigation responds properly.
How can I maintain the entire navigation of the website in the route controller, yet decouple the various segments of the website, eg. the sidebar nav, the headernav, the footernav, etc ?

To solve this problem, I did several things:
I removed vm as a pattern, I cannot see any value in using it so I reverted back to the prior pattern of $scope.
Although the parent controller's function changeView is now accessible to the child [again, once ALL 'vm' is replaced with $scope in the project, I opted for $emit] :
1. Index.html
<body data-ng-controller ="RouteCtrl">
<div data-header=""></div>
<div data-ng-controller="SidebarCtrl">
<div data-side-bar=""></div>
</div>
<div data-ng-view=""></div>
......
</body>
2. Sidebar.html
< a style="cursor: pointer;" data-ng-click="handleNav(subItem.module,subItem.url)">{{subItem.name}}</a>
3. Side Crtl:
$scope.handleNav = function (args1, args2) {
$scope.$emit('handleNewView', (args1, args2));
};
4. Parent Crtl:
$scope.$on('handleNewView', function (args1, args2) {
$scope.changeView(args1, args2);
});
$scope.changeView = function (moduleView, dashboardView) {
$rootScope.event = { viewUrl: dashboardView, moduleUrl: moduleView };
$location.path(dashboardView);
};
In conclusion, I cannot see any value in adopting "Controller as vm" as a pattern.

Related

Why does my service reinitialize?

On my web application, I have a menu on the left that should be the same on all pages. On top of that, whenever a page changes, the item that was selected should be selected again when the new page loads. For this I made a directive:
menu.html
<div id="sidebar-wrapper" ng-style="style()" fill-height="20">
<ul class="sidebar-nav">
<li class="component-menu" ng-repeat="component in menu.components">
<span>
<img src="img/arrow_down_ok.png" />
<span ng-class="{selected: menu.isActive(component.name)}" ng-click="menu.select(component.name,'all')" >{{ component.name }}</span>
</span>
<ul ng-if="component.devices">
<li class="device-menu" ng-repeat="device in component.devices">
<span ng-class="{selected: menu.isActive(component.name, device.name)}" ng-click="menu.select(component.name,device.name)">{{ device.name }}</span>
</li>
</ul>
</li>
</ul>
</div>
menu directive
var app = angular.module("myApp.directives", ["services"]);
app.directive("componentMenu", ["menuService", function() {
return {
restrict: "E",
templateUrl: "js/templates/menu.html",
controller: function($scope, $location, menuService) {
var that = this;
this.components = [ {
name : "pms",
devices : [ {name : "robot"}, {name : "controller"} ]
}, {
name : "bms",
devices : [ {name : "ScanningController"}, {name : "nozzle"} ]
}, ];
console.log("menu components:", this.components);
menuService.selectedComponent = "";
menuService.selectedDevice = "";
this.select = function(component, device) {
device = device || "all";
$location.search("component", component);
$location.search("device", device);
menuService.selectedComponent = component;
menuService.selectedDevice = device;
console.log("Menu selected:", menuService.selectedComponent + "/" + menuService.selectedDevice);
}
this.isActive = function(component, device) {
device = device || "all";
return component == menuService.selectedComponent && device == menuService.selectedDevice;
}
$scope.$watch(function($scope, that) {
return $location.url();
}, function(url) {
if (url) {
var component = menuService.selectedComponent;
var device = menuService.selectedDevice;
if (!(menuService.selectedComponent == component && menuService.selectedDevice == device)) {
that.select(component, device);
}
}
}
);
},
controllerAs: "menu",
};
}]);
menu service
var app = angular.module("myApp.services", []);
app.factory("menuService", [function() {
this._selectedComponent;
this._selectedDevice;
var menuSelection = {
selectedComponent: this._selectedComponent,
selectedDevice: this._selectedDevice
}
return menuSelection;
}]);
Whenever an item in the menu is selected, the console prints out the selected item correctly and next prints out the menu components (which seems correct since the URL gets changed). But when the page of the new URL is loaded, the variables inside the menu service are empty again.
Could anybody explain me why this is, as I understood that services were singletons and should keep their values.
When using service, it is instanciated with the new keyword which is why you bind the properties to the this and return a constructor. However you are using a factory. In that case you create an object that you return. Therefore you need your components to be instanciated with var =.

AngularJS and UI-Router: keep controller loaded

I am building a web application for our customer support. One of the needs is to be able to keep multiple tickets opened at the same time.
I was able to do the first part easily using a tabulation system and UI-Router.
However, with my current implementation, each time I change active tab, the previously-current tab is unloaded, and the now-current tab is loaded (because it was unloaded with a previous tab change).
This is not at all the expected behavior. I've already spent a couple of days trying to find a way to achieve this, without any luck.
The closest thing I was able to do is to use the multiple views system from UI-Router, but I need multiple instance of the same view to keep in memory (if multiple tickets are opened, they all are on the same view, with the same controller, but a different scope)
Here's my current implementation:
supportApp.js:
var app = angular.module("supportApp", ["ui.router", "ui.bootstrap"]);
app.config(function($stateProvider, $urlRouterProvider, $httpProvider){
$urlRouterProvider.otherwise("/");
$stateProvider
.decorator('d', function(state, parent){
state.templateUrl = generateTemplateUrl(state.self.templateUrl);
return state;
})
.state("main", {
abtract: true,
templateUrl: "main.html",
controller: "mainController"
})
.state("main.inbox", {
url: "/",
templateUrl: "inbox.html",
controller: "inboxController"
})
.state('main.viewTicket', {
url: '/ticket/{id:int}',
templateUrl: "viewTicket.html",
controller: "ticketController"
})
;
});
mainController.js: (handles other stuff, minimal code here)
app.controller("mainController", function($rootScope, $http, $scope, $state, $interval){
// Tabs system
$scope.tabs = [
{ heading: "Tickets", route:"main.inbox", active:false, params:{} }
];
var addTabDefault = {
heading: '',
route: null,
active: false,
params: null,
closeable: false
};
$rootScope.addTab = function(options){
if(!options.hasOwnProperty('route') || !options.route)
{
throw "Route is required";
}
var tabAlreadyAdded = false;
for(var i in $scope.tabs)
{
var tab = $scope.tabs[i];
if(tab.route == options.route && angular.equals(tab.params, options.params))
{
tabAlreadyAdded = true;
break;
}
}
if(!tabAlreadyAdded)
{
$scope.tabs.push($.extend({}, addTabDefault, options));
}
if(options.hasOwnProperty('active') && options.active === true)
{
$state.go(options.route, options.hasOwnProperty('params')?options.params:null);
}
};
$scope.removeTab = function($event, tab){
$event.preventDefault();
if($scope.active(tab.route, tab.params))
{
$scope.go($scope.tabs[0].route, $scope.tabs[0].params);
}
$scope.tabs.splice($scope.tabs.indexOf(tab), 1);
};
$scope.go = function(route, params){
$state.go(route, params);
};
$scope.active = function(route, params){
return $state.is(route, params);
};
$scope.$on("$stateChangeSuccess", function() {
$scope.tabs.forEach(function(tab) {
tab.active = $scope.active(tab.route, tab.params);
});
});
});
main.html:
<div class="container-fluid" id="sav-container">
<div class="row-fluid">
<div class="col-lg-2">
<form role="form" id="searchForm" action="#">
<div class="form-group has-feedback">
<input class="form-control" type="search" />
<span class="glyphicon glyphicon-search form-control-feedback"></span>
</div>
</form>
</div>
<div class="col-lg-10" id="support_main_menu">
<ul class="nav nav-tabs">
<li ng-repeat="t in tabs" ng-click="go(t.route, t.params)" ng-class="{active: t.active, closeable: t.closeable}" style="max-width: calc((100% - 128px) / {{tabs.length}});">
<a href class="nav-tab-text">
<button ng-show="t.closeable" ng-click="removeTab($event, t)" class="close" type="button">×</button>
<span>{{t.heading}}</span>
</a>
</li>
</ul>
</div>
</div>
<div class="row-fluid">
<div class="tab-content" ui-view></div>
</div>
</div>
It seems to me that what I ask is pretty standard, but I sadly couldn't find any usefull thing on the Internet
The basic idea is to store state (i.e. list of tickets) in a service as opposed to a controller. Services hang around for the life of the application. There are some articles on this. I'm still developing my approach but here is an example:
var RefereeRepository = function(resource)
{
this.resource = resource; // angular-resource
this.items = []; // cache of items i.e. tickets
this.findAll = function(reload)
{
if (!reload) return this.items;
return this.items = this.resource.findAll(); // Kicks off actual json request
};
this.findx = function(id)
{
return this.resource.find({ id: id }); // actual json query
};
this.find = function(id) // Uses local cache
{
var itemx = {};
// Needs refining
this.items.every(function(item) {
if (item.id !== id) return true;
itemx = item;
return false;
});
return itemx;
};
this.update = function(item)
{
return this.resource.update(item);
};
};
refereeComponent.factory('refereeRepository', ['$resource',
function($resource)
{
var resource =
$resource('/app_dev.php/referees/:id', { id: '#id' }, {
update: {method: 'PUT'},
findAll: {
method: 'GET' ,
isArray:true,
transformResponse: function(data)
{
var items = angular.fromJson(data);
var referees = [];
items.forEach(function(item) {
var referee = new Referee(item); // Convert json to my object
referees.push(referee);
});
return referees;
}
},
find: {
method: 'GET',
transformResponse: function(data)
{
var item = angular.fromJson(data);
return new Referee(item);
}
}
});
var refereeRepository = new RefereeRepository(resource);
// Load items when service is created
refereeRepository.findAll(true);
return refereeRepository;
}]);
So basically we made a refereeRepository service that queries the web server for a list of referees and then caches the result. The controller would then use the cache.
refereeComponent.controller('RefereeListController',
['$scope', 'refereeRepository',
function($scope, refereeRepository)
{
$scope.referees = refereeRepository.findAll();
}
]);

How should you access controller functions from other modules

I am having a hard time understanding how Modules should interact with each other in Angularjs. I would like to break the application into nice small modules, but I cannot seem to find the correct way to have these modules interact with each other.
JSFiddle:
http://jsfiddle.net/jwest80/o5o3sr8q/4/
The code shows a breadcrumb I would like to have at the top of the page. The BreadCrumb is in its own module 'bread' and included inside a parent module 'ngFSCH'.
There is a list outside BreadCrumb controller section whose actions should add breadcrumbs. However, I do not understand the correct way to access this addCrumb function. I can only make it work if it is called from inside the breadcrumb controller section in the markup.
Markup:
<div ng-app="ngFSCH">
<section ng-controller="BreadCrumbsCtrl">
<span ng-repeat="crumb in crumbs" class="breadcrumbs">
<span ng-hide="isLast($index)" ng-click="selectCrumb($index)">{{crumb.text}} > </span>
<span ng-show="isLast($index)">{{crumb.text}}</span>
</span>
</section>
<section>
<h4>Add Some Crumbs</h4>
<ul>
<li>Company</li>
<li>Department</li>
<li>User</li>
</ul>
</section>
</div>
Script:
var ngFSCH = angular.module('ngFSCH', ['bread']);
(function () {
var app = angular.module('bread', []);
app.controller('BreadCrumbsCtrl', ['$scope', '$log', function ($scope, $log) {
$scope.crumbs = [{ text: "Crumb 1", url: "url1" }, { text: "Crumb 2", url: "url2" }];
$scope.isLast = function(index) {
return index === $scope.crumbs.length-1;
}
$scope.addCrumb = function (newCrumb) {
$scope.crumbs.push({ text: newCrumb, url: "TestURL" });
}
$scope.selectCrumb = function (index) {
$log.info($scope.crumbs[index].url);
$scope.crumbs = $scope.crumbs.slice(0, index + 1);
}
}]);
})();
I would encapsulate the bread crumb functionality in a service and create a controller for the section with the links (that add the breadcrumbs). The new controller can then use the service to add and remove crumbs from the array. You can also add the crumbs array into a value.. Your controllers can then expose the add and select features to the tiny portions of html they control without polluting other sections of your page.
Here is the result. Hope it helps!
JSFiddle
Here is the code:
var app = angular.module('bread', []);
app.value('crumbs', [
{ text: "Crumb 1", url: "url1" },
{ text: "Crumb 2", url: "url2" }
]);
app.factory("BreadCrumbsService", ['$log', 'crumbs', function ($log, crumbs) {
var service = {
getCrumbs: getCrumbs,
addCrumb: addCrumb,
selectCrumb: selectCrumb
};
return service;
//I did not add a set crumbs because you can set it directly.
function getCrumbs(){
return crumbs;
}
function addCrumb(newCrumb) {
crumbs.push({
text: newCrumb,
url: "TestURL"
});
}
function selectCrumb(index) {
$log.info(crumbs[index].url);
crumbs = crumbs.slice(0, index + 1);
}
}]);
app.controller('BreadCrumbsCtrl', ['$scope', 'BreadCrumbsService', function ($scope, BreadCrumbsService){
$scope.crumbs = BreadCrumbsService.getCrumbs;
$scope.selectCrumb = BreadCrumbsService.selectCrumb;
$scope.isLast = function (index) {
return index === BreadCrumbsService.getCrumbs().length - 1;
}
}]);
app.controller('AddLinksCtrl', ['$scope', 'BreadCrumbsService', function ($scope, BreadCrumbsService) {
$scope.addCrumb = BreadCrumbsService.addCrumb;
}]);
Here is the links section with the new controller:
<section ng-controller="AddLinksCtrl">
<h4>Add Some Crumbs</h4>
<ul>
<li>Company</li>
<li>Department</li>
<li>User</li>
</ul>
</section>
That is intended because you are working within the scope of the controller. How about moving the ng-controller directive to the containing div where ng-app is?
<div ng-app="ngFSCH" ng-controller="BreadCrumbsCtrl">

Updating ng-include from directive in Angular

I am trying to click on a list item that is created via a repeater and update the template value that is being used by ng-inlude. The initial value that is being set in the controller is working fine (shows up in DOM), however when i change the value in the directive, it is not updating the DOM (stays as initial included DOM). When i use fire bug on the directive it looks as though the scope has changed, although I'm not sure if I am missing something, or if i should be doing this some other way.
This is my partial
<div ng-controller="menuCtrl" >
<ul class="sub-menu">
<li ng-repeat="item in menuItems.left" menu-repeater-directive>
<span>{{item.name}}</span>
<a class="reset-menu-btn">×</a>
</li>
</ul>
<div id="lft-wrapper" class="a-wrapper">
<div ng-include="template.url" id="lft-scroller" class="a-scroller"></div>
</div>
</div>
This is my Menu Control
angular
.module('simApp')
.controller("menuCtrl", function($scope, $element) {
$scope.menuItems = {
left: [
{
name: "Table of Context",
url: "static/partials/tree.html"
},
{
name: "Index",
url: "static/partials/dictionary.html"
},
{
name: "Exercises",
url: "static/partials/exercises.html"
},
{
name: "Search Results",
url: "static/partials/results.html"
}
],
right: [
{
name: "About Writer's Help",
url: "static/partials/about.html"
},
{
name: "Tags & Tools",
url: "static/partials/tools.html"
},
{
name: "Your Class",
url: "static/partials/class.html"
},
{
name: "Top Ten",
url: "static/partials/top10.html"
}
]
}
$scope.template = $scope.menuItems.left[0];
});
This is my directive
angular
.module('simApp')
.directive("menuRepeaterDirective", function() {
return function(scope, element, attrs){
var self = this;
this.scope = scope;
this.element = element;
var $ele = $(element);
$ele.find("span").fastClick(function(ev){
//angular specific
var index = $(ev.currentTarget).parent().index();
self.scope.template = self.scope.menuItems.left[index];
//end angular specific
....some other Jquery stuff
});
}
});
Try:
self.scope.$apply(function(){ //Use $apply to let angular aware of the changes.
var index = $(ev.currentTarget).parent().index();
self.scope.$parent.template = self.scope.menuItems.left[index]; //accessing self.scope.$parent instead of self.scope
});
DEMO
Explanation why we have to access self.scope.$parent:
self.scope is the scope of the current item generated by ng-repeat while your ng-include is binding to menuCtrl's scope. self.scope.$parent is menuCtrl's scope in this case.

How to refresh an ng-repeat list when the backing model has been updated via ng-click?

I'm rather new to AngularJS so I presume this is going to be something about confusing scope.
I'm trying to get a ng-repeat list watching a service on the scope to update when I push a new item to the service backing model from a ng-click anchor link.
An example failing fiddle is here: http://jsfiddle.net/znFwY/
JS:
'use strict';
angular.module('myApp', [])
.factory('Order', function () {
return {
packages: {
theList: [],
list: function () {
return this.theList;
},
push: function (p) {
if (p === undefined || !!this.theList[p.title]) return;
this.theList[p.title] = p;
console.log('pushed ' + p.title);
},
contains: function (p) {
return !!this.theList[p.title];
},
print: function () {
console.log(this.theList);
}
}
}
})
.controller('AppCtrl', function ($scope, $http, $location, Order) {
$scope.order = Order;
$scope.data = {
packages: [
{title: 'one'},
{title: 'two'},
{title: 'three'},
]
};
})
;
HTML:
<div ng-app="myApp">
<div ng-controller="AppCtrl">
<ul id="package-list">
<li ng-repeat="package in data.packages | orderBy:package.order">
<a ng-click="order.packages.push(package)" ng-class="{selected: order.packages.contains(package)}">{{package.title}}</a>
</li>
</ul>
<ul id="basket-list">
<li ng-repeat="package in order.packages.list() | orderBy:package.order"> {{package.title}}</li>
</ul>
<a ng-click="order.packages.print()">print list</a>
</div>
</div>
Thanks

Resources