8000 Allow injecting ComponentRef of a component through DI · Issue #47398 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

Allow injecting ComponentRef of a component through DI #47398

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

Open
Harpush opened this issue Sep 10, 2022 · 28 comments
Open

Allow injecting ComponentRef of a component through DI #47398

Harpush opened this issue Sep 10, 2022 · 28 comments
Assignees
Labels
area: core Issues related to the framework runtime feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature needs: clarification This issue needs additional clarification from the reporter before the team can investigate.
Milestone

Comments

@Harpush
Copy link
Harpush commented Sep 10, 2022

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

As of angular 14 we have the new super useful setInput on ComponentRef for dynamic components. It finally allows fully CD aware dynamic inputs changing.
Currently if I want to extend a component with a directive and set inputs from it I need to do one of the two options:

  1. Change the inputs statically in the constructor
  2. Change inputs to get/set as ngOnChanges won't fire

Proposed solution

Allowing to inject ComponentRef of the host component instead of the actual instance. This way we could just use setInput to update inputs and have a full CD aware binding support not through the template.

Alternatives considered

  1. Change the inputs statically in the constructor only - Can't apply to directive input based changes or observable based changes
  2. Change inputs to get/set - Possible but annoying and "leaks" the knowledge of this component is being used from a directive outside. How can one explain why ngOnChanges was not used...
  3. Maybe not through ComponentRef but as any other way to use setInput from DI injected components
@AndrewKushnir AndrewKushnir added feature Issue that requests a new feature area: core Issues related to the framework runtime labels Sep 12, 2022
@ngbot ngbot bot modified the milestone: Backlog Sep 12, 2022
@AndrewKushnir
Copy link
Contributor

