TypeScript minified breaks Angular UI Modal - angularjs

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.

Related

UI Router resolve not working with component-based states

Background:
On an app I am working on, we have a component that is being used in two places. In one place it's being called from the Material Design bottomSheet system. In another, we are using the component directly via the ui-router state system.
Here's the setup that's causing trouble. I've already got an angular.module statement that has all the proper package dependencies set up - I've been working on this app for months with my team, the problem is specifically the code below, which is what I've just added.
routes.ts
namespace Main {
RouteConfig.$inject = ['$stateProvider'];
function RouteConfig($stateProvider) {
$stateProvider
.state('main.myAwesomeFeature', {
url: '^/myawesomefeature',
component: 'awesomefeature',
resolve: {
awesomeDefaults: () => new Controllers.AwesomeDefaults(1, 2, 3)
}
});
// Other routing minutiae, unimportant to the question
}
angular.module('app').config(RouteConfig)
}
awesomefeature.ts
namespace Controllers {
export class AwesomeDefaults {
public constructor(
number1: number,
number2: number,
number3: number
) {
}
}
export class AwesomeFeatureCtrl {
public static $inject: string[] = [
'awesomeDefaults'
];
public controller(
public awesomeDefaults: AwesomeDefaults
) {
}
// ...Other methods and irrelevant stuff...
}
angular
.module('app')
.controller('awesomeFeatureCtrl', AwesomeFeatureCtrl);
}
namespace Components {
export var awesomeFeatureCmpt: ng.IComponentOptions = {
bindings: {},
controller: 'awesomeFeatureCtrl',
controllerAs: '$ctrl',
templateUrl: '(Irrelevant, as is the HTML)'
};
angular
.module('app')
.component('awesomefeature', awesomeFeatureCmpt);
}
Problem:
Whenever I try to navigate directly to the 'Awesome Feature', not only does my HTML not render, I receive the following console error:
angular.js:14525 Error: [$injector:unpr] Unknown provider: awesomeDefaultsProvider <- awesomeDefaults <- awesomeFeatureCtrl
http://errors.angularjs.org/1.6.4/$injector/unpr?p0=awesomeDefaultsProvider%20%3C-%20awesomeDefaults%20%3C-%20awesomeFeatureCtrl
at angular.js:66
at angular.js:4789
at Object.getService [as get] (angular.js:4944)
at angular.js:4794
at getService (angular.js:4944)
at injectionArgs (angular.js:4969)
at Object.invoke (angular.js:4995)
at $controllerInit (angular.js:10866)
at nodeLinkFn (angular.js:9746)
at angular.js:10154
It appears that for whatever reason, $stateProvider.state({resolve}) isn't properly resolving my awesomeDefaults and injecting the value into the awesomeFeatureCtrl.
Question:
Why isn't resolve working as I recall that it should?
To my understanding, the resolve object takes each named index on it, runs whatever function is on it, and then resolves it into the controller of the thing in the route, as per the UI Router Documentation. It's obvious I'm mis-remembering or mis-understanding something.
After looking at your error more closely, I’ve run into this issue before. Try changing this
resolve: {
awesomeDefaults: () => new Controllers.AwesomeDefaults(1, 2, 3)
}
To this
resolve: {
awesomeDefaults: /** ngInject */ () => new Controllers.AwesomeDefaults(1, 2, 3)
}
To properly inject awesomeDefaults.

Unit Testing AngularJS 'config' Written in TypeScript using Jasmine

