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

feat(router): binding from router-outlet to child component #4452

Closed
btford opened this issue Oct 1, 2015 · 58 comments
Closed

feat(router): binding from router-outlet to child component #4452

btford opened this issue Oct 1, 2015 · 58 comments
Labels
effort2: days feature Issue that requests a new feature
Milestone

Comments

@btford
Copy link
Contributor

btford commented Oct 1, 2015

For example:

@Component({ selector: 'foo-cmp' })
@View({
  template: `<router-outlet [prop-passed-to-child]="parentParams"></router-outlet>`,
  directives: ROUTER_DIRECTIVES
})
@RouteConfig([
  { path: '/', component: ChildCmp }
])
class ParentCmp {
  constructor(params: RouteParams) {
    this.parentParams = params;
  }
}

RouterOutlet should be able to pass the prop-passed-to-child binding to ChildCmp.

@btford btford added feature Issue that requests a new feature comp: router effort2: days labels Oct 1, 2015
@PatrickJS
Copy link
Member

👍

1 similar comment
@jakubsta
Copy link

👍

@0x-r4bbit
Copy link
Contributor

Perfect! As a current workaround, @cburgdorf and I introduced and are using "state" components. Those state components are just simply wrapper components that we navigate to and have the actual component that needs data through a binding as view child.

@btford
Copy link
Contributor Author

btford commented Oct 14, 2015

I need help from someone more familiar with Angular Core– so @tbosch, @mhevery, @rkirov, or @vsavkin on how to approach this.

@mrydecki
Copy link

👍

2 similar comments
@Antiavanti
Copy link

👍

@filipows
Copy link

👍

@cburgdorf
Copy link

I don't mean to ruin the party but since there may be several routes and therefore several completely different components behind <router-outlet> Each of those components would want to bind different properties. It may be that I'm getting it wrong but the way it stands now it seems to be only useful if all my routes take the same properties which is most likely not the case.

That is why we recently introduced the concept of state components for a demo app. This is what @PascalPrecht was referring to.

https://github.com/thoughtram/ng2-contacts-demo/blob/9fe0dcb838121294f48584592933db349a6b921c/app/components/contact-detail-state/contact-detail-state-component.ts

The reason we did that is so that we can build components that are kept free from routing concerns (e.g. <contact-list data="listOfContacts"></contact-list>).

@danturu
Copy link

danturu commented Oct 26, 2015

👍

@danturu
Copy link

danturu commented Oct 27, 2015

I totally need this feature or any idea how the point below could be implemented.

Here is a plunk - http://plnkr.co/edit/ZCjWxIz1RSQtIqPfXps8?p=preview. The point is how character from CharacterDashboard could be passed to children components (ShowCharacter, EditCharacter).

@naomiblack naomiblack added this to the beta-00 milestone Oct 28, 2015
@naomiblack
Copy link
Contributor

@vsavkin could you look into this and pair with Brian on the best way to move forward?

@btford
Copy link
Contributor Author

btford commented Nov 3, 2015

@vsavkin @yjbanov @IgorMinar and I just chatted about how to implement this. It'll take some changes to how DynamicComponentLoader works to implement for routing.

@yjbanov – can you please explain the approach?

@evanplaice
Copy link

It would work currently because the <router-outlet> element shows up in the source of the route output.

Is this a bug?

@yjbanov
Copy link
Contributor

yjbanov commented Nov 4, 2015

Goal

ParentCmp would like to pass some data to ChildCmp. We also would like <router-outlet> to be unaware of this contract between parent and child (single responsibility principle). Finally, we would like the child component to be unaware of the type of the parent component (perhaps the child component is reusable and therefore can have different types of parents).

Problem

<router-outlet> sits between the ParentCmp and ChildCmp, and because the router instantiates ChildCmp dynamically there's no element in the template to bind parent data to the child in a type-safe fashion.

One way we could deal with this problem is have parent publish the data via DI for child to inject instead of data binding. There are several problems with this:

  1. usually the router would instantiate one of several types of child components. Otherwise, why use router at all? If a child requires one piece of data, while another one requires a different piece of data, proactively publishing all possible data would be wasteful, and to prevent eager loading via indirection requires much more boilerplate.
  2. DI happens only once. If data changes over time, the child component would need to be destroyed and recreated to pass new values.
  3. DI is a bit verbose, with parent having to bind it in addition to setting up the actual data, and child having to inject it via constructor.

