How should you access controller functions from other modules - angularjs

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">

Related

AngularJS: ng-repeat track by obj.id doesn't reinitialise transcluded content when obj.id changes

function ComponentController() {
var vm = this;
this.$onInit = function() {
vm.link = 'http://example.com/obj/' + vm.obj.id;
}
}
function MainController($scope, $timeout) {
$scope.arrObjs = [{id: 1, name: "object1"},{id: 2, name: "object2"}];
console.log('object1\'s id is ', $scope.arrObjs[0].id);
$timeout(function() { // simulates a call to server that updates the id
$scope.arrObjs[0].id = '3';
console.log('object1\'s new id is ', $scope.arrObjs[0].id, '. Expected the link above to be updated with the new ID');
}, 1000);
}
var someComponent = {
bindings: {
obj: '='
},
template: '<div>URL: <span>{{$ctrl.link}}</span></div>',
controller: ComponentController
};
angular.module('myApp', []);
angular
.module('myApp')
.controller('MainController', MainController)
.controller('ComponentController', ComponentController)
.component('someComponent', someComponent);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MainController">
<div ng-repeat="obj in arrObjs track by obj.id">
<some-component obj="obj"></some-component>
</div>
</div>
</div>
After the ng-repeat and its someComponent are rendered one of the objects' obj.id changes (using a $timeout in the above example). The vm.link for that object still carries the old id!
Now, I know that the $onInit() is only run once inside someComponent, but why doesn't track by re-initialise the component because the obj.id changed?
If Angular truly tracked an array by the obj.ids, it should treat an obj whose id changes as a completely different object and re-initialise it, no?
Obviously a $watch on vm.obj.id within someComponent will fix it, but is there a way without adding yet another $watch?
NOTE: previously I was using
<div ng-repeat="objID in vm.arrObjIDs track by objID" ng-init="obj = vm.fnLookUpObjByID(objID)">
<someComponent obj="obj"></someComponent>
</div>
And that works perfectly! This is exactly how I expected the track by obj.id to work. But I'm trying to move away from the ng-init pattern.
You're missing something somewhere in your code that you're not showing us.
The following snippet works.
init() is not a standard function though. You probably mean $onInit()
function ComponentController() {
var vm = this;
console.log("not in init: " + this.obj.id);
this.$onInit = function() {
console.log("in init: " + vm.obj.id);
}
}
function MainController($scope) {
$scope.arrObjs = [{id: 1, name: "object1"},{id: 2, name: "object2"}];
}
var someComponent = {
bindings: {
obj: '='
},
template: '<div>ID: <span>{{$ctrl.obj.id}}</span></div>',
controller: ComponentController
};
angular.module('myApp', []);
angular
.module('myApp')
.controller('MainController', MainController)
.controller('ComponentController', ComponentController)
.component('someComponent', someComponent);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MainController">
<div ng-repeat="obj in arrObjs track by obj.id">
<some-component obj="obj"></some-component>
</div>
</div>
</div>
EDIT:
If you add an asynchronous function in the middle, the compiling of the code will always be faster than the return of the async function.
Using $watch is the standard way of updating the view when data changes.
There's not other way.
Note: With components you can use $onChanges() but in this particular case it won't trigger since you have to change the reference of the object for it to update. $onChanges() calls $watch in any case.

Factory value not updated in model ...what I am doing wrong?

