Closed stupidawesome closed 4 years ago
I approve of this pull request
Effects can also be composed using the ngOnConnect
hook.
Similar to setup
from Vue, but called after the component is constructed. It receives a reactive this
context so that effects are automatically triggered when values change. When state is updated outside of ngOnConnect
, such as when a button triggers incrementCount
, its value is propagated to effects on the next change detection run. Template events automatically trigger change detection so no manual change detection is required. A non reactive reference to the component can be injected with useContext()
.
Example:
@Component()
export class MyComponent extends Connectable {
count: number = 0
incrementCount() {
this.count += 1
}
ngOnConnect() {
const http = inject(HttpClient)
effect(() => {
console.log(this.count)
return timer(1000).subscribe(() => {
this.incrementCount()
})
})
}
}
Effects can be composed by calling functions inside the ngOnConnect
hook, or provided as a Connectable
in the providers array.
// Connectable provider
const MyConnectable = connectable<MyComponent>((state) => {
effect(() => console.log("MyConnectable"))
})
// Composable function
function useConnectable(state: MyComponent) {
effect(() => console.log("useConnectable"))
}
@Component({
providers: [MyConnectable]
})
export class MyComponent extends Connectable {
ngOnConnect() {
// functional composition
useConnectable(this)
effect(() => console.log("ngOnConnect"))
}
}
After trying several different approaches I've decided that the next minor version of this library will deprecate the use of the decorator API in favour of this composition API. Additionally, components will need to extend a base Connectable
class to retain connect functionality. This is a breaking change, but I'll keep the old API around for existing projects that use it until v10 comes out. Projects not using the new API can just tree shake it away and vice versa.
Connected components will look something like this:
@Component({
selector: "app-connected",
template: `
<div>Count: {{ count }}</div>
`,
styleUrls: ["./connected.component.css"],
})
export class ConnectedComponent extends Connectable {
@Input()
count: number = 0
@Output()
countChange = new HostEmitter(true)
incrementCount() {
this.count += 1
}
ngOnConnect() {
const elementRef = inject(ElementRef)
effect(() => {
this.countChange(this.count)
return timer(1000).subscribe(() => this.incrementCount())
})
}
}
By using class initializer syntax and injecting dependencies within the ngOnConnect
hook we can omit the constructor. If the constructor is needed, it will need to pass in the node Injector
to the super call:
constructor(injector: Injector) {
super(injector)
}
We can still provide additional connectable
functions to the component if we wish.
const MyConnectable = connectable(state => {
// etc
})
@Component({
providers: [MyConnectable]
})
export class ConnectedComponent extends Connectable {
// etc
}
The Connectable
base class uses lifecycle hooks to drive the connect mechanism. Components are now "connected" during ngOnInit
, which means inputs and static queries will be resolved before the ngOnConnect
hook is fired. In previous versions components would be connected during the constructor call. The base class also implements other lifecycle hooks, so the user needs to be aware that they shouldn't override them carelessly. The user is advised not to mix ngOnConnect
with other lifecycle hooks. Connected components are presented as an alternative method of declaring angular components.
This PR adds the foundation for a composition API similar to that of Vue. There are a number of fundamental differences which will become apparent but aren't that important.
Basic usage will look something like this.
Here we borrow the use of reactive proxies from Vue to drive the scheduling of an effect. We also borrow the behaviour of lifecycle hooks so that effects are reset each time a lifecycle hook fires (with the exception of
onDestroy()
which is for cleanup only).Dependency injection works via
inject()
, which is the same as the inject function exported by Angular, except the interface has been tweaked to acceptAbstractType<T>
.The
connectable
function returns a connected provider which is instantiated after callingconnect(injector)
inside the constructor component. This provider can be used with any component or directive, as well as combined with other providers. Only effects provided to the component are executed, they are not inherited from parent injectors.