Proposal

<router-outlet> directive takes a callback route-will-load as @Input. When DynamicComponentLoader instantiates the child, but before we run change detection on the child, we call the callback passing it a reference to the child component. What the callback does with the child component is not router-outlet's concern. It's entirely between ParentCmp and ChildCmp (or any other child type that can appear).

Example:

@Component({ selector: 'foo-cmp' })
@View({
  template: `<router-outlet [route-will-load]="passParams"></router-outlet>`,
  directives: ROUTER_DIRECTIVES
})
@RouteConfig([
  { path: '/', component: ChildCmp }
])
class ParentCmp {
  constructor(params: RouteParams) {
    this.parentParams = params;
  }

  passParams(child: ChildCmp): void {
    child.parentParams = this.parentParams;
  }
}

The implementation of passParams is very simple for this use-case, but one can imagine more advanced use-cases. Perhaps router-outlet could host multiple types of child components and the parent would need to supply different values to different types.

NOTE: an alternative to a callback is to use an (event) for parent to react to. The limitation of an event is that it flows up. A callback could be called synchronously so the entire sequence of route change + child instantiation + passing values + UI update happens in a single change detection pass.

@jelbourn
Copy link
Member

jelbourn commented Nov 4, 2015

I think this API is a bit off as it stands. It's using a property-binding (not an event binding) to set up a function that gets called at a specific time, which is weird. Also, the param to passParams in the example wouldn't be able to be ChildCmp, since it could be any component, so it would have to be be any, forcing the user to switch on the type and typecast. Overall this doesn't seem like any less work than setting up a @ViewChild query for the component and setting the data on afterViewInit. If this existing feature covers this use-case, I'd suggest sticking to that for our immediate goals and exploring a nicer API.

Exploring a different API:
The idea we want to express is something like: "I am a component containing some router outlets. When component X is loaded into one of those outlets, I need to give it some data." I think that something like this captures that pretty well:

@Component({ selector: 'foo-cmp' })
@View({
  template: `<router-outlet></router-outlet>`,
  directives: ROUTER_DIRECTIVES
})
@RouteConfig([
  { path: '/', component: ChildCmp }
])
class ParentCmp {
  constructor(params: RouteParams) {
    this.parentParams = params;
  }

  // Naming notwithstanding, just the concept. 
  @ChildRouteInit(ChildCmp, 'optional-specific-outlet-identifier')
  passParams(child: ChildCmp): void {
    child.parentParams = this.parentParams;
  }

  // Very similar to this, which we can do today.
  @HostListener('click')
  handleClick() { /***/ }
}

Obviously it would also need a non-decorator syntax as well.

@jelbourn
Copy link
Member

jelbourn commented Nov 4, 2015

cc @yjbanov @vsavkin @IgorMinar for thoughts on a) whether @ViewChild + afterViewInit is enough for now, and b) for the second API

@timkindberg
Copy link

+1 @jelbourn idea

@mikehayesuk
Copy link

👍 for something along these lines to pass data between routed components.

@timkindberg
Copy link

Couple Ideas:

  1. One approach I created for this @State decorator for a1atscript was just passing in all the state data via a state input. It had all state params and resolved datas. Like this:
@Component({
   inputs: ['state']
})
class MyPage {
   state: { params: any, resolves: any}
}
  1. Another new idea is a declarative approach, perhaps where you nest the routed components inside router-outlet and they are used like a configuration. I envision needing access to local convenience vars like $params. Like this:
@Component({ selector: 'child' })
class ChildCmp {
  @Input() name;
}

@Component({ selector: 'foo' })
class FooCmp {
  @Input() bar;
}

@Component({ 
  selector: 'parent',
  template: `
  <router-outlet>
    <child name="$params.name"></child>
    <foo id="$params.id"></foo>
  </router-outlet>`
})
@RouteConfig([
  { path: '/child/:name', component: ChildCmp },
  { path: '/foo/:id', component: FooCmp }
])
class ParentCmp {}

