I am creating an AngularJS application that is a web planner that consists of 6 steps. Each step has its own view and its own controller. There is some data that should be accessible from all states (There is Order level information that is used in the header/title of the planner). I have put this information in a service, but the problem is in this root controller, it does not have access to the $stateParams which is how I know whether or not API data needs to be fetched.
I could use $scope.$emit() or $scope.$broadcast to tell my root controller when ui.router runs and finds the ID in the URL, but that just seems like bad design. What is the best way to make the $stateParams available to a global controller?
Here is what the HTML looks like.
<div ng-app="MyApp.SubModule.Module" ng-controller="RootController">
<sk-overlay ng-show="overlayVisible"></sk-overlay>
<h2 class="dashTitle"> {{ (Order.CustomerInfo.CompanyName ? '- ' + Order.CustomerInfo.CompanyName : '') }}</h2>
<div id="wpMenuContainer" class="wpMenuContainer">
<ul class="wpMenu clearfix">
<li ng-repeat="chevron in Chevrons" ng-class="{ active : chevron.active, navable : $index < Index }" ng-click="!($index < Index)||navigate(chevron, $index)">
<span>{{ chevron.name }}</span>
</li>
</ul>
</div>
<div ui-view class="do"></div>
</div>
I want RootController to be used to control data that is global to the entire application, but that state does not exist in the $stateProvider, therefore it does not get access to the $stateParams variables.
You could always use a shared service (I think I actually mean factory here) to enable two way communication between different controlers.
This is a hot topic and there are lots of related questions on this:
Share data between AngularJS controllers
Passing data between controllers in Angular JS?
Update
You could actually make that factory global by assigning it to the $rootScope, thus being able to observe it without having to assign it in every controller:
angular.module('app')
.factory('sharedData', function() {
return {};
})
.controller('mainController', function($rootScope, sharedData) {
$rootScope.sharedData = sharedData;
$rootScope.$watch('sharedData', updateMyStuff, true);
})
.controller('subController', function(sharedData) {
sharedData.stateParams = 'inaccessible stuff';
});
$stateParams can be injected into any of your controllers/services/directives using DI, it is a services that is provided by ui-router.
$state can also be injected into any of your controllers/services/directives using DI, it is a services that is provided by ui-router. You can access stateParams through $state via $state.params
Also, ui-router mentions that it is handy to set up $state in $rootScope on run() so that it can be access primarily in templates.
Related
I'm in an interesting conundrum.
In my app, I am using a recursive <script> which is referred to in my main page via an ng-include:
<div ng-include="'theTemplate.html'" onload="one=features.items.one"></div>
The data for features.items.one is loaded via external api using $http and is set when the $http promise is kept.
The problem is, that the ng-include loads before the promise is kept, and when the promise does complete, the {{one}} INSIDE the script does not update (because ng-include creates a new scope)
How can I get the variable inside the script to update when the promise completes?
Extending my comment:
<div ng-if="your condition">
<div ng-include="'theTemplate.html'" onload="one=features.items.one"></div>
</div>
Use a controller, and consider refactoring to a service in due course
<div ng-include="'theTemplate.html'" ng-controller="myCtrl"></div>
and then add a controller
xxx.controller('myCtrl', function($scope, Features) {
$http request.then(function(data) {
$scope.one = data.items.one;
})
})
Most articles demonstrate one method or the other... specifying the controller in the route OR the body. While I realize specifying the controller in the route provides additional benefits WHEN NEEDED (pre-loading required view resources, etc.), it is illogical to think that a modular application will be able to (or SHOULD) handle all of the functionality for a complex view.
Any proven examples (links) showing a combined approach would really help ease my mind.
Thanks, in advance.
No.
You may use both, but not for the same instance of a controller.
As an example, you can use ng-controller for your page menu controller and define the page controller loaded into your ui-view in the state config. This is fine as long as the page view does not have an ng-controller the same controller.
Your controllers constructor will run twice if you invoke it twice.
Added Snippet to Demonstrate
So in the following code we bring in the controller IndexCtrl in the uiRouter state config and in the index.html view. As you can see if you run this snippet, you have two instances of the IndexCtrl running.
Why is this a problem?
It is not a problem if this is your intention. It becomes a problem if this is your intention, to have two instances of the controller. If you follow this pattern unknowingly and create an application you will have an unneeded instance of every controller.
This will also cause developer confusion. How come I can't get back the value of a property in a controller? You will see bugs like this because a developer will set a property on one instance of a controller and then expect the same value to be in the other instance.
Controllers are classes. They are constructor functions -- so you can have multiple instances of them. And there are valid use cases to have multiple instances of a controller. But you shouldn't accidentally have two instances.
angular.module('app', ['ui.router'])
.config(['$stateProvider',
function($stateProvider) {
$stateProvider.state('index', {
url: '*path',
controller: "IndexCtrl as vm1",
templateUrl: 'index.html'
});
}
])
.controller('IndexCtrl', IndexCtrl);
function IndexCtrl() {
this.value = 0;
}
IndexCtrl.prototype.add = function() {
this.value += 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="http://angular-ui.github.io/ui-router/release/angular-ui-router.min.js"></script>
<div ng-app="app">
<ui-view></ui-view>
<script type="text/ng-template" id="index.html">
<div ng-controller="IndexCtrl as vm2">
Controller1 {{vm1.value}} Controller2 {{vm2.value}}
<button ng-click="vm1.add()">Add Ctrl1</button>
<button ng-click="vm2.add()">Add Ctrl2</button>
</div>
</script>
</div>
In my web application i start feeling the need to pass objects from one page controller to another, not just parameters in the route. I think that this can make my application faster and the code clean. By page controllers i mean controllers corresponding to $routeProvider routes.
I think i can easily implement this with a service, say pageBroker. This service will take a route and an object, and load the page corresponding to the route:
pageBroker.load('/assign-task/', { task: { ... } });
That page would find the object in a special section of the service:
pageBroker.pageData.task;
Page data can be reset for every route change.
This is not an implementation problem, but a software design problem. Usually similar questions get closed on Stack overflow, but i will try once again: is this a good or a bad idea? Is it suitable for the Angular architecture? Why yes or why not?
From my experience with AngularJS, I would say using Services is a good approach, always worked well for me. (But if anyone thinks differently, I will be more than happy to hear your thoughts on this)
var myApp = angular.module('myApp', []);
myApp.factory('Data', function() {
return {message: "I'm data from a service and I can be shared between multiple controllers"}
})
function FirstCtrl($scope, Data){
$scope.data = Data;
}
function SecondCtrl($scope, Data){
$scope.data = Data;
}
and in your view you can use it like
<div ng-app="myApp">
<div ng-controller="FirstCtrl">
<input type="text" ng-model="data.message">
<h1>{{data.message}}</h1>
</div>
<div ng-controller="SecondCtrl">
<input type="text" ng-model="data.message">
<h1>{{data.message}}</h1>
</div>
</div>
You can even bind the Data.message to a model in the controller scope and whenever you change the model, the data-binding will change also on the Service and consequently on the other Controller (But sometimes you might need to call $apply).
This video might be useful to you: https://egghead.io/lessons/angularjs-sharing-data-between-controllers
I am making first steps in Angular.JS and faced the problem when variable available in scope is no longer available in router view. Setup is as follows:
var vApp = angular.module('appG', ['ngRoute', 'appG.directives'])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when('/', {templateUrl: '/partials/form.html', controller: 'ctrlMain'})
.when('/welcome', {templateUrl: '/partials/welcome.html', controller: 'ctrlMain'})
.otherwise({redirectTo: '/'});
});
validationApp.controller('ctrlMain', function($scope, $http, $location) {
$scope.user = {};
$scope.submitForm = function () {
$http.post('/signup', $scope.user).
success(function(data) {
$location.url('/welcome');
});
};
$scope.submitData = function () {
if ($scope.signupForm.$valid) {
$scope.submitForm();
} else {
$scope.signupForm.submitted = true;
}
};
});
HTML code:
<html ng-app="appG">
<div ng-controller="ctrlMain">
<ng-view></ng-view>
</div>
</html>
form.htm (just part of it):
<form name="signupForm" id="signupForm" ng-submit="submitForm()" novalidate>
<input type="text" class="text" ng-class="{'submit-error' : signupForm.submitted}" name="uFirst" placeholder="First name" ng-model="user.first" required/>
<i class="fa fa-sign-in pull-left"></i>Submit
</form>
welcome.html:
<p>{{user.first}}, thank you! </p>
Partial form.html contains form code with basic validation and works ok, partial welcome.html contain simple with texts containing {{user.first}} (user has binding from form and name is a property of the user object. The problem is that {{user.first}} is always empty in welcome.html partial. I would appreciate any help to figure out how it is possible to pass value of the $scope.user to the second partial. Thank you!
New scopes are created whenever you define a route/controller. Your two controllers have completely seperate $scope objects. In this case they're called "sibling" scopes. A variable defined in ctrlMain won't be available in welcome even though you use the same controller reference. The router is actually creating a fresh instance of ctrlMain.
The quick easy fix is to inject $rootScope and set $rootScope.user instead of $scope.user. $rootScope is a special scope that is the parent of all scopes. The values will be "inherited" by all child scopes in your entire app.
Obviously that's not that efficient and usually frowned upon, so the proper way is usually to use a service. You'll have to read up on creating a service, but you'd then inject the service and set your user object there. In fact, your HTTP stuff would go into the service as well.
An option that I often use, which follows the same pattern as most server side MVC frameworks (like Rails) is to create a controller called 'ApplicatonController' , and set that on your body tag, with ng-controller="ApplicationController"
Your MainCtrl will load inside the body tag, so it will inherit anything found in the parent controller.
It's called controller nesting.
You create a controller called "ApplicationController" as you have your MainCtrl, except now you can access properties and methods from the application controller from within the MainCtrl without the need to inject anything additional like $rootScope.
The hierarchy now goes
$rootScope -> ApplicationController -> MainCtrl
I find this approach is easier to wrap your head around if you're familiar with frameworks like Rails that follow the same structure.
I also like to avoid over confusing things by creating a service for everything, because you end up with huge dependency injection strings which become a hassle to manage.
Controller nesting is probably not going to be the most "Angular" way of doing it, but it solves your problem with minimal effort.
I have a route defined as
$routeProvider.when('/:culture/:gameKey/:gameId/closed', { templateUrl: '/templates/tradingclosed', controller: TradingClosedCtrl });
I would like angular to include the "culture" parameter when requesting the template somehow, so I can serve a translated template.
Is this possible?
If I'm reading this correctly you'd like to somehow use the culture parameter from the url route to determine which location to retrieve your template.
There may be a better way but this post describes retrieving the $routeParams inside a centralized controller with ng-include to dynamically load a view.
Something similar to this:
angular.module('myApp', []).
config(function ($routeProvider) {
$routeProvider.when('/:culture/:gameKey/:gameId/closed', {
templateUrl: '/templates/nav/urlRouter.html',
controller: 'RouteController'
});
});
function RouteController($scope, $routeParams) {
$scope.templateUrl = 'templates/tradingclosed/' + $routeParams.culture + '_template.html';
}
With this as your urlRouter.html:
<div ng-include src="templateUrl"></div>
You can define the controller you want to load in your views using ng-controller and access the $routeParams for the additional route parameters:
<div ng-controller="TradingClosedCtrl">
</div>
I've posted similar question with working Plnkr example of solution like #Gloopy suggested.
The reason why you can't implement that without ng-include is that routing is done in 'configuration' block, where you can't inject any values (you can read about these blocks in Modules documentation, section Module Loading & Dependencies
If you want not to introduce new scope, you can replace ng-include with my stripped version of ng-include directive, that do absolutely same that ng-include does, but do not create new scope: source of rawInclude directive
Hope that solution will satisfy your use case.