-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(core): improve component bindings #1924
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
Conversation
589a5ab to
4700f7a
Compare
4a3b252 to
c95f689
Compare
+1, this part makes sense to me. Can we have a dedicated pull request for this feature please? We should discuss documentation updates, descriptive API naming, etc. as part of the review.
I don't understand the benefits of this change. I believe the example shown above can be easily rewritten without class MyComponent implements Component {
controllers: [MyController],
bindings = [binding];
classes: {'my-class': MyClass},
providers: {'my-provider': MyProvider},
constructor() {
// Dynamically add more bindings
this.bindings.push(...);
}
}What benefits do you @raymondfeng see in the I don't want to introduce two ways how to accomplish the same thing, because that usually confuse users. Let's pick one way that we think is the best, make it easy to use and well documented. BTW I think the current approach for building components is too cumbersome and we should look into ways how to leverage As long as both Let me open a new issue to discuss that idea: #1968 |
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's reduce the scope of this pull request please and start with the following feature: Improve the component design to allow contribution of various bindings.
|
As I was thinking about this some more and in the light of #1968, I think it would be best if the code example shown in the PR description was instead written this way: class MyComponent extends Component {
constructor() {
this.controller(MyController);
this.bind('foo').to('bar');
this.bind('my-class').toClass(MyClass);
this.bind('my-provider').toProvider(MyProvider);
}
}I see few important benefits in such approach:
|
|
@bajtos I think the different options boil down to IIRC, I proposed before that each component becomes a context to group its contributions. If that happens, the application will becomes:
We also need to find a way to aggregate all contexts, either by merging or delegating. |
|
BTW, we can do something similar today: export class MyComponent {
constructor(@inject(CoreBinding.APPLICATION_INSTANCE) app: Application) {
app.controller(MyController);
app.bind('foo').to('bar');
app.bind('my-class').toClass(MyClass);
app.bind('my-provider').toProvider(MyProvider);
}
} |
I don't think it's a good idea for Component to be also a Context:
Here is what I am proposing: (1) Let's keep the contract between Application and Component simple. The component exports an array of (2) Give us and the community freedom to experiment with different ways of building/setting-up components. Let the solution described in my comment above (and little bit later here as well) be only one of the possible ways. (3) As for my proposal: my vision is to conceptually extract Context + Application APIs into two groups:
So far, As for components, let's implement a base component class that provides interface Component {
// ...
bindings?: Binding[];
}
interface BindingBuilder {
bind(key: string): Binding;
}
function AppArtifactRegistrationMixin<T extends Class<any>>(superClass: T) {
return class extends superClass {
controller(controllerCtor: ControllerClass, name?: string): Binding {
name = name || controllerCtor.name;
return this.bind(`controllers.${name}`)
.toClass(controllerCtor)
.tag('controller');
}
// etc.
};
}
class ComponentBindingBuilder implements BindingBuilder {
bindings: Binding[] = [];
bind(key: string): Binding {
const binding = new Binding(key);
this.bindings.push(binding);
return binding;
}
}
class ComponentBase implements Component
extends AppArtifactRegistrationMixin(ComponentBindingBuilder) {
}
// and also in the Application class:
class Application extends AppArtifactRegistrationMixin(Context) {
// ...
} |
Honestly, this seems a bit hacky to me, I consider this as a (hopefully short-term) workaround until we enhance Component interface to support all kinds of artifacts. On the other hand, maybe this is a sign that we need to revisit Component design? In the current design, whenever we add a new artifact to Application/RestApplication/etc. (e.g. static assets), we also need to find a way how to enable components to contribute these artifacts too. In the example you have shown, the components are manipulating the application directly, thus there is no need to enhance the Component API contract to support new features. I guess it boils down to declarative vs imperative as you wrote yourself. If we feel that declarative support is something we should support, then I think we should improve our design to make this more obvious/easier to use. For example, the component constructor can receive the Application instance directly in the constructor without any DI. This raises a question though: do we want to support both declarative and imperative approaches to defining components? Personally, I prefer to choose only one approach, to keep the maintenance costs low. If we support both approaches, then we have more documentation to write and maintain, more questions to answer because people will be confused about which approach to choose, etc. @strongloop/loopback-maintainers thoughts? (See also my previous comment proposing a solution how to allow extensions to contribute arbitrary bindings.) |
|
@raymondfeng To move this pull request forward: I think we both agree that it's a good idea to allow components to export |
c95f689 to
c9d33ac
Compare
|
@bajtos I reduced the scope of this PR to add |
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice 👏
In addition to my comments below, please add some documentation for this new feature. For example, you can add a new section to https://github.com/strongloop/loopback-next/blob/master/docs/site/Creating-components.md. Please check out if there are any other places where to mention this new feature and/or cross-link to the new content you will write.
packages/context/src/context.ts
Outdated
| * same key, an error will be thrown. | ||
| * @param binding The configured binding to be added | ||
| */ | ||
| add<ValueType = BoundValue>(binding: Binding<ValueType>): this { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nitpick: I think = BoundValue is not needed, because the specialization of add can be always inferred by the compiler from the specialization of the binding argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raymondfeng PTAL - is the default value for ValueType needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simplified it as add(binding: Binding<unknown>): this {. The same is also applied to a few other methods.
| */ | ||
| providers?: ProviderMap; | ||
|
|
||
| classes?: ClassMap; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this name confusing. What does it mean for a component to export a class? Some classes are exported via JavaScript/TypeScript exports, e.g. export class FooBar. Why should be some classes exported via JS/TS and some others exported via Component properties?
Personally, I prefer to remove this API entirely, keep providers for backwards compatibility (possibly even deprecate it) and ask users to leverage bindings API for all bindings.
If you feel it's important to keep this shortcut, then let's find a better name please. For example:classBindings or classesToBind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added jsdocs to make it clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am afraid jsdocs are not good enough. Consider the following code snippet from your pull request:
class MyComponentWithClasses implements Component {
classes = {'my-class': MyClass};
}The person reading the code (e.g. while reviewing a pull request on GitHub) does not see tscode comments.
e17ba58 to
76c5557
Compare
|
@bajtos PTAL. |
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test code looks much better now!
Let's discuss the classes property more, see my comment in the thread above.
Most importantly, we need documentation for these new features.
In addition to my comments below, please add some documentation for this new feature. For example, you can add a new section to https://github.com/strongloop/loopback-next/blob/master/docs/site/Creating-components.md. Please check out if there are any other places where to mention this new feature and/or cross-link to the new content you will write.
packages/core/src/component.ts
Outdated
| }; | ||
|
|
||
| /** | ||
| * An array of bindings |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's improve this docs please and add an example showing how to leverage this feature (keep it consistent with tsdocs for other Component properties).
An array of bindings to be added to the application context.
76c5557 to
d70b2a7
Compare
|
I updated to docs and tsdocs. I still prefer to keep |
6eafae3 to
4fbbb39
Compare
|
@bajtos PTAL |
bajtos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two more comments to address, the rest LGTM.
No further review is necessary as long as my comments are addressed (feel free to use a different solution from what I proposed).
docs/site/Creating-components.md
Outdated
|
|
||
| The example `MyComponent` above will add `MyController` to application's API and | ||
| create a new binding `my-value` that will be resolved using `MyValueProvider`. | ||
| create the following bindings to the application context: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if create ... to is grammatically correct. I think create ... in or add ... to would be better.
| create the following bindings to the application context: | |
| create the following bindings in the application context: |
ping @bschrammIBM @dhmlau
packages/core/src/component.ts
Outdated
| /** | ||
| * An array of bindings to be aded to the application context. For example, | ||
| * ```ts | ||
| * const bindingX = new Binding('x').to('Value X'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use the newly added sugar API please.
| * const bindingX = new Binding('x').to('Value X'); | |
| * const bindingX = Binding.bind('x').to('Value X'); |
624df0c to
f95737c
Compare
f95737c to
cc96229
Compare
Reactivation of #929
A component class can be declared as follows:
Checklist
npm testpasses on your machinepackages/cliwere updatedexamples/*were updated