(linking #8277 here as an additional use-case)

@angular-robot angular-robot bot added the feature: votes required Feature request which is currently still in the voting phase label Sep 13, 2022
@angular-robot
Copy link
Contributor
angular-robot bot commented Sep 13, 2022

This feature request is now candidate for our backlog! In the next phase, the community has 60 days to upvote. If the request receives more than 20 upvotes, we'll move it to our consideration list.

You can find more details about the feature request process in our documentation.

@angular-robot
Copy link
Contributor
angular-robot bot commented Oct 23, 2022

Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

Find more details about Angular's feature request process in our documentation.

@angular-robot angular-robot bot removed the feature: votes required Feature request which is currently still in the voting phase label Nov 12, 2022
@angular-robot
Copy link
Contributor
angular-robot bot commented Nov 12, 2022

Thank you for submitting your feature request! Looks like during the polling process it didn't collect a sufficient number of votes to move to the next stage.

We want to keep Angular rich and ergonomic and at the same time be mindful about its scope and learning journey. If you think your request could live outside Angular's scope, we'd encourage you to collaborate with the community on publishing it as an open source package.

You can find more details about the feature request process in our documentation.

@angular-robot angular-robot bot added the feature: insufficient votes Label to add when the not a sufficient number of votes or comments from unique authors label Nov 12, 2022
@AndrewKushnir AndrewKushnir added feature: under consideration Feature request for which voting has completed and the request is now under consideration and removed feature: insufficient votes Label to add when the not a sufficient number of votes or comments from unique authors labels Nov 14, 2022
@pkozlowski-opensource
Copy link
Member

I think that we should start with the use-case first, as the notation of a host directive setting an input of its host component is ... surprising.

@Harpush could you please share real-life use case where you need to set component inputs from a directive?

@pkozlowski-opensource pkozlowski-opensource added the needs: clarification This issue needs additional clarification from the reporter before the team can investigate. label Nov 30, 2022
@Harpush
Copy link
Author
Harpush commented Nov 30, 2022

@pkozlowski-opensource sure. I had not a while ago a third party component with several inputs. We set all of them to the same values in most of the app. Now the idea I had is adding a directive that sets those inputs. The component is using ngOnChanges and not input get set so I got stuck. If I had a way to inject the component ref and call set input - the ngOnChanges would have been invoked.

@mikezks
Copy link
mikezks commented Dec 10, 2022

I agree, @Harpush.

@pkozlowski-opensource, this would make a lot of third-party integration szenarios easier. It would not make the learning-process for Angular beginnners harder, but would support advanced use-cases if needed.

As mentioned, IMHO it would fit well into the setInput API.

@DaSchTour
Copy link

One real-life example, although not very nice is, that in one project I have a lot of components that have and id as input. Now I wanted to write a directive that can set this id. But currently I would have to inject all the components into this directive to be able to set the input. With the ComponentRef I would be able to set the input of a component even if I don't know the instance of the component.

In general this allows inversion of dependency. It would allow directives to interact with a component without knowing the interface. This has certain downsides, as for example type safety is not ensured, but on the other hand that is always the case if you have such loose coupling.

@Harpush
Copy link
Author
Harpush commented Feb 1, 2023

@pkozlowski-opensource any news concerning this?

@HyperLife1119
Copy link
Contributor
HyperLife1119 commented Aug 23, 2023

We should also need a DirectiveRef, this can also be used to interact with HostDirectives.

@csisy-bt4w
Copy link

Since components cannot be composited very well (e.g. an element can have a single component only), there is currently no way to create a specialized component from a base. For example:

There is a button component with an attribute selector ([my-button]) so it can be added to a button or a element. It contains some styling and functionality.

Now, we'd like to have a specialized button component which extends its style and/or sets sensible input properties. Since these components cannot be composited, we have to:

  • Either wrap the button component
  • Or use a directive instead of a component

The problem with the first approach is that in this case, we have to forward everything that a button can do. And that's not even enough. What if there is a directive (e.g. [routerLink]) that we want to place on the wrapped button? We are out of luck.

The problem with the second approach is that directives cannot contain styles (see #17766). Also, with the new signal-based inputs, we cannot even update the properties from the typescript.

@Component({ selector: 'button[my-button],a[my-button]', ... })
class MyButtonComponent {
  readonly myInput = input<'foo' | 'bar'>('foo');
}

// must be a directive because component cannot be composited
@Directive({ selector: 'button[my-button][my-item]' })
class MyItemDirective {
  constructor() {
    const button = inject(MyButtonComponent, { self: true });
    // button.myInput .... ? we cannot update this here
    // with the ComponentRef we could at least call setInput
    // which is far from ideal because we have no code completion for the property names
  }
}

// we could inherit from the MyButtonComponent instead, but that does not inherit its template and styles...

@reesemclean
Copy link

Since components cannot be composited very well (e.g. an element can have a single component only), there is currently no way to create a specialized component from a base. For example:

There is a button component with an attribute selector ([my-button]) so it can be added to a button or a element. It contains some styling and functionality.

Now, we'd like to have a specialized button component which extends its style and/or sets sensible input properties. Since these components cannot be composited, we have to:

  • Either wrap the button component
  • Or use a directive instead of a component

The problem with the first approach is that in this case, we have to forward everything that a button can do. And that's not even enough. What if there is a directive (e.g. [routerLink]) that we want to place on the wrapped button? We are out of luck.

The problem with the second approach is that directives cannot contain styles (see #17766). Also, with the new signal-based inputs, we cannot even update the properties from the typescript.

@Component({ selector: 'button[my-button],a[my-button]', ... })
class MyButtonComponent {
  readonly myInput = input<'foo' | 'bar'>('foo');
}

// must be a directive because component cannot be composited
@Directive({ selector: 'button[my-button][my-item]' })
class MyItemDirective {
  constructor() {
    const button = inject(MyButtonComponent, { self: true });
    // button.myInput .... ? we cannot update this here
    // with the ComponentRef we could at least call setInput
    // which is far from ideal because we have no code completion for the property names
  }
}

// we could inherit from the MyButtonComponent instead, but that does not inherit its template and styles...

I have this exact use case while trying to refactor a component to signal-based inputs.

@Harpush
Copy link
Author
Harpush commented Feb 29, 2024

Reading through https://netbasal.com/angulars-model-function-explored-a-comprehensive-overview-4481d023c822 I saw the same problem described here trying to be solved with the new model.
A directive that wants to set default values to the component it is declared on.
I believe creating models just for this is not a good idea and before signal inputs we could update the input property on the instance which we now can't (didn't work well before too anyway) - which makes it even more important now.
Using what was suggested here - one can inject the component ref in the directive and call set input instead which seems better.

Why is this issue still with needs clarification?

@Jethril
Copy link
Jethril commented Jul 18, 2024

Since components cannot be composited very well (e.g. an element can have a single component only), there is currently no way to create a specialized component from a base.

I would add that inheritance VS composition is a subject of discussion and with Angular I usually go with composition.
The only alternative I see would be to create a kind of "wrapper components" that allows extending a single component's functionalities and that delegates its inputs and outputs to its inner component.

But that seems a bit overkill: directives look pretty well suited for adding a behavior or setting inputs to components since you can cumulate several ones like traits in PHP or Rust, without having to build 4 - 5 wrapper components and without having to face a diamond inheritance issue.

@MurhafSousli
Copy link

I have the same use case, I have an addon directive that meant to be applied on a specific component and override its input values. currently I cannot replace @Input() with InputSignal

@montella1507
Copy link

This is really needed when you have component with input() signal. You cannot change input value and you don't have componentRef<> available to use setInput().

So it is forcing you to change to model<>() which is basically antipattern.

@montella1507
Copy link
montella1507 commented Sep 16, 2024

Typical usecase is, when you want to write reusable directives to set inputs of components, several component libraries use that.

Example usecases:

  • "datasources" for dropdowns, grids, etc.. (directive has to set "data", "valueField", "textField" etc.)
  • clientside filtering for dropdowns, grids etc...
  _dropdownList = inject(DropdownListComponent, 
  {
    self: true,
  });

 
..
if (this._dropdownList) {
          if (value.status === PackStatus.Loading) {
            this._dropdownList.loading.set(true);
            this._cdr.markForCheck();
          }
          if (value.status === PackStatus.Success) {
            this._dropdownList.data.set(value.data);
            this._dropdownList.loading.set(false);
            this._cdr.markForCheck();
          }
          if (value.status === PackStatus.Error) {
            this._dropdownList.data.set([]);
            console.warn('Could not load data for DropdownList');
            console.warn(value.error);
            this._cdr.markForCheck();
          }
        }

This is not possible without ComponentRef.setInputs() or model() (which is antipattern to change all inputs to models() because of external usage)

Before input() signals. Everyone was able to set inputs via these steps:

  • set inputs
  • call ngOnChanges manually (construct simplechanges)

Now, we have no way to do that.

@Marshland
Copy link
Marshland commented Oct 13, 2024

The only way that i found at this moment is by use a computed that create a new signal with the signal input value internal to the directive, and the expose a method from directive to modify that derived signal. So every time the input of directive change, reset the derived signal value. But this is a simple workaround, and can't be a good pattern, especially if we need to do this mechanism for a lot of inputs of directive.

Example:

@Directive({ selector: '[myDirective]' })
class MyDirective {
  someInput = input('');
  #internalSomeInput = computed(() => signal(this.someInput()));

  changeSomeInput(value: string) {
    this.internalSomeInput().set(value);
  }

  #someMethod(): void {
    ...
    const someInputValueToUse = this.internalSomeInput()();
  }
}

@Component({
  selector: 'my-component',
  hostDirective: MyDirective
})
class MyComponent {
  myDirective = inject(MyDirective, { self: true });
  ....
  someMethod(value: string): void {
    this.myDirective.changeSomeInput(value);
  }
}

@ValentinFunk
Copy link
ValentinFunk commented Oct 19, 2024

Just wanted to reiterate what @montella1507 is saying - this is something that was possible to do without signal inputs before (set the input and call change detection) and is no longer supported with signal inputs. It's a pattern we're using as well and it's stopping us from fully going to signals.

Tagging @JeanMeche since you closed #54987 where the conclusion was to use ComponentRef. setInput(). This issue shows why that doesn't work (outside of the test usecase).

@JeanMeche
Copy link
Member

In the context of unit tests you should indeed use ComponentRef.setInput.
If you're in the context of a component, you should go toward using a linkedSignal with the input as source. This way you'll define an writable inner state.

@imaliouris
Copy link

@JeanMeche that seems more like a workaround than a proper solution to this problem.
If we go with the approach that you propose all the components, that we want to alter from a directive, should have a linkedSignal.
That seems a little dirty because a component already exposes an input in the outer word and now we will provide another way to alter the same thing.

Also if we create a directive for a component or directive that comes from a package, it won't be possible to alter that input if they dont already have a linkedSignal

628C
@montella1507
Copy link
montella1507 commented Dec 2, 2024

@JeanMeche sorry to say, but this is the same terrible design decision, like the allowSignalWrites was. Forcing the developers to "guess the future" - to implement something somehow, that does not make sense in a scope of a particular piece of software, because there is mistake in a framework.

Let me explain in examle with allowSignalWrites.

  1. We had a component implementing Control Value Accessor - implementing WriteValue

  2. Signals were used internally inside that CVA component = signal was set inside WriteValue - 100% legit scenario.. writeValue is NOT effect and writing to writable signal is.. well, main reason for writable signals to exist.

  3. ANOTHER developer, was calling setValue/patchValue inside effect(()=> {}) = 100% legit scenario, he is not writing to signals.. he is seting value to the FormControl and that case is... well, main reason for effect() to exist.

And... because of design decision, it does not work, because... developer is writing inside signals, but HE DID NOT WANT TO, and he did not know at all.. he was just setting value with patchValue()...

The reality, that there is Signal() inside, is hidden.. should be hidden, should be implementation detail.. But it is not.

The point is, CVA-component creator, was doing 100% legit things, just using signals... setting them not inside of a effect...

With allowSignalWrites and this one.. we were (are) heading to ERA where every developer is forced to now ENTIRE callstack and implementation details... even to the future.


The reason it was bad design was the usage of "ambient context pattern" - that somehing relies on something else from "outter space".

And now.. You suggest to do the same...

@thaoula
Copy link
thaoula commented Dec 19, 2024

Hi Team,

We have several directives that simplify the the usage of complex components by setting inputs and their host component. We are unable to migrate @input to InputSignal because it is not possible to set them from directives.

In some cases, we can go to the host component an change the input to a model ... we have too many properties to even consider using linkedSignal pairings.

In addition, even if we did not migrate any of the components to InputSignals, you have library Authors such PrimeNG who are talking about modernised components using signals coming up in future versions. This means either they will be stuck building the new versions or we will break because the inputs will become InputSignals and we cannot set them.

Is there anyway to fast track a solution to this, injecting ComponentRef, providing a footgun method on InputSignal, allowing casting to writable signal etc?

Kind Regards,
Tarek

@mikezks
Copy link
mikezks commented Dec 19, 2024

@thaoula, could you please prepare a simple Stackblitz PoC that demonstrates your current design? I am curious how it looks like and what you struggle with. Let's see whether there is a proper way to refactor this to the modern Angular APIs.

@thaoula
Copy link
thaoula commented Dec 19, 2024

Hi @mikezks ,

Many, thanks for your reply. I have create the simplest example to demonstrate what we are doing.

In this simple example, I have a basic host component (ComplicatedLookup) and two directives (JobLookup, ContactLookup) that set properties using different data.

https://stackblitz.com/edit/stackblitz-starters-jxjmiswb?file=src%2Fmain.ts

In real life the host component is something similar to NGSELECT with many inputs.

We have reasons to utilise this strategy in our business application and we find it extremely powerful. I do not want to get into a discussion about the merits of the strategy. It works currently and is very effective.

The core issue is that we have this object, InputSignal that allows setting from outside (magically via the template binding) but not setting inside the component. I guess, this ensures unidirectional flow.

In our case, we have a directive that is technically outside the component and would like to set the inputs on its host and it cannot. We cannot get a ComponentRef and we cannot cast it to something because API to do so is not public.

We are just asking for a way either via ComponentRef (not strongly typeed) or some other mechanism to emulate the template binding technique.

@montella1507
Copy link

@thaoula, could you please prepare a simple Stackblitz PoC that demonstrates your current design? I am curious how it looks like and what you struggle with. Let's see whether there is a proper way to refactor this to the modern Angular APIs.

you mean more react-like antipattern design based APIs :-)

