There have been a few feature requests, issues and even a pull request asking for the ability to inject dependencies into properties and they were all closed back in 2016. Several reasons where quoted as to why this amazing feature was not allowed in the core framework, one of which being that we can use a plugin. I argue that this functionality is needed by the core Aurelia framework and I would like to provide an example of why and bring this topic back up for discussion.
@EisenbergEffect, you stated in issue #125 of the dependency-injection project that:
"If you have classes, with many dependencies, then that can be a sign that you need to refactor in some way. "
This is a great suggestion and for most well-written (web-based) projects a class should never need more than a handful of dependencies. In these situations the current @inject
pattern that Aurelia allows works just fine. However this principle–in practice, when scaled to large enterprise web development projects; can still result in a large number of composite dependencies. When you are building a configurable application, that supports multiple market segments, with complex business logic, you can have a well written class following composite design patterns and still need 40+ optional dependencies.
The application I work on contains over a thousand distinct pages. Most of these pages we are able to build from configuration files with generic components. We have a form
component that dynamically creates, interacts with and composes controls. Our form
class has dependencies on 40+ composite components. Doing this with Aurelia’s current dependency injection pattern looks very messy and it is difficult to maintain. To me, this feels like an issue with how well Aurelia’s built in IoC patterns scale.
My situation is further complicated by the fact that we must inject factories for all of our components into the constructor and save them to properties for later use. This is necessary because each component is optional and takes parameters into their constructors to control initialization. The dependencies we inject are composite controls (following the composite pattern) that return many different control configurations based on how they are constructed. Here is an example of just how terrible Aurelia’s built-in @inject
pattern looks when applied to an example that is 1/5th the size I actually have to implement.
//This is really terrible
import ComboBox from './ComboBox/ComboBox';
import Checkbox from './Checkbox/Checkbox';
import NumericBox from './NumericBox/NumericBox';
import DatePicker from './DatePicker/DatePicker';
import TimeMask from './TimeMask/TimeMask';
import HyperLink from './HyperLink/HyperLink';
import Image from './Image/Image';
import TextBox from './TextBox/TextBox';
import { inject, Factory } from 'aurelia-framework';
// Type for Factory Creates a function with the same return type and parameters as a constructor
type Factory<T extends new (...a: any[]) => any> =
T extends new (...a: infer A) => infer R ? (...a: A) => R : never;
@inject(
Factory.of(ComboBox),
Factory.of(Checkbox),
Factory.of(NumericBox),
Factory.of(DatePicker),
Factory.of(TimeMask),
Factory.of(HyperLink),
Factory.of(Image),
Factory.of(TextBox)
)
class Form {
public GetComboBox: Factory<typeof ComboBox>;
public GetCheckbox: Factory<typeof Checkbox>;
public GetNumericBox: Factory<typeof NumericBox>;
public GetDatePicker: Factory<typeof DatePicker>;
public GetTimeMask: Factory<typeof TimeMask>;
public GetHyperLink: Factory<typeof HyperLink>;
public GetImage: Factory<typeof Image>;
public GetTextBox: Factory<typeof TextBox>;
constructor(
GetComboBox: Factory<typeof ComboBox>,
GetCheckbox: Factory<typeof Checkbox>,
GetNumericBox: Factory<typeof NumericBox>,
GetDatePicker: Factory<typeof DatePicker>,
GetTimeMask: Factory<typeof TimeMask>,
GetHyperLink: Factory<typeof HyperLink>,
GetImage: Factory<typeof Image>,
GetTextBox: Factory<typeof TextBox>
) {
this.GetComboBox = GetComboBox;
this.GetCheckbox = GetCheckbox;
this.GetNumericBox = GetNumericBox;
this.GetDatePicker = GetDatePicker;
this.GetTimeMask = GetTimeMask;
this.GetHyperLink = GetHyperLink;
this.GetImage = GetImage;
this.GetTextBox = GetTextBox;
}
}
In contrast, the property injection plugin allows us to implement the same class with much less complexity:
//This is quite elegant
import ComboBox from './ComboBox/ComboBox';
import Checkbox from './Checkbox/Checkbox';
import NumericBox from './NumericBox/NumericBox';
import DatePicker from './DatePicker/DatePicker';
import TimeMask from './TimeMask/TimeMask';
import HyperLink from './HyperLink/HyperLink';
import Image from './Image/Image';
import TextBox from './TextBox/TextBox';
import { inject, factory } from 'aurelia-property-injection';
// Type for Factory Creates a function with the same return type and parameters as a constructor
type Factory<T extends new (...a: any[]) => any> =
T extends new (...a: infer A) => infer R ? (...a: A) => R : never;
class Form {
static injectProperties = {};
@factory(ComboBox) GetComboBox: Factory<typeof ComboBox>;
@factory(Checkbox) GetCheckbox: Factory<typeof Checkbox>;
@factory(NumericBox) GetNumericBox: Factory<typeof NumericBox>;
@factory(DatePicker) GetDatePicker: Factory<typeof DatePicker>;
@factory(TimeMask) GetTimeMask: Factory<typeof TimeMask>;
@factory(HyperLink) GetHyperLink: Factory<typeof HyperLink>;
@factory(Image) GetImage: Factory<typeof Image>;
@factory(TextBox) GetTextBox: Factory<typeof TextBox>;
constructor() {
//setting the `injectConstructor` flag to true gives you access to injected properties here!
}
}
This plugin is great and I probably would not be making this post if this plugin was still being supported. Unfortunately, the property injection plugin has not been touched in two years with issues that are over a year old.
I really believe that this should be part of the core framework so that we don’t need to use an external package that is not being supported. When this feature was proposed as a pull request back in April of 2016, I don’t feel that the Aurelia team provided a good reason as to why the pull request was rejected. I want to bring this back up for discussion. I do not belive I am alone in my desire for this feature. Since it was created, the aurelia-property-injection plugin has been downloaded from npm 5,400 times