Are you keeping count? Last time I went through the basics of adding, editing and deleting from state, given that the initial list was populated from an Http Service. Today, I am diving into a specific example of continuous pagination, where the list is updated incrementally.
anchorChallenge: appending to the current list
The list is initially populated with page 1, but on subsequent calls, we need to append, rather than set list. We start here...
// pass parameters for pagination
this.tx$ = this.txService .GetTransactions({ page: 1, size: 10 }).pipe(
switchMap((txs) => this.txState.SetList(txs)));
Adding the button with a simple next click for now:
<div class="txt-c">
<button class="btn" (click)="next()">More</button>
</div>
The next
function in its simplest shape would do the following:
this is too simple but as we build the blocks we can identify which parts need rewriting
// pagination
next() {
this.tx$ = this.txService.GetTransactions({ page: 2, size: 10 }).pipe(
switchMap((txs) => {
// append to state and return state
this.txState.appendList(txs);
return this.txState.stateList$;
})
);
}
So now we do not set state list, we simply append to the current state with appendList
and return the actual stateList$
observable. This, as it is, believe it or not, actually works. The main tx$
observable resetting is not so cool, what is the use of an observable if I have to reset it like that, right? In addition to that, we do not want to save the current page anywhere as a static property, because we are a bit older than that, right? Now that we have a state class, why not make it richer to allow page parameters to be observables too?
anchorChallenge: state of a single object
Let us make room for single objects in our State class. This is not the most handsome solution, nor the most robust, but it shall do for the majority of small to medium scale apps. You can create a state of either a list, or a single item, never both. In our example we need state for the pagination params.
The final product will be used like this:
// we want to watch a new state of params and build on it
this.tx$ = this.paramState.stateSingleItem$.pipe(
switchMap(state => this.txService.GetTransactions(state)),
// given that initial BehaviorSubject is set to an empty array
// let's also change appendList to return the state observable so we can safely chain
switchMap((txs) => this.txState.appendList(txs))
);
// setoff state for first time
this.paramState.SetState({
page: 1,
size: 10
});
So now we need to do two things in our state class, update the appendList to be a bit smarter (return an observable), and add a new BehaviorSubject for single item state. Let's call that stateItem$
(so creative!)
// in state class, a new member
protected stateItem: BehaviorSubject<T | null> = new BehaviorSubject(null);
stateItem$: Observable<T | null> = this.stateItem.asObservable();
appendList(items: T[]): Observable<T[]> {
const currentList = [...this.currentList, ...items];
this.stateList.next(currentList);
// change to return pipeable (chained) observable
return this.stateList$;
}
// set single item state
SetState(item: T): Observable<T | null> {
this.stateItem.next(item);
return this.stateItem$;
}
// and a getter
get currentItem(): T | null {
return this.stateItem.getValue();
}
And of course, since we have set-state, we need, update and remove state
UpdateState(item: Partial<T>): void {
// extend the exiting items with new props, we'll enhance this more in the future
const newItem = { ...this.currentItem, ...item };
this.stateItem.next(newItem);
}
RemoveState(): void {
// simply next to null
this.stateItem.next(null);
}
Now back to our component, we need to create a new state service of "any" for now (page, and size), and inject it.
// param state service
@Injectable({ providedIn: 'root' }) // we need to talk about this later
export class ParamState extends StateService<any> {}
In Transaction list component
constructor(
private txState: TransactionState,
private txService: TransactionService,
// injecting new state
private paramState: ParamState,
) {}
ngOnInit(): void {
this.tx$ = this.paramState.stateItem$.pipe(
switchMap(state => this.txService.GetTransactions(state)),
// nice work
switchMap((txs) => this.txState.appendList(txs))
);
// setoff state for first time
this.paramState.SetState({
page: 1,
size: 10
});
}
And in transaction template, there is nothing to change. Let's now fix the next
function, so that all it does, is update the param state, now that's a relief.
next() {
// get current page state
const page = this.paramState.currentItem?.page || 0;
// pump state, and watch magic
this.paramState.UpdateState({
page: page + 1,
});
}
And because we are extending the item in the UpdateState
method, to the current item, we do not have to pass all props. But that was a shallow clone, do we need to deep clone the new item? Not sure. Do you know?
Cleanup, extend, and make a mess
It is clear to us now that some functions are redundant, and some, could return observables rather than void
. For example, I do not have to have a SetList
, if I have an empty-list and append-list. But I don't like that. It is easier from a consumer point of view to have two distinctive methods, and on the long run, it is less error prone. We can however, reuse the SetList internally, and add an empty-list feature.
appendList(items: T[]): Observable<T[]> {
const currentList = this.currentList.concat(items);
// reuse set-list
return this.SetList(currentList);
}
// add empty list for vanity purposes
emptyList() {
this.stateList.next([]);
}
But because we are going the "backwards" way of designing the class, I do really want to avoid a function I am not using in a component. So let's keep it down a bit, let's not return an observable, until we need one.
anchorNext Tuesday...
We have other properties to keep track of, specifically the total count of records on the server, and also, the local param state, instead of the one provided in root. These, I will set time to write about next week. Let me know what you think about this approach.