vaadin / flow

Vaadin Flow is a Java framework binding Vaadin web components to Java. This is part of Vaadin 10+.
Apache License 2.0
618 stars 167 forks source link

Support @Id constructor injection #3603

Open oluwasayo opened 6 years ago

oluwasayo commented 6 years ago

When my PolymerTemplate gets initialized, I want its dependencies wired by constructor (and setter) injection so that:

Given a template like:

<my-form>
  <vaadin-textfield id="firstname></vaadin-textfield>
  <vaadin-textfield id="lastname></vaadin-textfield>
</my-form>

I want to be able to make my server-side template like:

@Tag("my-form")
public class MyForm {

  private TextField firstname;
  private TextField lastname;

  public MyForm(@Id("firstname") TextField firstname, 
                @Id("lastname") TextField lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }
}

In production code, I expect Flow to wire-in the dependencies while in tests, I want to be able to write code like:

TextField firstname = mockTextField(); // Record a behavior into it.
TextField lastname = mockTextField(); // Record a behavior into it.
MyForm systemUnderTest = MyForm(firstname, lastname);

Context.

Legioth commented 6 years ago

What would the non-test code for instantiating a MyForm instance look like?

The injection could be handled transparently in the case when MyForm is in itself also created and injected into another PolymerTemplate, but what about standalone usage?

oluwasayo commented 6 years ago

I'm not sure I understood what you meant by standalone usage. MyForm could be a route for example, which is not directly instantiated by the application in production or integration test code but directly instantiating it in unit test code is still desirable.

@Route("signup")
@PageTitle("Join my awesome social network")
@Tag("my-form")
public class MyForm extends PolymerTemplate<TemplateModel> {
  // Gather data
}
Legioth commented 6 years ago

It could be a @Route, it could be @Id injected into some other PolymerTemplate, but that's not always the case. What if the component is used in a context where usage with the current field injection approach would be written out as MyForm form = new MyForm(); in application code?

It would be slightly weird if that case would require doing things in a completely different way depending on something that is essentially implementation details for the component?

oluwasayo commented 6 years ago

I think that should be a design choice on the side of the application developer. A component can be self contained (e.g "I will paint a portrait") or could accept dependencies (e.g "I will paint a portrait on your canvas"). So I wouldn't say the dependencies of a component are always an implementation detail.

Another place where the implementation detail argument falls short is that even if a component does not accept dependencies via a constructor, nothing stops an application code from reflecting over it and setting the fields to something else anyway.

Looking at the following example:

@Tag("my-form")
public class MyForm {

  @Id("firstname")
  private TextField firstname;

  @Id("lastname")
  private TextField lastname;

  public void submit() {
     // get data from fields and call some service 
     // then navigate to a result page
  }
}

This is a typical component with no constructor at all. If this is instantiated by Flow, the fields would have been initialized before the the instance gets to application code. However if instantiated like MyForm form = new Form(); then the instance would be in a "invalid" state because the fields would be null. So a developer self-instantiating is basically taking responsibility. Perhaps I'm missing something?