As @thaoula . This pattern is usually used as "attached behavior". For the instance.. The most used libraries like Kendo are using this pattern.

We are using this too for "Datasources". Usually directives to set datasource for "dumb components", like dropdowns, comboboxes, or even the grids.

They usually nned to set inputs like items, state, to react to events etc.

@Althain
Copy link
Althain commented Apr 10, 2025

Is this subject somehow alive?
I have the same case right now, that I would need to set input from another component, but not through HTML.
Use case: Date-picker with an input component. Inside input, when it's value changes - I would like to update value(Which is an InputSignal) of date-picker component, but the date-picker is not used in HTML of the input component, but they are linked together by directive.

Also I don't want to emit value from input to pass it through HTML, that's not the point here.

<date-picker-input datepickerInput [datepicker]="datePicker"/>
<date-picker #datePicker/>

To address this I would either need to access SetInput from ComponentRef or be able to update the value of InputSignal somehow.

@Harpush
Copy link
Author
Harpush commented May 17, 2025

Maybe a function called in ctor similar to the new bindings: bindInput?

@Directive()
class Dir {
  private comp = inject(Comp, {host: true});

  test = input.required<string>();

  constructor() {
    bindInput(comp.test, this.test);
  }
}

Pretty sure it will be helpful for the new signal forms too :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature needs: clarification This issue needs additional clarification from the reporter before the team can investigate.
Projects
None yet
Development

No branches or pull requests

0