I'm currently trying to Unit Test the config of a new AngularJS component. We are using ui-router to handle the routing in our application. We have been able to successfully test it for all our previous components, but the code for all of them was written in plain Javascript. Now that we switched to TypeScript we are having some issues.
This is the TypeScript code where we make the configuration of the module:
'use strict';
// #ngInject
class StatetiworkpaperConfig {
constructor(private $stateProvider: ng.ui.IStateProvider) {
this.config();
}
private config() {
this.$stateProvider
.state('oit.stateticolumnar.stateticolumnarworkpaper', {
url: '/stateticolumnarworkpaper',
params: { tabToLoad: null, groupTabId: null, jurisdiction: null, showOnlyItemsWithValues: false, showOnlyEditableItems: false},
template: '<stateticolumnarworkpaper-component active-tab-code="$ctrl.activeTabCode"></stateticolumnarworkpaper-component>',
component: 'stateticolumnarworkpaperComponent',
resolve: {
onLoad: this.resolves
}
});
}
//#ngInject
private resolves($q, $stateParams, ColumnarWorkpaperModel, ChooseTasksModel, localStorageService) {
// Some not important code
}
}
angular
.module('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper')
.config(["$stateProvider", ($stateProvider) => {
return new StatetiworkpaperConfig($stateProvider);
}]);
This is the Spec file, which is written in Javascript:
describe('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper', function () {
beforeEach(module('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper'));
beforeEach(module('oit'));
var state = 'oit.stateticolumnar.stateticolumnarworkpaper';
it('has a route', inject(function ($state) {
var route = $state.get(state);
expect(route.url).toBe('/stateticolumnarworkpaper');
}));
});
My issue is when executing the line var route = $state.get(state), as the route variable is always null. I could verify that the config() method is being executed, but I'm simply out of ideas as to why route is always null on my test.
Just for reference, this is the configuration of another component, but using Javascript
'use strict';
angular
.module('oit.components.binders.binder.dom_tas.taxaccountingsystem.stateworkpapers.stateworkpapersreview')
.config(stateworkpapersreviewConfig);
function stateworkpapersreviewConfig($stateProvider) {
$stateProvider
.state('oit.binder.taxaccountingsystem.stateworkpapersreview', {
url: '/stateworkpapersreview?reviewType&binderId&year&jurisdiction&chartId&withBalance',
templateUrl: 'components/binders/binder/dom_tas/taxaccountingsystem/stateworkpapers/stateworkpapersreview/stateworkpapersreview.tpl.html',
controller: 'StateworkpapersreviewController',
controllerAs: 'stateworkpapersreviewCtrl',
resolve: {
onLoad: resolves
}
});
function resolves($q, $stateParams, StateTiBinderJurisdictionsModel, WorkpaperModel, localStorageService, StateTiFiltersModel) {
// Some not important code
}
}
As you can see the code is basically the same, but still, I can successfully test this component's config in the way I described, but when I try with the one written in TypeScript I get the error I mentioned.
PD: I'm aware of several similar posts (like this one), but none of them deal with TypeScript, which is my issue.
There is huge difference between the TS snippet and the JS one.
I’m not sure why you are using a class to elite a function? .config suppose to get a function.
You can write the same code as in JS just with .ts suffix, it is a valid TS code.
Then you just can import that config function, pass it all the injectables and test it.

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

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.

AngularJS: route.resolve in ES6

I am trying to refactor an existing Angular-Project to use ES6-Modules and import statements. This works for the majority of the application, but the resolve blocks in my routes are giving me trouble.
As far as I can tell the syntax should be the same as before:
$stateProvider.state('stateName'), {
template: <div></div>
controller: 'stateCtrl'
controllerAs: 'ctrl'
resolve: {
someData: ['DataService', function(DataService){
return DataService.getData();
}]
}
}
However setting a breakpoint in someData tells me that my 'DataService' is not resolved properly by angulars dependency injection (or rather: it is resolved properly, but was not initialized yet). The service gets registered on the according module, but its constructor did not get called before entering the someData-function.
Since i thought that i got the syntax wrong i experimented a bit and found that some other Services (created with the same syntax and registered on the same module as the DataService) are actually initialized and injected properly within the resolve-block.
Do you have any idea where to look or how i can troubleshoot this?
If you're using ES6, it should be like this:
/* #ngInject */
export default function config($stateProvider) {
resolve: {
/* #ngInject */
someData: (DataService) => DataService.getData()
}
}
To read a little more about ngInject see the ng-annotate github page. It does the injections for you.

Refreshing Resolve Data - Ui Router

In ionic I'm resolving my data with the Ui-Router's resolve functionality before the controller is initialized. As of now I don't have to $inject my EventService into the Controller. The EventService's getEvents() method resolves the data before the controller is initialized. Everything works correctly this way, but now i'm trying to implement the Ion Refresher. I could easily refresh my $scope.events array within the controller, bloating the controller itself, because I would have to $inject the EventService into the controller, and that also means that every controller that uses the same data will have to contain logic to handle a refresh. What is the best way refresh the data outside of the controller or is that the best way?
Events State Definition and data resolution
.state('tab.events', {
url: '/events',
views: {
'tab-event': {
templateUrl: 'views/events.html',
controller: 'EventsController',
resolve: {
events: function (EventService) {
return EventService.getEvents(); //resolves data before ctrl initialized
}
}
}
}
})
Events Controller
(function() {
'use strict'
angular
.module('app.events')
.controller('EventsController', EventsController);
EventsController.$inject = ['$scope','events'];
function EventsController ($scope,events) {
$scope.events = events;
}
}
)();
Bloated Events Controller - Example
(function() {
'use strict'
angular
.module('app.events')
.controller('EventsController', EventsController);
EventsController.$inject = ['$scope','events','EventsService'];
function EventsController ($scope,events,EventsService) {
$scope.events = events;
$scope.refresh = refresh;
function refresh () {
clearCache(); //pretend method
EventsService.getEvents()
.then(function (events) {
$scope.events = events;
$scope.$broadcast('scroll.refreshComplete');
})
}
}
}
)();
Rather than bloating the controller can I refresh this data another way?
call $state.reload() which is an alias for:
$state.transitionTo($state.current, $stateParams, {
reload: true, inherit: false, notify: true
});
This will cause all your states to be "re-entered" which fetches the resolves and re-initializes the controllers.
I wish a hard refresh, which is basically what a $state.reload() does wasn't the answer. I too have this issue and would rather be able to call some method that just forces all the resolved data objects to rerun. The reload causes a page refresh, which causes nasty UI artifacts.

Resources