Angular UI router doesn't process the resolve function when i use async/await feature?

I've been trying to render certain template related to a state and component according to this article

In my project running under dev-server it all works fine and when I execute $state.go("home") the component template is loaded how I expect but when I do this in a testing environment, this doesn't work.

Before, in testing, when I use the "old way" using "template" instead "component" with ui-router, execute $rootScope.$digest() was enough for add the template inside the <div ui-view></div> but using this new way this doesn't work anymore.

What am I doing wrong?

Edit: I've been trying to deeply understand the problem and I see that the problem is related to the HTTP request that was done. Maybe it's related to the way that my promise resolves on the resolve callback using async/await. Please check the Service:

Service

export class TodoService {
    constructor($http, BASE_URL) {
        this.http = $http;
        this.url = `${BASE_URL}/todos`
    }
    async getTodos() {
        const apiResponse = await this.http.get(this.url)
        return apiResponse.data.todos
    }
}

Router

import '@uirouter/angularjs'

export function routes($stateProvider, $locationProvider) {
    $locationProvider.html5Mode({
        enabled: true,
        requireBase: false,
        rewriteLinks: true,
    })

    $stateProvider
        .state("home", {
            url: "/",
            component: "todoList",
            resolve: {
                todosList: TodoService => TodoService.getTodos()
            }
        })
}

Test

import { routes } from "routes"
import { TodoListComponent } from "components/todoList.component"
import { TodoService } from "services/todo.service"

describe("TodoListComponent rendering and interaction on '/' base path", () => {
    let componentDOMelement
    let stateService

    beforeAll(() => {
        angular
            .module("Test", [
                "ui.router"
            ])
            .config(routes)
            .constant("BASE_URL", "http://localhost:5000/api")
            .component("todoList", TodoListComponent)
            .service("TodoService", TodoService)
            //I enable this for better logs about the problem
            .run(['$rootScope','$trace', function($rootScope, $trace) {
               $trace.enable("TRANSITION")
             }])
    })
    beforeEach(angular.mock.module("Test"))

    beforeEach(inject(($rootScope, $compile, $state, $httpBackend) => {
        //build the scene
        //1st render the root element of scene: We needs a router view for load the base path
        let scope = $rootScope.$new()
        componentDOMelement = angular.element("<div ui-view></div>")

        $compile(componentDOMelement)(scope)
        scope.$digest()
        
         document.body.appendChild(componentDOMelement[0]) //This is a hack for jsdom before the $rootScope.$digest() call
        //2nd let's create a fake server for intercept the http requests and fake the responses
        const todosResponse = require(`${__dirname}/../../stubs/todos_get.json`)
        $httpBackend
            .whenGET(/.+\/todos/)
            .respond((method, url, data, headers, params) => {
                return [200, todosResponse]
            })

        //3rd Let's generate the basic scenario: Go at home state ("/" path)
        $state.go("home")
        $rootScope.$digest()
        $httpBackend.flush()
    }))

    it("Should be render a list", () => {
        console.log("HTML rendered")
        console.log(document.querySelectorAll("html")[0].outerHTML)
    })
})

The HTML result that not rendering

<html>
<head>
<style type="text/css">
@charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate) {
  display:none !important;
}
ng\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{
  position:absolute;
}
</style>
</head>
<body><!-- uiView: -->
</body>
</html>

Also, I traced the stateChange before the HTML:

console.log node_modules/@uirouter/core/_bundles/ui-router-core.js:1276
    Transition #0-0: Started  -> "Transition#0( ''{} -> 'home'{} )"

console.log node_modules/@uirouter/core/_bundles/ui-router-core.js:1282
    Transition #1-0: Ignored  <> "Transition#1( ''{} -> 'home'{} )"

console.log node_modules/@uirouter/core/_bundles/ui-router-core.js:1313
    Transition #1-0: <- Rejected "Transition#1( ''{} -> 'home'{} )", reason: Transition Rejection($id: 0 type: 5, message: The transition was ignored, detail: "undefined")

I see a problem in a transition but no reason was given.

========================================================================

Edit 2 Finally we found the problem but I can't figure out the real problem. I created a branch in my project for showing the problem. This it's related to async/await javascript feature:

export class TodoService {
    constructor($http, BASE_URL) {
        this.http = $http;
        this.url = `${BASE_URL}/todos`
    }
    //Interchange the comment on the getTodos method and run `npm run tdd` for see the problem:
    //When async/await doesn't used, the html associated to the resolve in the
    // "/" route that used this service, the promise was resolved that expected.
    //The idea for this branch it's research about the problem and propose a way
    //for we can use async/await on the production code and on the testing environment
    async getTodos() {
        const apiResponse = await this.http.get(this.url)
        return apiResponse.data.todos
    }
    // getTodos() {
    //     return this.http.get(this.url).then(res => res.data.todos)
    // }
}

The repository

So my new Questions are:

Edit 3 The issue 3522 reported in angular UI router repository

Upvotes: 38

Views: 2561

Answers (2)

jovi De Croock
jovi De Croock

Reputation: 615

The issue is that angular expects an angular Promise that's why your then will work but your await won't, you can solve this by using a library like: https://www.npmjs.com/package/angular-async-await or make a construction like they're demonstrating here https://medium.com/@alSkachkov/using-async-await-function-in-angular-1-5-babel-6-387f7c43948c

Good luck with your problem!

Upvotes: 1

AL the X
AL the X

Reputation: 1051

This is just an educated guess based on my understanding of the way resolve-ers work and what ngMock does. In your first example, your resolve for getTodos does not return until the $http promise has resolved, at which point you pluck the value off the response and return it. However, resolve-ers expect a $q.Promise value as a sentinel to hold the rendering of the router until it resolves. In your code, depending on how it's transpiled, the await and return call likely doesn't produce the correct sentinel value, so it's treated like a synchronous response.

One way to test would be to request the resolve-er in the controller for your todolist component and inspect the value. I'll bet it's NOT a $q.Promise, although it might be a native Promise.

When using resolve, though, just chain the Promise by adding a then and return it. The router will handle the rest.

Or better, switch to Observables! (/me ducks incoming tomatoes)

Upvotes: 0

Related Questions