@gionkunz
Copy link
Contributor

The route outlet should obviously not be used to propagate bindings to instantiated components. What about the @RouteConfig and by specifying a hostTemplate ?

#5875

@IvanRave
Copy link

or something like this

<div>
  {{city | json}}
  <div *ngIf="routePath === '/regions'">
    <region-list [cityId]="city.id"></region-list>
  </div>
  <div *ngIf="routePath === '/region/:id'">
     <region-detail [id]=":id" [city]="city"></region-detail>
  </div>
</div>

@eXaminator
Copy link

I just came across this issue as I was looking for a way to pass data to a routed child component.

The given workaround above doesn't seem to work in dev mode as I get an exception when changing the child's property in ngAfterViewInit:
Expression 'prop in ChildComponent@2:19' has changed after it was checked.

I'm not sure if I've done anything wrong, but if not this issue is pretty important and should be addressed soon. It seems this was also discussed in #6094.

I would also like to propose another syntax:

@RouteConfig([
  { path: '/child/:name', component: ChildCmp, bindings: {'[childProp]': 'parentProp'} },
])
class ParentCmp {
  parentProp: string = 'Hello World';
}

This would allow to bind any input the child component might have just like you would if you would use the component right in the template.

@EgorkZe
Copy link

EgorkZe commented Apr 27, 2016

And what is solution for it? How can i get access navbar in childs from <router-outlet [navbar]="vc"></router-outlet> ?

@zoechi
Copy link
Contributor

zoechi commented Apr 27, 2016

@EgorkZe https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

@michal-filip
Copy link

@zoechi I'd hate to hijack the thread but is there any similar approach for angular 1.5 where we can't create service instances for a specific component tree?

@zoechi
Copy link
Contributor

zoechi commented Apr 27, 2016

@michal-filip Sorry, no knowledge about 1.5

@maximedupre
Copy link

@jelbourn

Overall this doesn't seem like any less work than setting up a @ViewChild query for the component and setting the data on afterViewInit.

I tried implementing your solution, but I don't think it's feasible. The ViewChild component can't be found because it is instantiated and/or placed in the DOM later by the router-outlet. @ViewChild(MyComponent) child: MyComponent; is always undefined.

@ericmartinezr
Copy link
Contributor

The old new router has been deprecated, so it shouldn't be a issue anymore. You can't query dynamic loaded routes anymore since beta.16

e.g. for router-outlet: loaded components can't be queries via @ViewQuery, but router-outlet emits an event activate now that emits the activated component

That activate event emites the current instruction -> <router-otulet (activate)="..."></router-outlet>.

If you need to pass data to your routed componentes you must use services.

But again, this old new router has been deprecated, stay tuned for the new new router.

@maximedupre
Copy link

@ericmartinezr Thanks for the response. It has cleared up some things, but made others more confusing.

  1. What do you mean by old new router? Does that mean that I shouldn't be using router-outlet anymore?
  2. There is no non-deprecated router as of now?
  3. Why should I use a service to pass data vis service if I now have access to a routed component via the activate event? Because this is the old new router and that it is deprecated?

@CaptainCodeman
Copy link

The "router formerly known as new" is no more. It is deprecated. It is an ex-router. It has ceased to be.

(the name switch-a-roo is happening right now)

The new "alt" router which will henceforth just likely be called "router" is similar in some ways, different in others. Until it's done and available to use it's hard to say what it will be exactly but it looks like there will still be router-outlets and the need to pass things via services.

@ericmartinezr
Copy link
Contributor

