- [x] implement
- [x] test
- [x] web docs
- [x] JS docs
Often setters are enough. In such cases, we can use state.set({prop}).
If we want to apply behaviour we need to get a stream. This often leads to bloated code with Subjects.
@Component({
template: `
<input (input)="searchBtn.next($event.target.value)" /> {{search$ | async}}
<button (click)="submitBtn.next()" >Submit<button>
`
})
class Component {
_submitBtn = new Sunject<void>();
_search = new Sunject<string>();
set submitBtn() {
_submitBtn.next()
}
set search(search: string) {
_search.next(search)
}
get search$() {
return _search.asObservable();
}
refresh$ = this. search$.pipe(
switchMap(fetchData)
)
}
We could use a Proxy object and a little TS to reduce the boilerplate here:
@Component({
template: `
<input (input)="uiActions.searchBtn($event.target.value)" /> {{search$ | async}}
<button (click)="uiActions.submit()" >Submit<button>
`
})
class Component {
uiActions = getActions<{submit: viod, search: string}>()
refresh$ = this. uiActions.search$.pipe(
switchMap(fetchData)
)
}
A config object could even get rid of the mapping in the template:
@Component({
template: `
<input (input)="uiActions.search($event)" /> {{search$ | async}}
<button (click)="uiActions.submit()" >Submit<button>
`
})
class Component {
uiActions = getActions<{submit: viod, search: string}>({search: (e) => e.target.value})
refresh$ = this. uiActions.search$.pipe(
switchMap(fetchData)
)
}
If we use the actions only in the class with RxEffects
or RxState
we won't cause any memory leaks.
@Component({
template: `
<input (input)="uiActions.search($event.target.value)" /> {{search$ | async}}
<button (click)="uiActions.submit()" >Submit<button>
`
})
class Component {
uiActions = getActions<{submit: viod, search: string}>()
refresh$ = this. uiActions.search$.pipe(
switchMap(fetchData)
)
}
A config object could even get rid of the mapping in the template:
@Component({
template: `
<input (input)="uiActions.search($event)" /> {{search$ | async}}
<button (click)="uiActions.submit()" >Submit<button>
`,
providers: [ RxState, RxEffects]
})
class Component {
uiActions = getActions<{submit: viod, search: string}>({search: (e) => e.target.value})
constructor( private effects: RxEffects, private state: RxState<{list: any[]}>) {
this.state('list', this. uiActions.search$.pipe(switchMap(fetchData)));
this.effects(this.uiActions.search$, fetchData);
}
}
As we have no subscriber after the component is destroyed we don't need to complete the Subjects.
However if we would pass it to another service which has a longer life time we could create a memory leak:
@Component({
template: `
<input (input)="uiActions.search($event)" /> {{search$ | async}}
<button (click)="uiActions.submit()" >Submit<button>
`,
providers: [ RxState, RxEffects]
})
class Component {
uiActions = getActions<{submit: viod, search: string}>({search: (e) => e.target.value})
constructor( private globalService: GlobalService) {
this.globalService.connectSearchTrigger(this.uiActions.search$);
}
}
Here, as the global service lives longer than the component we have subscribers on the subject after the component is destroyed.
For such situations, a hook is needed to get the component destroyed event.
Update:
Due to typing issues, I had to refactor to an architecture where we have a wrapper scope for the action create function.
Incredible BIG THANKS to @ddprrt for the support here.
The current solution provides a service and a factory function.
The service is hooked into Angular's life-cycles and so it cleans up on destruction.
The factory function returns a pair of functions, create and destroy that can be used manually.
Service:
export class RxActionFactory<T extends Actions> implements OnDestroy {
private readonly subjects: SubjectMap<T> = {} as SubjectMap<T>;
create<U extends ActionTransforms<T> = {}>(transforms?: U): RxActions<T, U> {
return new Proxy(
{} as RxActions<T, U>,
actionProxyHandler(this.subjects, transforms)
) as RxActions<T, U>;
}
destroy() {
for (let subjectsKey in this.subjects) {
this.subjects[subjectsKey].complete();
}
}
ngOnDestroy() {
this.destroy();
}
}
Service Usage:
interface StateActions {
refreshList: viod,
}
@Injectable({providedIn: 'root'})
class GlobalState implements OnDestroy {
_fac = new RxActionFactory<StateActions >();
uiActions = this._fac.create()
ngOnDestroy() {
this._fac.destroy();
}
Component Usage:
interface UIActions {
submit: viod,
search: string
}
@Component({
template: `
<input (input)="uiActions.search($event)" /> {{search$ | async}}
<button (click)="uiActions.submit()" >Submit<button>
`,
providers: [ RxState, RxEffects, RxActionFactory]
})
class Component {
uiActions = this.actionFactory.create({
search: (e: Event | string | number) => e.target.value !== undefined ? e.target.value + '' : e + ''
})
constructor(
private globalService: GlobalService,
private actionFactory: RxActionFactory<UIActions>
) {
this.globalService.connectSearchTrigger(this.uiActions.search$);
}
}
The factory function is maybe irrelevant for Angular but still worth looking at it:
export function rxActionCreator<T extends {}>() {
const subjects = {} as SubjectMap<T>;
return {
create,
destroy: (): void => {
for (let subjectsKey in subjects) {
subjects[subjectsKey].complete();
}
},
};
function create<U extends ActionTransforms<T> = {}>(
transforms?: U
): RxActions<T, U> {
return new Proxy(
{} as RxActions<T, U>,
actionProxyHandler(subjects, transforms)
) as RxActions<T, U>;
}
}
Usage:
type UIActions = {
search: string;
check: number;
};
const {create, destroy} = rxActionCreator<UIActions>();
const actions = create({
search: (v: number) => 'string',
});
create.search(4);
Update:
I removed the function because we can also do new RxActionsFactory<any>().create()
.
Additionally, docus are now present as well as tests.
Missin features:
Missing tests:
- subscribe to a subject that is not emitted on
- emit on a property without subscriber
- error in transform
The only flaw we could maybe see here is the wrapper e.g. new RxActionsFactory().create().prop
For now I have no clue how we could get rid of it and expose the setter and observables directly e.g. new RxActionsFactory().prop.
One problem is typescript, the other Proxies, classes and typescript :). Maybe problems to solve in a later step...
Related issues:
#423, #1013
{ } State đ Test đ Docs API đ ī¸ CDK