Skip to content

Need ability to get updated resolve values in uiOnParamsChanged() #3210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Maximaximum opened this issue Dec 18, 2016 · 9 comments
Closed

Need ability to get updated resolve values in uiOnParamsChanged() #3210

Maximaximum opened this issue Dec 18, 2016 · 9 comments
Labels

Comments

@Maximaximum
Copy link

Maximaximum commented Dec 18, 2016

My state definition goes like this:

        .state('_.projects.view', {
          url: '/{projectId:int}/{folderId:int?}',
          params: {
            folderId: { dynamic: true }
          },
          templateUrl: 'templates/project/projects.view/projects.view.html',
          resolve: {
            project: function (server, $stateParams) {
              return server["Project/Get"]($stateParams.projectId).then(
                function (response) {
                  return response.data;
                }
              );
            },
            folders: function (server, $stateParams) {
              return server["Project/GetFolders"]($stateParams.projectId).then(
                function (response) {
                  return response.data;
                }
              );
            },
            folderBreadcrumbs: function (server, $stateParams) {
              if ($stateParams.folderId) {
                return server["Folder/GetBreadcrumbs"]($stateParams.folderId).then(function (response) {
                  return response.data;
                });
              } else {
                return [];
              }
            },
            folderActiveFn: function (folderBreadcrumbs, folderActiveService) {
              return folderActiveService(folderBreadcrumbs);
            }
          },
          controller: 'ProjectsViewController'
        })

Now, inside my ProjectsViewController, I set

this.uiOnParamsChanged = (newParams, transition) => {
   let breadcrumbs = transition.injector().get("folderBreadcrumbs");
}

When a folderId state param value changes, the url gets updated, the controller doesn't get reloaded and uiOnParamsChanged is called, which is all perfect!

But, inside the uiOnParamsChanged function, breadcrumbs value is not the new updated value but instead it is just the same value it was before the folderId change. This might be exactly what everyone expects in most cases, but it would be really great if we could somehow trigger the state resolves to be re-resolved (perhaps only some of them) and passed to uiOnParamsChanged.

Please note that my resolve system is quite complex (e.g., folderActiveFn resolve depends on folderBreadcrumbs value), and I wouldn't really want to move all of the resolves into the controller, given that ui-router already has an excellent resolve managing infrastructure that only needs to be exposed somehow.

I'm using v1.0.0-beta.3

@christopherthielen
Copy link
Contributor

christopherthielen commented Dec 19, 2016

This proposal has been brought up previously. It's a pretty dramatic departure from the core workings of ui-router. Especially with your desire to only re-fetch "some of the resolves".

I've given it some thought over the last year and my conclusion is that this change would be very intrusive to the code, and would be a huge paradigm shift for users (although it could potentially be made opt-in only)


I also think that Observables are a great existing solution for this approach. In a resolve, return an Observable which fetches data based on a dynamic parameter. When the parameter changes, the observable emits a new object. The Observable is resolved once, and your component can respond to the changes. If a different (non-dynamic) parameter value changes, the entire state can still be reloaded and all other resolves re-fetched.

There's one key piece missing (which is easy to add yourself): the parameter values themselves should be exposed as an observable in a top-level resolve.

I've started working on this rx integration. The ng2 version currently has this code embedded, but I will be publishing this as a Plugin at http://github.com/ui-router/rx by the time ui-router-ng2 is released as 1.0.

To use this you'd have a state something like:

