Skip to content
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

Can't access future state children #384

Closed
LM-G opened this issue Sep 9, 2018 · 12 comments
Closed

Can't access future state children #384

LM-G opened this issue Sep 9, 2018 · 12 comments
Labels

Comments

@LM-G
Copy link

LM-G commented Sep 9, 2018

@christopherthielen We cannot target a substate defined inside a lazy loaded state which hasn't been fetched yet.

If i have a future state with a child state :

// lazy.router.ts
const states: Ng2StateDeclaration[] = [  
  {  
    name: 'lazy',  
	url: '/lazy',
	component: LazyComponent
  },
  {  
    name: 'a',  // child state
    parent: 'lazy',
	url: '/a',
	component: LazyAComponent
  }
]
export LazyRouterStates: ModuleWithProviders = UIRouterModule.forChild({ states })

included in this module :

// lazy.module.ts
@NgModule({  
  imports: [SharedModule, LazyRouterStates], //shared module exports CommonModule and some other cool stuff  
  declarations: [LazyComponent, LazyAComponent]  
})  
export class LazyModule {}

lazy loaded in my main router:

// app.router.ts
const states: Ng2StateDeclaration[] = [  
  {  
    name: 'lazy.**',  
	url: '/lazy',  
    loadChildren: './lazy/lazy.module#LazyModule'  
  }  
];
  
export const RootRouterStates: ModuleWithProviders = UIRouterModule.forRoot({ states, config });

referenced in the main module:

@NgModule({  
  declarations: [AppComponent],  
  imports: [BrowserModule, SharedModule, RootRouterStates],  
  providers: [{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }],  
  bootstrap: [AppComponent]  
})  
export class AppModule {}

I cannot reach the lazy.a state from my main component:

<!-- app.component.html -->
<a uiSref="lazy">Lazy</a> <!-- works and creates http://some-adresse/lazy link -->
<a uiSref="lazy.a">Lazy > A</a> <!-- doesn't work and creates http://some-adresse/lazy link -->

Since lazy.a state url is unknown when the router definition hasn't been fetched from the server, StateService.href(...) used in the uiSref directive update() function can't return the correct href because it has no means to know it.
Same thing when you manually try to reach the state with some click function calling StateService.go(...) :

// stateService injected in the component ctor
someOnClickFn(): void {
  this.stateService.go('lazy.a'); //doesn't work
}

It would be nice to pass a relative or absolute url or some kind of option to the uisref to achieve this. For example in this case something like:

<a uiSref="lazy.a" [uiOptions]="{ lazyPath: '/a' }">Lazy > a</a>
<!-- or directly the path -->
<a uiSref="/lazy/a">Lazy > a</a>

Thanks in advance.

@wilmarques
Copy link

You gave the name a for the child state and was looking for lazy.a, which doesn't exist.

Try changing it to this:

{  
    name: 'lazy.a',  // child state
    url: '/a',
    component: LazyAComponent
}

@LM-G
Copy link
Author

LM-G commented Sep 11, 2018

@wileymarques You missed the parent: 'lazy' instruction.

Declaring:

{  
    name: 'lazy.a',  // child state
    url: '/a',
    component: LazyAComponent
}

is the same as declaring:

{  
    name: 'a',  // child state
    parent : 'lazy', // parent state
    url: '/a',
    component: LazyAComponent
}

@wilmarques
Copy link

Yes, it's almost the same.

The difference is simply the name.

@Xen3r0
Copy link

Xen3r0 commented Jul 11, 2019

You have found the solution ?
I have same problem.

Thanks

@ebrentnelson
Copy link

ebrentnelson commented Oct 2, 2019

I'm running into the same issue. It seems that we're providing enough information in our state declarations that the URLs should be able to be generated. I wrote some tests against the ui-router code to attempt to fix this, but unfortunately the issue runs pretty deep. The problem here is that lazy.a doesn't even seem to exist in the state registry. My guess is that perhaps it is hidden somewhere in the future state of lazy. Because of this, when href generation happens it can't find the child state and instead the URL matcher uses the parent state instead and you get a URL pointing to the parent (which seems wrong).

To get around this I started writing my own wrapper around this using a URL registry that I would keep track of and then use the router's urlMatcherFactory to compile the URL and fill in params and stuff.... but then I noticed that a bunch of stuff is now deprecated for public use.

So I'm stuck as well. I am probably going to go back into the core ui-router code and see what I can do about flattening future states so that lazy.a can be matched during URL generation... unfortunately it'll likely mean significant changes to how ui-router does things and that scares me a bit haha.

EDIT:
I should note that my use case is a little different. I am registering my states ahead of time as future states (I register lazy.** and lazy.a.**). Which works as intended except for URL generation (that is why I made the statement about knowing everything we need to about what the URL should be.)

@roelofjan-elsinga
Copy link

roelofjan-elsinga commented Dec 13, 2019

This is a very old thread but bare with me: This is issue is to be expected for the following reason:

The state is lazy loaded through future states, so when you haven't loaded the module containing the state implementations, the router that's trying to resolve the lazy-loaded state has no clue the lazy-loaded state even exists.

When you're resolving the lazy state through uiSref="lazy", you will get the correct URL, as the "lazy.**" default URL is /lazy, which is the same as the "lazy" state. The module creating the URL for the "lazy" state doesn't actually know that state "lazy" exists, but it does know a future state "lazy.**" exists and just generates a URL for that state instead. This just happens to be identical to "lazy".

So in short, you're right, you can't access the future state children if the module has not been loaded yet, because the router has no clue those states even exist.