I am new to angular-js. I have two controllers (welcomeContoller,productController) and both handling the same model within the factory.
When the model getting updating by one controller(productController) it should reflect the update in another controller. (welcomeContoller)
But its not happening now.
HTML code :
<body ng-app="myApp">
<div ng-controller="welcomeContoller">
{{totalProductCnt}}
</div>
<div ng-controller="productController">
<div class="addRemoveCart">
<span class="pull-left glyphicon glyphicon-minus" ng-click="removeProduct()"></span>
<span class="pull-right glyphicon glyphicon-plus" ng-click="addProduct(1)"></span>
</div>
</div>
JS code
var myApp = angular.module("myApp", ['ui.bootstrap']);
myApp.factory("productCountFactory", function() {
return {
totalProducts:0
};
});
myApp.controller("welcomeContoller", function($scope, productCountFactory)
{
$scope.totalProductCnt = productCountFactory.totalProducts;
});
myApp.controller("productController", function($scope, productCountFactory) {
$scope.addProduct = function() {
productCountFactory.totalProducts++;
alert(productCountFactory.totalProducts);
};
$scope.removeProduct = function() {
if(productCountFactory.totalProducts >=1)
productCountFactory.totalProducts--;
alert(productCountFactory.totalProducts);
};
});
Even after the addProduct is called the totalProductCnt is displaying as zero. I want to display the value for each increment.
Plunkr Link
Put the factory object reference on scope:
myApp.controller("welcomeContoller", function($scope, productCountFactory) {
$scope.productCountFactory = productCountFactory;
});
Watch the property of the object.
{{productCountFactory.totalProducts}}
The DEMO on PLNKR.
By putting a reference on scope, on every digest cycle the watcher looks up the value of the property and updates the DOM if there is a change.
The totalProductCnt from your welcomeController isn't updated because it is assigned only once when the controller is created.
You can use several solutions to refresh the displayed value. Use a getter for your totalProducts in the factory :
myApp.factory("productCountFactory", function() {
var totalProducts = 0;
return {
getTotalProducts: function() {
return totalProducts;
},
addProduct: function() {
totalProducts++;
},
removeProduct: function() {
totalProducts--;
}
};
});
myApp.controller("welcomeContoller", function($scope, productCountFactory) {
$scope.getTotalProducts = productCountFactory.getTotalProducts;
});
myApp.controller("productController", function($scope, productCountFactory) {
$scope.addProduct = function() {
productCountFactory.addProduct();
};
$scope.removeProduct = function() {
if (productCountFactory.getTotalProducts() >= 1)
productCountFactory.removeProduct();
};
});
And update the view accordingly:
<div ng-controller="welcomeContoller">
{{getTotalProducts()}}
</div>
Plunkr Link

Angularjs ng-click inside ng-repeat

I'm trying to load a list of clickable news feed URLs in a dropdown-menu. When I use fixed addresses in view, it works fine but when I populate the addresses using controller, dropdown menu is fine but ng-click doesn't work as expected.
Here is jsfiddle of working version:http://jsfiddle.net/mahbub/b8Wcz/
This code works:
<ul class="dropdown-menu">
<li>ABC News
</li>
<li>CNN
</li>
</ul>
controller code:
var App = angular.module('RSSFeedApp', []);
App.controller("FeedCtrl", ['$scope', 'FeedService', function ($scope, Feed) {
$scope.loadButonText = "Select news channel";
$scope.loadFeed = function (e) {
Feed.parseFeed($scope.feedSrc).then(function (res) {
$scope.loadButonText = angular.element(e.target).text();
$scope.feeds = res.data.responseData.feed.entries;
});
}
App.factory('FeedService', ['$http', function ($http) {
return {
parseFeed: function (url) {
return $http.jsonp('//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=50&callback=JSON_CALLBACK&q=' + encodeURIComponent(url));
}
}
}]);
This code does not:
<ul class="dropdown-menu">
<li ng-repeat =" newsChannel in channels">
{{newsChannel.title}}
</li>
</ul>
controller code:
App.controller("FeedCtrl", ['$scope', 'FeedService', function ($scope, Feed) {
$scope.loadButonText = "Select news channel";
$scope.loadFeed = function (e) {
Feed.parseFeed($scope.feedSrc).then(function (res) {
$scope.loadButonText = angular.element(e.target).text();
$scope.feeds = res.data.responseData.feed.entries;
});
}
$scope.channels = [
{
'src': 'http://www.abc.net.au/news/feed/45910/rss.xml',
'title': 'ABC News'
},
{
'src': 'http://rss.cnn.com/rss/cnn_topstories.rss',
'title': 'CNN'
}
];
}]);
App.factory('FeedService', ['$http', function ($http) {
return {
parseFeed: function (url) {
return $http.jsonp('//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=50&callback=JSON_CALLBACK&q=' + encodeURIComponent(url));
}
}
}]);
TypeError: Cannot read property 'feed' of null
Any idea why the $scope.feedSrc doesn't get the feedSrc url?
Thanks
So I recommend using $parent for this. ng-repeat has it's own scope so when you say "feedSrc" it is looking on the ng-repeat scope for that data. "newsChannel" is available because that gets added by ng repeat but you will notice that is never added to your parent controller's $scope.
<ul class="dropdown-menu">
<li ng-repeat =" newsChannel in channels">
{{newsChannel.title}}
</li>
</ul>
As an aside, you might consider not doing assignments in the view like that, it is generally preferable to only assign variables in the controller and make sure that your HTML is read only
The problem is how you are passing arguments to your ng-click:
ng-click="$parent.feedSrc='{{newsChannel.src}}';..."
You don't need to wrap the prperty in handlebars...
ng-click="$parent.feedSrc='newsChannel.src';..."
should fix your problem.
I found a workaround. Added another parameter to loadFeed method.
<ul class="dropdown-menu">
<li ng-repeat =" newsChannel in channels">
{{newsChannel.title}}
</li>
</ul>
and here is the controller's new version:
App.controller("FeedCtrl", ['$scope', 'FeedService', function ($scope, Feed) {
$scope.loadButonText = "Select news channel";
$scope.loadFeed = function (e, feedUrl) {
Feed.parseFeed(feedUrl).then(function (res) {
$scope.loadButonText = angular.element(e.target).text();
$scope.feeds = res.data.responseData.feed.entries;
});
}
$scope.channels = [
{
'src': 'http://www.abc.net.au/news/feed/45910/rss.xml',
'title': 'ABC News'
},
{
'src': 'http://rss.cnn.com/rss/cnn_topstories.rss',
'title': 'CNN'
}
];
}]);
thanks to this post: ng-click inside ng-repeat

AngularJS: how to modify an array in the service from multiple controllers

I want to have an array in the service, which can be modified from different controllers.
The purpose of this is the have an array accessible through every controller.
I want to be able to push items to this array from controllers, as well to delete them.
Service:
.service('EmailOps', function () {
var templates = [];
return {
pushToEmailBody: function (newObj) {
templates.push(newObj);
console.log(templates);
}
};
});
Controller:
angular.module('app')
.controller('mainCtrl', function ($scope, $rootScope, EmailOps) {
$scope.include = EmailOps.pushToEmailBody;
});
HTML:
<div ng-controller="mainCtrl">
1
2
3
</div>
To summarize, I would like to be able to add multiple new elements to the array in the service by clicking on these links. Currently when it adds one of them, it replaces the one added before, so that I have an array with one element only. Any idea what I might be doing wrong?
please see here : http://jsbin.com/gesirira/1/edit
service:
app.service('EmailOps', function () {
var templates = [];
function pushToEmailBody (newObj) {
templates.push(newObj);
console.log(templates);
}
return {
templates:templates,
pushToEmailBody : pushToEmailBody
};
});
controller:
app.controller('firstCtrl', function($scope,EmailOps){
$scope.include = function(obj)
{
EmailOps.pushToEmailBody(obj);
};
$scope.temp = EmailOps.templates;
});
html:
<body ng-app="app">
<div ng-controller="firstCtrl">
1
2
3
<br/>
templates: {{temp |json}}
</div>
</div>
</body>
You can modify your service like this
.service('EmailOps', function () {
this.templates = [];
this.pushToEmailBody = function (newObj) {
templates.push(newObj);
console.log(templates);
}
});
and then in the controller :
$scope.include = function(obj)
{
EmailOps.pushToEmailBody(obj);
};

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

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.

Resources