Why doesn't $onInit() fire in an angular material dialog? - angularjs

Background:
On a project I'm working on, we've adopted the Angular Material Design framework for the UI part of the system. We're using this in conjunction with AngularJS 1.6.2, and TypeScript 2.1.5.
One piece of advice we keep encountering, is to use the AngularJS lifecycle hooks, to be ready for when AngularJS gets deprecated in favor of Angular 2. Specifically, the lifecycle hooks are introduced to make it easier to upgrade, since these hooks - of which $onInit() is a member - are part of the purely component-based setup that is a main feature of Angular 2.
Problem:
We've found that, when you define a dialog controller that implements angular.IController, and has $onInit() defined with contents...those contents are not executed.
Problem Example TypeScript:
// Bootstrap Angular to our example.
class App {
public constructor() {
angular.module('app', ['ui.router', 'ngMaterial']);
}
}
let app: App = new App();
// Set up routing for the example...
class RouteConfig {
constructor(
$stateProvider: angular.ui.IStateProvider,
$urlRouterProvider: angular.ui.IUrlRouterProvider,
$locationProvider: angular.ILocationProvider
){
$stateProvider.state('Main', {
url: '/Main',
templateUrl: 'app/Main.html',
controller: 'mainController'
});
$urlRouterProvider.otherwise('/Main');
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
}
}
angular.module('app').config(['$stateProvider', '$urlRouterProvider', '$locationProvider', RouteConfig]);
// Controller for the page that will launch the modal...
class MainController implements angular.IController {
public static $inject: string[] = ['$mdDialog'];
public constructor(public $mdDialog: angular.IDialogService) {
}
public $onInit(): void {
console.log('This works.');
}
public onButtonClick(): void {
this.$mdDialog.show({
controller: ModalController,
controllerAs: '$ctrl',
templateUrl: '/App/myModalTemplate.html', // Contents of modal not important.
parent: angular.element(document.body),
fullscreen: true
})
.then(() => console.log('Modal closed.'),
() => console.log('Modal cancelled.'));
}
}
angular.module('app').controller('mainController', MainController);
// Controller for the actual modal that should be triggering $onInit.
class ModalController implements angular.IController {
public static $inject: string[] = ['$mdDialog'];
public constructor(public $mdDialog: angular.IDialogService) {
alert('ModalController.ctor fired!');
}
public $onInit(): void {
// PROBLEM! This never actually executes.
alert('$onInit fired on modal!');
}
public ok(): void {
this.$mdDialog.hide();
}
}
angular.module('app').controller('modalController', ModalController);
app/Main.html
<div ng-controller="mainController as $ctrl"
md-content
layout-padding>
<h4>Modal Lifecycle Hook Problem Example</h4>
<md-button class="md-raised md-primary"
ng-click="$ctrl.onButtonClick()">
Show Modal
</md-button>
</div>
app/myModalTemplate.html:
<md-dialog>
<md-toolbar class="md-theme-light">
<h2>Modal</h2>
</md-toolbar>
<md-dialog-content class="md-padding">
Stuff, y'all.
</md-dialog-content>
<md-dialog-actions>
<md-button class="md-primary"
ng-click="$ctrl.ok()">
OK
</md>
</md-dialog-actions>
</md-dialog>
If you set all of this stuff up, and run the project, you'll see a page with a button. When you click the button you will only get one alert - that the constructor has fired. What I was expecting that you'd get is two alerts: the constructor fired message, as well as the $onInit fired message.
Thus, this leads to my...
Question: Why is it that, if a Material Design Dialog controller implements IController, that apparently the lifecycle hooks don't fire?

I'm beginning with AngularJS and I recently had the same issue during my internship. I think that the problem is your ModalController is not called as a component (it's called with $mdDialog) so it does not have any life cycle.
To solve this issue, you have to call explicitly your $onInit method. Maybe you can do that in your ModalController constructor.

In your code you have angular.module('app').controller('modalController', ModalController);. I do not see modalController anywhere in your code so suspect this controller is actually never triggered.

Related

TypeScript minified breaks Angular UI Modal

I have an MVC application with lots of Angular code. I have an Angular service that creates an Angular UI modal. The pertinent code is:
export class ConfirmRenderingOfLargeResultsModalService {
static $inject = ["$uibModal"];
constructor(private $uibModal: any) {
}
public open(options: LargeResultSetModalOptions): ng.IPromise<LargeResultsModalAction> {
var modalInstance = this.$uibModal.open(
{
templateUrl: "myTemplate.html",
controller: "ConfirmRenderingOfLargeResultsModalController",
controllerAs: "confirmRenderingOfLargeResultsModalController",
backdrop: "static",
resolve: {
modalOptions: () => options
}
});
return modalInstance.result.then((action: LargeResultsModalAction) => {
return action;
});
}
}
angular.module("My.Module").service("Services.ConfirmRenderingOfLargeResultsModalService", Services.ConfirmRenderingOfLargeResultsModalService);
The controller (snippet) is:
export class ConfirmRenderingOfLargeResultsModalController {
public confirmRenderingOfLargeResultsModalController: ConfirmRenderingOfLargeResultsModalController = this;
//static $inject = ["$uibModalInstance"];
constructor(private $uibModalInstance: any, private modalOptions: LargeResultSetModalOptions) {
}
public ok(): void {
this.$uibModalInstance.close(LargeResultsModalAction.Continue);
}
// more methods not shown
}
angular.module("My.Module").controller("Services.ConfirmRenderingOfLargeResultsModalController", Services.ConfirmRenderingOfLargeResultsModalController);
Now, in my dev env, this works fine. The modal pops up, the controller has the $uibModalInstance and modalOptions parameters correctly injected. In Production, with bundling enabled, it breaks - in that nothing happens at all. No console error either.
If I uncomment the static inject line on the controller, then it breaks in dev and in Production the modal pops up but doesn't work correctly because presumably it's not the modal instance instantiated by the service as the modalOptions parameter remains undefined.
My question is, how can I minify this code and still have the correct instance of $uibModalInstance injected? Hopefully I'm missing something very simple and all the hair that has been pulled today has been a mere exercise in frustration.
I would suggest that you use what is known as safe or inline dependency injection annotation.
For example in your controller:
angular.module("My.Module")
.controller("Services.ConfirmRenderingOfLargeResultsModalController", ["uibModalInstance", "modalOptions", Services.ConfirmRenderingOfLargeResultsModalController]);
This will preserve the dependency names when you minify your code.

Angular JS Button ng-click doesn't call method

for now I read a lot of post for this problem, but none seems to have a solution.
In my angular app I created a new route with angular-fullstack:route
So here are my controller, template and config file:
controller:
'use strict';
(function() {
class AnmeldungCtrl {
constructor($http, $window, $uibModal, $state) {
}
searchUser(searchString) {
console.log("search Method");
if (searchString.isNaN()) {
this.state = "search";
console.log("search");
//TODO Search for name
}
else if (searchString >= 1000000) {
this.state = "anmeldung";
console.log("anmeldung");
//TODO Search for bar code
}
else {
this.state = "edit";
console.log("edit");
//TODO Search for Participant
}
}
}
angular.module('schwimmfestivalApp').controller('AnmeldungCtrl', AnmeldungCtrl);
})();
template:
<div>
<input type="text" ng-model="query" >
<button ng-click="ctrl.searchUser(query)">Search</button>
{{query}}
</div>
config file:
'use strict';
angular.module('schwimmfestivalApp')
.config(function ($stateProvider) {
$stateProvider
.state('anmeldung', {
url: '/anmeldung',
templateUrl: 'app/anmeldung/anmeldung.html',
controller: 'AnmeldungCtrl',
controllerAs: 'ctrl'
});
});
As I mention in my heading for some reason the method at the controller doesn't get called. And I have no idea why.
At my other routes it does work.
I hope you can give me a hint.
Thanks in advance.
Okay I got it to work. It was a fault at my point (as expected). A colleague of mine created a cnontroller in an other path with the same identifier.
So angular doesn't throw an error if something like this happened. Because the second controller was in a lower path it came at the include after the original controller.
Thanks to #MMhunter who put a console output at the constructor. This wasn't printed in my dev environment. So I started to search why, and found the second controller.
Thanks to all of you for your help.
try using just searchUser(query) it might work because controller is already added no need to call the function using controller

Is it possible to autoscroll in AngularUI Router without changing states?

I'm using AngularUI Router with Bootstrap. I have two views within the same state, and want to scroll to the second view when a button is clicked. I don't need to do any scrolling when the initial state and views load. I want the page to scroll to #start when the "Add More" button is clicked. I've attempted to use $anchorScroll to accomplish this, but am not having any luck.
Any suggestions on a good way to accomplish this?
HTML:
<!-- index.html -->
<div ui-view="list"></div>
<div ui-view="selectionlist"></div>
<!-- list.html -->
<div id="scrollArea"><a ng-click="gotoBottom()" class="btn btn-danger btn-lg" role="button">Add More</a>
<!-- selectionlist.html -->
<div class="row" id="start"></div>
Javascript for Controller:
myApp.controller('SelectionListCtrl', function (Selections, $location, $anchorScroll) {
var selectionList = this;
selectionList.selections = Selections;
this.selectedServices = Selections;
selectionList.gotoBottom = function() {
$location.hash('start');
$anchorScroll();
};
});
Javascript for Routes:
myApp.config(function ($stateProvider, $urlRouterProvider, $uiViewScrollProvider) {
$uiViewScrollProvider.useAnchorScroll();
$urlRouterProvider.otherwise('/');
$stateProvider
.state('selection', {
url: '/selection',
views: {
'list': {
templateUrl: 'views/list.html',
controller: 'ProjectListCtrl as projectList'
},
'selectionlist': {
templateUrl: 'views/selectionlist.html',
controller: 'SelectionListCtrl as selectionList'
}
}
})
Yes, it is possible to autoscroll in AngularUI Router without changing states.
As mentionned previously in my comment, you need to call the scrolling function with an ng-click="gotoBottom()" instead of an ng-click="gotoSection()"
Also, the function definition gotoBottom() must be in the ProjectListCtrl, not in the SelectionListCtrl. This is because the call gotoBottom() happens in the list view:
'list': {
templateUrl: 'list.html',
controller: 'ProjectListCtrl as projectList'
}
As gotoBottom() is called from the list.html view, the corresponding controller in $stateProvider must be the one where you define gotoBottom().
Here are two working ways of accomplishing your goal:
1. You inject $scope inside the controller ProjectListCtrl. You then define the $scope.gotoBottom function in the same controller.
The scope is the glue between the controller and the view. If you want to call a controller function from your view, you need to define the controller function with $scope
app.controller('ProjectListCtrl', function ($location, $anchorScroll,$scope) {
var selectionList = this;
//selectionList.selections = Selections;
//this.selectedServices = Selections;
$scope.gotoBottom = function() {
console.log("go to bottom");
$location.hash('start');
$anchorScroll();
};
});
In the list.html view, you can then call the $scope.gotoBottom function just with gotoBottom(): ng-click="gotoBottom()"
2. Or you use the controller as notation, as when you wrote ProjectListCtrl as projectList.
this.gotoBottomWithoutScope = function(){
$location.hash('start');
$anchorScroll();
};
With this notation, you write this.gotoBottomWithoutScope in the ProjectListCtrl. But in the view, you must refer to it as projectList.gotoBottomWithoutScope().
Please find a working plunker
To learn more about the this and $scope notations, please read this:
Angular: Should I use this or $scope
this: AngularJS: "Controller as" or "$scope"?
and this: Digging into Angular’s “Controller as” syntax

Add/Remove CSS Class on Route Change in Angularjs

I am not sure how this can be achieved in Angular. I want to add and remove CSS class on route change. I am trying to Show and Hide vertical menu. Currently I am using ui-route. Any Suggestion or link to example would be appreciated or any other suggestion on different approach to my problem is also welcome
Easiest and most efficient way:
angular.module(...).run(function($rootScope, $state) {
$rootScope.$state = $state;
});
<div ng-if="$state.contains('someState')">...</div>
This will remove the DOM which will improve performance if the menu has lots of bindings.
However, I constantly tell people to consider leveraging named views for navigation:
<body>
<nav ui-view="nav"></nav>
<div class="container" ui-view></div>
</body>
$stateProvider.state('home', {
views: {
'nav#': {
templateUrl: 'nav.html'
}
'': {
// normal templateUrl and controller goes here.
}
}
});
The cool part about this is that children states can override and control what nav file to use, and can even setup resolves and controllers that share data between the nav and the content. No directives/services needed!
Finally, you can do these too:
<nav ng-show="$state.contains('somestate')"></nav>
<nav ng-class="{someClass:$state.contains('somestate')}"></nav>
Alternatively checkout ui-sref-active
All of my suggestions primarily assume you're using UI-Router since it's the best!
Try this:
app.run(function ($rootScope) {
$rootScope.$on("$stateChangeStart", function(event, toState, fromState){
if (toState.url === "/path") {
$('div').addClass('className');
} else {
$('div').removeClass('className');
}
});
});
You can register the route changed and add this css to your DOM:
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
// Add your logic, for instance:
$('body').addClass('hide-menu');
});
Obviously there are events raised before the route has been changed: "$locationChangeStart", here.
/Edit/ - Better approach
Also I would rather using the ng-class attribute and simple bind a certain value from your main controller to it.
app.controller('MainController', function ($scope) {
$scope.toggleMenu = function(isToShow) {
$scope.isVisibleMenu = isToShow == true;
};
});
then in your html:
<!-- Menu toggle button-->
<button ng-click="toggleMenu()"></button>
<div class="toggleable-menu" ng-class="{'visible-menu': isVisibleMenu}">
<!-- The menu content-->
</div>
and the simplest CSS possbile (you can obviously add animations or any other thing to toggle this menu.)
.toggelable-menu {
display: none;
}
.toggelable-menu.visible-menu {
display: block;
}