I'd suggest closing this issue as this is expected behavior. It does need some kind of service to be able to generate URLs for lazy loaded states though because it's inconvenient dealing with this situation.

@wawyed
Copy link
Member

wawyed commented Dec 13, 2019

@roelofjan-elsinga I suppose alternatively ui-router could try to load the lazy loaded module in an attempt to resolve the url for a lazy loaded state, although that might defeat the purpose of lazy load...

@christopherthielen
Copy link
Member

In the future state paradigm, a single placeholder state is loaded up front. This placeholder state has wildcards which match all child states (name: 'lazy.**') and match all child URLs (url: '/lazy*'). When a URL is activated which matches /lazy*, or when the a uiSref is activated to a state name matchinglazy.**, the router runs the future state's lazy load code. Upon success, the the future state lazy.** is first deregistered and the newly lazy loaded states are registered.

the router that's trying to resolve the lazy-loaded state has no clue the lazy-loaded state even exists.

As @roelofjan-elsinga mentions, the router doesn't know the details of the lazy states until after they have actually been lazy loaded. If the goal is to have correct URLs in links for lazy loaded states, perhaps we can eagerly provide that information to the router.

A new lazy load paradigm

Instead of using the future state paradigm, the lazy module can eagerly provide partial state definitions to the router. The partial definitions should be just enough to generate proper URLs. When any of those states are activated, the module code can be lazy loaded, the partial state definitions deregistered, and the full state definitions registered.

The eagerly provided partial state definitions can be as simple as a state name and url.

export const placeholder = { name: "placeholder", url: "/placeholder" };
export const placeholderChild = { name: "placeholder.child", url: "/child" };
export const placeholderNest = { name: "placeholder.child.nest", url: "/nest" };

This would allow the router to generate proper urls for, e.g., placeholder.child.nest.

When the full module is lazy loaded, it can provide full state definitions, including components and resolves. It can either duplicate the name and url properties, or it could copy them from the eager definitions so as not to have two sources of truth.

import * as eager from './eager.states';
export const placeholder = {
  ...eager.placeholder,
  component: Lazy1Component,
  resolve: ...
}
export const placeholderChild = {
  ...eager.placeholderChild,
  component: Lazy2Component,
  onEnter: ...
}

...

However, we would still need a hook that lazy loads the module, deregisters the partial states, and loads the lazy NgModule. Unfortunately, unlike the lazyLoad callback on a state, the loadChildren callback doesn't provide any arguments to allow state deregistration. However, by examining the implementation of loadChildren, we can work around this deficiency by supplying our own lazyLoad function that does all the magic. This callback only needs to be on the root state of the placeholdert state tree because lazyLoad is an entering hook.

Our new state definition looks like:

export const placeholder = {
  name: "placeholder",
  url: "/placeholder",
  // This hack replaces the `loadChildren` with a `lazyLoad` block.
  // Unlike loadChildren, lazyLoad has access to the current Transition object.
  // In a future version of uirouter/angular, perhaps loadChildren would have access to the Transiton.
  lazyLoad: (transition, stateObject) => {
    const { stateRegistry } = transition.router;
    // loadNgModule is an internal detail of the `loadChildren` implementation.
    // See https://github.com/ui-router/angular/blob/master/src/statebuilders/lazyLoad.ts
    const callback = loadNgModule(() => import("./placeholder.module")
      .then(m => {
        // Deregister the eager placeholders before loading the lazy loaded NgModule.
        stateRegistry.deregister('placeholder');
        return m.PlaceholderModule;
      }));
    return callback(transition, stateObject);
  },
};

One more detail to consider... when defining the lazy half of these states, if we copy all the properties from the eager portion ({ ...eager.placeholder, }), we've now copied the lazyLoad function as well. This can cause an infinite lazy loading loop which we don't want, so filter out the lazyLoad property from the full state.

export const placeholderRoot = { 
  // Copy the details from the eager half
  ...eager.placeholder,  
  // Add any lazy loaded stuff
  component: Lazy1Component,
  // Set layLoad to undefined so we do not infinitely loop attempting to
  // lazy load the root placeholder tree
  lazyLoad: undefined,
};

Example

I've created a proof of concept on stackblitz that demonstrates both paradigms (one future state tree and one eager/lazy placeholder tree)

https://stackblitz.com/edit/uirouter-angular-lazy-load-placeholders?file=src%2Fplaceholder%2Fplaceholder.states.eager.ts

I'd like to hear your thoughts

@LM-G
Copy link
Author

LM-G commented Dec 17, 2019

It does the trick i guess.

In a future version of uirouter/angular, perhaps loadChildren would have access to the Transiton. Would be amazing 👍.

@stale
Copy link

stale bot commented Jun 14, 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 Jun 14, 2020
@stale stale bot closed this as completed Jun 28, 2020
@cloakedninjas
Copy link

Run into this myself just now 😞

Was linking to a lazy-loaded module from a root module never a use-case consideration ?

@christopherthielen
Copy link
Member

@cloakedninjas

There are two things being discussed in this issue:

  1. The original reporter tried to access a state via the name lazy.a but the state was actually named a. Children of future states should be prefixed by the future state name (instead of { name: 'a', parent: 'lazy' } it should be name: 'lazy.a')
  2. Sref URLs for not-yet-loaded future states are not rendered with the full URL. When the lazy module is loaded, the URLs are then updated.

To be clear: Srefs from the root module to lazy loaded modules are supported, with the caveats above.

I commented with a workaround to address issue #2

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

8 participants