angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
95.91k stars 25.34k forks source link

Allow injecting ComponentRef of a component through DI #47398

Open Harpush opened 2 years ago

Harpush commented 2 years ago

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 commented 2 years ago

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

angular-robot[bot] commented 2 years ago

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[bot] commented 1 year ago

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[bot] commented 1 year ago

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.

pkozlowski-opensource commented 1 year ago

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?

Harpush commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@pkozlowski-opensource any news concerning this?

HyperLife1119 commented 1 year ago

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

csisy-bt4w commented 7 months ago

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:

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 commented 7 months ago

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 commented 7 months ago

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 commented 2 months ago

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 commented 3 weeks ago

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 commented 2 weeks ago

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 commented 2 weeks ago

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

Example usecases:

  _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:

Now, we have no way to do that.