Angular-ui ui-router - using multiple nested views

I am trying out the nested views feature of ui-router plugin, but faced the issue I don't know how to solve.
The code that shows the problem can be found at http://jsfiddle.net/3c9h7/1/ :
var app = angular.module('myApp', ['ui.router']);
app.config(function($stateProvider) {
return $stateProvider.state('root', {
template: "<div class='top' ui-view='viewA'></div><div class='middle' ui-view='viewB'></div>"
}).state('root.step1', {
url: '',
views: {
'viewA': {
template: '<h1>View A</h1>'
}
}
}).state('root.step1.step2', {
url: '/step2',
views: {
'viewB': {
template: '<h1>View B</h1>'
}
}
});
});
app.controller('MainCtrl', [
'$scope', '$state', function($scope, $state) {
$state.transitionTo('root.step1.step2');
}
]);
<div ng-app='myApp' ui-view ng-controller='MainCtrl'>
</div>
So, the code activates "root.step1.step2" state by using $state.go method(https://github.com/angular-ui/ui-router/wiki/Quick-Reference#stategoto--toparams--options)
According to ui-router documentation:
When the application is in a particular state—when a state is
"active"—all of its ancestor states are implicitly active as well.
So, I expect that "root.step1" and "root" will be active and it works as expected, but "viewB" is not filled with the template as you can see in jsfiddle sample : the top viewA(root.step1) is OK, but the middle viewB(root.step1.step2) is empty.
What I am doing wrong?
The documentation says:
Child states will load their templates into their parent's ui-view.
So there should be a ui-view='viewB' inside the viewA template, since the parent state of root.step1.step2 is root.step1. Or the viewB should be one of the views of root.step1, and there should be no root.step1.step2.

Resources