.state('mystate', {
  resolve: {
    userObs: ($transition$, UserService) => {
      let params$ = $transition$.router.globals.params$;
      return params$.map(x => x.userId)
        .distinctUntilChanged()
        .map(id => UserService.get(id))
    }
  },
  component: 'user'
}
.component('user', {
  bindings: { 'userObs': < },
  template: '<h2>{{ $ctrl.user.name }}</h2>',
  controller: function($scope) {
    this.$onInit = () => {
      this.userObs.subscribe((user) => this.user = user);
      $scope.$on("$destroy", () => this.userObs.unsubscribe());
    }
  }

@Maximaximum
Copy link
Author

Thank you, @christopherthielen, for your help!

However, the main feature I'm searching for is managing complex resolve trees, when one resolve depends on the results of other resolves, like in my example folderActiveFn depends on folderBreadcrumbs resolve value. Is there a way to easily get all the newly resolved values using the ui router infrastructure?

PS. For me, it would be perfectly ok, if ALL of the resolves got re-evaluated (not just "some of them", as you have mentioned in your comment).

@christopherthielen
Copy link
Contributor

Is there a way to easily get all the newly resolved values using the ui router infrastructure?

There is not currently a way, without also reloading the views. This is because resolve data has generally been injected into controllers which happens when the views are loaded.

It's theoretically possible to wire ui-router to retain views for views that are reloading and probably a way to tell the views about new resolve data.

However, this is out of scope for main codebase. If you want to take a shot at implementing something like this, I can guide you. However, you will have to dig into the ui-router architecture quite a bit and customize the interactions.

@Maximaximum
Copy link
Author

As a starting point, I think it would be enough if the user simply had an opportunity to manually fire the resolve process and get the new resolve values without affecting other parts of the application (no view/controllers reloads). Passing the new resolve values into uiOnParamsChanged might be a next step.

I'd like to try to implement that and I'd be really grateful for your guidance. Where should I start from?

@christopherthielen
Copy link
Contributor

christopherthielen commented Dec 22, 2016

You can manually invoke resolves yourself using a combination of PathNode and ResolveContext. These are internal APIs.

let's say you want to invoke all resolves for the path of states foo.bar.baz.

 let $state = $uiRouter.stateService;
    let $registry = $uiRouter.stateRegistry;
    let barstate = $state.get('home.foo.bar').$$state();
    
    let pathAsNodes = barstate.path.map(x => new PathNode(x));
    let resolveContext = new ResolveContext(pathAsNodes);
    
    resolveContext.injector().getAsync('barData').then(data => {
      console.log("Got this bar data: " + data)
    });

    // or tell all resolves in the path to load: 
    resolveContext.resolvePath().then(results => {
      console.log("I got this resolve data: ", results);
      console.table(results);
    });

plunker: http://plnkr.co/edit/Unc2X8uBftBlCAwRip81?p=preview

@Maximaximum
Copy link
Author

Thank you @christopherthielen! I've finally achieved what I needed.

In case someone is looking for how to implement it, here is my code inside the controller (in Typescript):

    public uiOnParamsChanged(newParams, transition) {
      this.$stateParams = newParams;

      let toState = transition.to().$$state();
      let pathAsNodes = toState.path.map(x => new PathNode(x));
      let resolveContext = new ResolveContext(pathAsNodes);

      resolveContext.resolvePath().then((results: any[]) => {
        let obj = results.reduce((total, current) => { total[current.token] = current.value; return total; });
        this.update(obj);
      });
    };

    private update({folders, folderActiveFn, folderBreadcrumbs}) {
      this.$scope.folders = folders;
      this.$scope.folderActiveFn = folderActiveFn;
      this.$scope.folderBreadcrumbs = folderBreadcrumbs;

      ...
    };

Now it seems to be quite easy to use, so I doubt if there is any sense in simplifying this somehow. Closing this issue for now.

@Maximaximum
Copy link
Author

Another issue has appeared regarding this.

This dynamic state has got a child state, which needs to access the actual value of one of the parent state's resolves. However if I add a uiOnParamsChanged member to the child controller, it never gets fired.

Please note that the scope are not inherited, so just grabbing the actual value from $scope.folders is not an option.

Any ideas on how to fix this and/or let the child controller get the actual value of a parent controller's resolve?

@Maximaximum Maximaximum reopened this Mar 14, 2017
@christopherthielen
Copy link
Contributor

note: uiOnParamsChanged is not invoked for the initial parameters. It's only triggered for subsequent parameter value changes. Can you inject the initial $transition$ into the child view?

I believe the uiOnParamsChanged callback for the child should get invoked

@stale
Copy link

stale bot commented Jan 24, 2020

This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.

This does not mean that the issue is invalid. Valid issues
may be reopened.

Thank you for your contributions.

@stale stale bot added the stale label Jan 24, 2020
@stale stale bot closed this as completed Feb 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants