Angular Challenges #5 (crud) solution.
Communicating and having a global/local state in sync with your backend is the heart of any application. You will need to master those following best practices to build strong and reliable Angular Application.
In this exercise, you have a small CRUD application, which get a list of TODOS, update and delete some todo.
Currently we have a working example but filled with lots of bad practices.
What you will need to do:
- Avoid any as a type. Using Interface to leverage Typescript type system prevent errors
- Use a separate service for all your http calls and use a BehaviourSubject for your todoList
- Use AsyncPipe to suscribe to your todo list. (Let you handle subscription, unsuscription and refresh of the page when data has changed), avoir manual subscribe when it's not needed
- Don't mutate data
// Avoid this
this.todos[todoUpdated.id - 1] = todoUpdated;
// Prefer something like this, but need to be improved because we still want the same order
this.todos = [...this.todos.filter((t) => t.id !== todoUpdated.id), todoUpdated];
- Use ChangeDectection.OnPush
- Add a Delete button: Doc of fake API
- Handle errors correctly. (Globally)
- Add a Global loading indicator. You can use MatProgressSpinnerModule
- Add 2/3 tests
- Use the component store of ngrx as a local state of your component. (or any other 3rd Party lib)
- Have a localize Loading/Error indicator, e.g. only on the Todo being processed and disable all buttons of the processed Todo. (Hint: you will need to create an ItemComponent)
- Angular CLI version 16.2.0.
- Angular
- NgRx
- There are no actions in a component store implementation.
- It is not worth adding Angular Material for a spinner. All you need is a simple component and a simple css animation.
- You can get a loading spinner from loading.io.
- OnPush works by comparing references of the inputs of the component. If you do not provide a reference to a new object and instead mutate an existing one, the OnPush change detector will not get triggered. To keep your ui in sync with state, you need to create new objects.
- @ngneat/falso - optimization bailout error - need to add "seedrandom" to allowedCommonJsDependencies array in angular.json's build options to suppress the error.
- OnStateInit & OnStoreInit seem unnecessary. Useful for toggling loading states? Niche?
- If you have to bring in a service in the constructor to use effects, why not just pass initial state to super() ?
- If you inject the service, then you need OnStateInit & OnStoreInit ?
- I had problems rendering the todos in the template. I thought using *ngIf was the problem (getting object Object) so I switched to *ngrxLet but really, it was my initial store configuration.
- LetModule is now LetDirective. Need to import LetDirective to use *ngrxLet.
- *ngrxLet directive will create an embedded view when an observable emits a value, regardless of whether it is truthy or falsy. It handles the zero problem. (Zero is a falsy value so it is difficult to conditionally render something when a successful api request returns nothing)
- I think *ngrxLet implementation is unnecessary in this implementation. The api will never return 0 todos and to have zero todos rendered in the html a user would need to delete 200 todos.
- Adding *ngrxLet is not free since you need to add another dependency - @ngrx/component.
- In Thomas's solution, he uses a switchMap and {debounce: true}. switchMap is needed to unsubscribe to many clicks of the update button and deliver the last randText() call.
- It seems like the randText() call is synchronous.
- I had a little confusion why debounce was used. The debounce pertains to the state and not the buttons in the html.
- If you don't use debounce, your stream will emit everytime a new value is patched to your state.
- "If you add a new value to todos and loading at the same time, viewmodel will emit twice, but with debounce, it will only emit once because we wait that the state is settled."
- I used a simplified callState with just a string - versus an enum with various states and a separate errorState.
- I used a global spinner that renders when callState is "loading" - used toLowerCase on callState in the html.
- A todo item component would require a separate component store with update and delete methods. The app store would not have those methods.
- I think it is possible to have a todo-item component without adding a component store to it. You would need to add {providedIn: 'root'} to app store injectable decorator and then inject the store in the todo item component. This approach makes the store like a service. See this video for a hybrid approach.
- Not a lot of great learning material out there on testing ngrx stores.
- Thomas' solution doesn't have tests and other solutions don't either.
- Once again, I think ngrx has had a lot of changes that are not really reflected in the documentation out there.
- Can't really show individual loading when the todos are being fetched -> you can show loading when deleting and updating actions are performed.
- Instead of disabling buttons ie
[disabled]="vm.callState === 'Updating' "
, I initially just removed the todo title and the buttons from the html by conditional rendering them. - Both stores have callState properties -> can show spinner when loading the todos and then show status of update / delete actions in the TodoItemComponent
- Where you put
fixture.detectChanges
matters and moving the call to it could make a failing test pass. - The commented-out html in
todo-item.component.html
is harder to test, so I changed the html, and it ended up more like the original directions' requirements. - Most likely, you will have to activate slow mode in devtools to actually see the buttons disable.
- The todo title disappears when updating / deleting. It would be preferrable to have it stay until either action completes.
- The application is fully tested. Some tests are definitely not ideal, but I am working off limited documentation and few practical examples I can follow.
- The TodoItemStore.vm$ observable is readonly so it is saved to a different variable in the todo-item component so you can override it in the todo item test.
- Updating/Deleting tests have async problems. The callState is stuck on 'updating' or 'deleting'. Need to investigate more.
- I re-wrote the tests in testing library when I submitted the pull request to the Angular Challenges Repo. You can see that pull request here.
- The TodoItemStore was not initialized correctly. This didn't prevent the app from working because I used setState instead of patchState in the todo-item Input set method.
- I looked into performance considerations. I think you could add ChangeDetectionStrategy.OnPush. I think adding a trackBy to the list would be the best optimization you could do.
Need to change angular.json to suppress a commonjs warning caused by a dependency in the @ngneat/falso package
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"allowedCommonJsDependencies":[
"seedrandom"
],
}
}
- Better test isolation
- Typescript improvements -> problems from conditional logic -> todo might be undefined, etc.
- Performance -> trackBy / ChangeDetectionStrategy.OnPush
- Medium - thomas laforge solution
- YouTube - Keeping Side Effects out of your Angular Components with NgRx ComponentStore
- YouTube - How to use NgRx ComponentStore? - Alex Okrushko | NG-DE 2022
- loading.io - css loading spinners
- Angular University - onpush change detection how it works
- Bobby Hadz - angular commonjs or amd dependencies can cause optimization bailouts
- Blog - ngrx component store introduction
- YouTube - Angular State Management Tutorial with NgRx Component Store
- LogRocket - types vs interfaces
- Stack Blitz - angular behavior subject
- indepth dev - manage angular state in your components
- indepth dev - angular viewmodel as observable
- indepth dev - component store debouncing
- dev.to - ngrx v14 update
- dev.to - removing boilerplate code
- dev.to - reactive angular templates
- dev.to - ngrxlet directive
- Stack Overflow - mock spy an imported function
- YouTube - Angular 2022Q3. Workshop: NgRx. Unit tests.
- Angular University - rxjs higher order mapping
- Medium - how to unit test the ngrx component store
- Github - RFC: Add "ngrxOnInitStore" lifecycle method to ComponentStore #3335
- Stack Overflow - observable void doesn't invoke the subscriber
- Github - not initialized store if using constructor
- Stack Overflow - getting ng rx component store not initialized error
- Github - need to mutate state when using patchState
- Stack Overflow - angular pass callback function to child component as input
- Stack Overflow - unit test with input
- Stack Overflow - testing ngOnInit
- Stack Overflow - detect changes
- Stack Overflow - unit test input set function not triggered
- Stack Overflow - angular testing an input with a set attached to it
- Stack Overflow - mock ngrx selector in a component
- Stack Overflow - can't find button inside ngIf statement
- Stack Overflow - override read only object within a jasmine unit test