@maximedupre answering your questions

  1. The router is currently being rewritten (see for example Alt. Router Initial Work #8173)
  2. @CaptainCodeman answered the two first questions, I saw it while I was writing!
  3. The main issue in this issue (?) is to pass data from a parent component to a child component loaded by routing and the only way is by using services. I mentioned the activate event because of your comment to @jelbourn 's solution which doesn't work anymore. Those are two different things, sorry if I mixed them up.

@maximedupre
Copy link

What about using the child component that was received in the activate event to pass data? I just tried it and it works, but is it a bad practice?

@ericmartinezr
Copy link
Contributor

@maximedupre to be honest this issue shouldn't be used a conversation forum, if you have any doubts about how to use the router you could go to gitter's chatroom and ask over there (https://gitter.im/angular/angular). This issue is closed and it will be buried with time.

@sharpmachine
Copy link

Need something like @ViewChild except it can be used in a child rather than a parent...like @ViewParent. Really need to pass data from the parent to the child using <router-outlet></router-outlet> in the parent template.

@zoechi
Copy link
Contributor

zoechi commented May 27, 2016

@sharpmachine use a shared service

@sharpmachine
Copy link

@zoechi How? If I'm not using a selector for a component because I want to use router-outlet, then how can I use a service to shared data?

@zoechi
Copy link
Contributor

zoechi commented May 27, 2016

I don't see how using a selector is related. See https://angular.io/docs/ts/latest/cookbook/component-communication.html. If the component is added to a router-outlet and has a constructor parameter then DI looks up the parent components for a provider. If it finds one it requests its instance and passes it to the constructor.

@sharpmachine
Copy link

sharpmachine commented May 27, 2016

@zoechi I'm lost I'm doing that and it's not working for me.

Parent:

import { Component, OnInit } from '@angular/core';
import { RouteSegment, Routes, ROUTER_DIRECTIVES } from '@angular/router';

import {
  NsgWorkspaceComponent,
  NsgSubjectCardComponent
} from '../../shared';
import { Subject, SubjectService } from '../shared';
import { SubjectDetailComponent } from '../subject-detail';

@Component({
  moduleId: module.id,
  template: `
    <nsg-workspace>
      <nsg-subject-card [subject]="subject"></nsg-subject-card>
      <router-outlet></router-outlet>
   </nsg-workspace>`,
  directives: [
    ROUTER_DIRECTIVES,
    NsgWorkspaceComponent,
    NsgSubjectCardComponent
  ],
  providers: [SubjectService]
})
@Routes([
  { path: '/profile', component: SubjectDetailComponent }}
])
export class SubjectComponent implements OnInit {
  subject: Subject;

  constructor(
    private _subject: SubjectService,
    private _segment: RouteSegment
  ) { }

  ngOnInit() {
    this.getSubject();
  }

  getSubject() {
    let id = this._segment.getParam('id');

    this._subject.get(+id)
      .subscribe(subject => this.subject = subject);
  }

}

Child:

import { Component, OnInit } from '@angular/core';
import { RouteSegment } from '@angular/router';

import { Subject, SubjectService } from '../shared';

import {
  NsgSheetComponent,
  NsgToolbarComponent,
} from '../../shared';

@Component({
  moduleId: module.id,
  template: `
    <nsg-toolbar [title]="subject?.name"></nsg-toolbar>
    <nsg-sheet>
      <div class="well well-sm">
        <pre>{{subject | json}}</pre>
      </div>
    </nsg-sheet>`
  directives: [
    NsgSheetComponent,
    NsgToolbarComponent,
  ],
  providers: [SubjectService]
})
export class SubjectDetailComponent {
  constructor(
    private _subject: SubjectService
  ) { }
}

This doesn't work. I can't get subject in the SubjectDetailComponent. I could try to call the same service method (.get(id)), but the child doesn't have access to the parents route param...

@zoechi
Copy link
Contributor

zoechi commented May 27, 2016

@sharpmachine

don't add providers: [SubjectService] on SubjectDetailComponent or it wull get a new instance.

This is not the right place for such discussions. Please post further questions at StackOverflow, Gitter, Google groups,...

@sharpmachine
Copy link

@zoechi I will do a stackoverflow but if it's not possible to do this via router-outlet, then it's an issue that needs to be discussed here.

@gmsergiov
Copy link

I'm facing the exact same issue. I've a resolver which resolves the first parameter info related and in the second resolver the param is not present.
is there any update i'm not aware of?
Is there any issue with jusing locatStorage to store the data?, I don't see any approach following this idea.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
effort2: days feature Issue that requests a new feature
Projects
None yet
Development

No branches or pull requests