Skip to content
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

Initial implementation #1

Merged
merged 27 commits into from
Feb 1, 2018
Merged

Initial implementation #1

merged 27 commits into from
Feb 1, 2018

Conversation

petrovica
Copy link
Contributor

No description provided.

angular-cli and others added 4 commits January 10, 2018 12:49
    _                      _                 ____ _     ___
   / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
 / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
               |___/
"styleExt": "scss",
"component": {},
"schematics": {
"collection": "@nrwl/schematics"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nrwl/schematics provides additional templates for Angular CLI code generator. The only thing I used it for is ngrx setup. More info on: https://nrwl.io/nx/guide-setting-up-ngrx

nx module also provides other helpers like effects for data loading (we probably won't use that)
https://nrwl.io/nx/guide-data-persistence

or multirepo setup (that we might actually use)
https://nrwl.io/nx/guide-nx-workspace

@@ -0,0 +1,14 @@
import { AppPage } from './app.po';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All *.spec.ts files are not modifed - they are generated by Angular CLI. I'll fix them later when the implementation will be "stable".

{
path: 'users',
loadChildren: './users/users.module#UsersModule'
}
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess all modules should be lazy loaded. New ngrx v4 is prepared for that quite well 👍

OPINION NEEDED:
Btw. this file was generated by the CLI. Not sure why they started using extra module for that instead simple file with route configuration, but I kept it this way. Any ideas why not to use it this way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About lazy loading feature modules, why not? I would keep it that way by default as well.

As for having whole separate module for routing definition per feature, that has been in the official style guide for some time. I think it makes sense as it helps to separate what belongs where. Even before you basically created a module but now it is made more proper. Plus it keeps all the route specific services, guards, etc in one place.

<app-api-loader></app-api-loader>
<app-api-error-toast></app-api-error-toast>`
})
export class AppComponent {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an entry component.

OPINION NEEDED:
I know, it's more consistent to have extra HTML template for that, but don't you think we could make some rule that really small templates that won't change much in the future could be inlined?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A agree that angular gets bloated when everything gets its own file. Or if you include them by default even if they are left empty. So my opinion if we do not want to be as strict:

  • No need to keep empty files if there is no upcoming short-term expectation for their usage.
  • Small templates could be inlined but a line-limit should be specified. Maybe up to 5 lines? Or is that too low? Up to 10 would be my absolute maximum.
  • Styles probably the same as templates. Maybe an added constraint might be to one selector or something.

If the above is allowed it would be great if there were some tslint plugins/rules to check it is followed correctly. Otherwise it could end up with templates of whatever size always inlined.

Personally I would keep everything separate. However this is definitely something that should be decided and agreed upon among a group of people.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to find any TSLint plugin regarding this topic 🙁

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it was just an idea to potentially enforce the rule properly and not just by word. No worries.

Copy link
Contributor

@yuqiora yuqiora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First quick review batch of some of the root files.

Having both package-lock.json and yarn.lock is a bit questionable to me. Unless the tools update both at the same time. That being said if maybe it is not such an issue as it will be used mostly by one developer, they can pick one and delete the other.

Might continue with additional comments either as separate comments or as another review batches. Depends what I find.

"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be my first choice but since this is just a training/sample project the default app prefix should be ok as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the default fot the application, but king of expect that if this implementation would be approved, modules in code/ would be separated in they own modules with custom prefix.

"styleExt": "scss",
"component": {},
"schematics": {
"collection": "@nrwl/schematics"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for this. I was actually also thinking about using these since they already incorporate ngrx / state management. Better to use available solution than reinventing the wheel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to say I like the default template pretty much.

colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A detail but I would prefer the use of ChromeHeadless here. I don't like the browser window popping up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll look into that when I start writing test 👍

package.json Outdated
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth tweaking these a little. Or at least look at the default options and consider modifying those, e.g. formatters for output, etc. But it is on low priority.

tsconfig.json Outdated
"es2017",
"dom"
],
"noUnusedLocals": true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is the generated default. We likely do not need to do much else here. The only thing I would consider is to apply some additional checks that were supposed to be removed from tslint, like the noUnsedLocals here. Though not sure if there are any others like that we might want or need. Again, low priority and possibly unnecessary. At least until we have some agreed upon configuration to follow across projects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to add noUnsedLocals by myself. I would love to add another options for both TS and Angular (angularCompilerOptions), but it would be great if we could agree on some sane default.

tslint.json Outdated
"label-position": true,
"max-line-length": [
true,
140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would lower down to 100 to follow prettier config. At the maximum I would use 120 so that it is not such a jump.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this will force the developer do format long lines manually instead of letting prettier do it instead of him. Wouldn't it be better to disable this rule completely for the comfort?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is another option. Really depends how tslint and prettier are setup to work together.
Related:

package.json Outdated
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"ngrx-action-creator-factory": "0.0.1",
"prettier": "^1.10.2",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing any call in scripts or more importantly the lint-staged setup. Another thing to consider is how to combine with tslint. If we want to make it part of one lint process or if we want to keep them separate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tooling is completely untouched too (except for running prettier manually), but having that in an exmple app would be great, I'll add that 👍

import { EntityRepositoryStateRoot } from './core/entity-repository/entity-repository.interfaces';
import { RouterState } from './core/router/router.interfaces';

export type AppState = ApiState & CrudStateRoot & EntityRepositoryStateRoot & RouterState;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ngrx code that Angular CLI generates (provided by nrwl/schematics module) define name of the namespace directly in the module. Example:

users/+state/users.interfaces.ts

export interface UsersState { ... }
export interface UsersStateRoot {
  users: UsersState // <-- namespace of the module in store is chosen in here
}

This means that when you import the feature module in main module, you can't decide where to put it in the store. Pros:

  • you are able to define all the selector of UsersModule directly in UsersModule.

Cons

  • you can't change the namespace of the module's store
  • there might be conflicts in the naming

This is pretty neat feature for feature modules (eg. users, settings), but what about modules that are imported into the application as npm deps (eg. CrudModule, EntityRepositoryModule). Best thing would probably be to define the namespace in app.module.ts for those modules, but sometimes any of those modules need to access data of another npm module - in this application CrudResolver from CrudModule needs to access data of current route that are stored in the state by RouterModule. So...

OPINION NEEDED:
Should modules like CrudModule or EntityRepositoryModule also define their namespace by themselves so they can communicate between each other when needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would need to look into this a bit more to give proper answer. For now I think it is not such a bad idea to have them namespaced directly. As for conflicts, you could always isolate them a bit more or prefix the namespaces just like with components. However I am new to redux and state management overall so I don't know what other consequences there are to one or the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, in that case we could discuss that on next JS trainings meeting. I would probably prefer namespacing them directly too. I would probably be easier, I'm just not sure if it has some real negative impact on the app.

api: apiReducer,
crud: crudReducer,
entityRepository: entityRepositoryReducer,
router: routerReducer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again the same question - import modules into the Store/Effects like this (explicitly selecting they namespace) or create feature modules for them and choosing the namespace there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about it the more I think that it is better for each module to define its namespace by itself.

!environment.production ? StoreDevtoolsModule.instrument({ maxAge: 50 }) : [],

// @nx
NxModule.forRoot(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not actually used. I you think we won't really need it, I'll remove it. One think we might use it for are tests. They have some helpers to test RxJS streams.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at its code it only provides DataPersistence capability. If we do not use it and do not plan to then there is no reason to keep it here. And if we do want to use its helpers for tests then we could include it just in tests themselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use DataPersistence from the start for data loading, but it does not support blocking requests like resolvers do. We might still use it for POST/PUT/DELETE requests as a helper.

@@ -0,0 +1,14 @@
import { ActionCreatorFactory } from 'ngrx-action-creator-factory';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used ngrx-action-creator-factory to simplify the setup of action creators. Great thing it that it support types.

@@ -0,0 +1,53 @@
import { Store } from '@ngrx/store';
import { catchError } from 'rxjs/operators/catchError';
import { tap } from 'rxjs/operators/tap';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using pipeable operators from RxJS 5.5 was weird from the start but I got use to it and I like it! The only downside for now is probably a lot of imports. I had to import them one by one because Webpack cant handle tree shaking with grouped imports:
import { map, filter, scan } from 'rxjs/operators';

OPINION NEEDED:
It this preferred usage in your opinion? If yes, I probably need to clean up few last import of rxjs/add/operator/... which I used in the beginning...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go with the pipeable operators. I think it is easier to reason about to always import just what you really need. With the older approach of rxjs/add/operator/... it was a bit confusing when and where to import so that it works properly.

import { ApiError, BusinessValidationError, NETWORK_ERROR, UNKNOWN_API_ERROR } from './api.errors';
import { HTTP_CONFLICT } from './api.status-codes';

export const apiErrorHandling = () => {
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decorators might be nice syntactic sugar for some things. This one wraps method that makes an API call (HttpClient) and handles some basic errors.

OPINION NEEDED:
It this ok?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting usage and definitely not unheard of in angular/typescript. Might result in a cleaner code. Though depends how it is used. E.g. if we would want multiple decorators their order might be important. Another point to consider is how often it is used. Both for error handling and loading indicator, do we use them for every single call or just for a select few? If it is used everywhere it might be better to approach this differently. On the other hand, we build this with the intent of modularizing it and possibly using on other projects.

In short, should be ok (unless we find out that it isn't).

And sorry for nitpicking but you are not really handling errors here. Rather you translate or normalize them and throw them anyway. Actual error handling has to be done separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll rename it 👍

store: Store<any>;
}

export const withLodingIndicator = () => {
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really weird usage of decorators, but I wanted to show it as an example. Basically what this one does is that is sets isLoading state for the application before any HTTP request fires and stops the loading when it completes.

The controversial thing in here is that it requires the class of decorated method to have HasStore interface - to have store property of type Store.

OPINION NEEDED:
(insert vomit emoticon) or tolerable hacky solution? Using service for that would be probably cleaner...
Please check the usage in one of the Resolvers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem somewhat hacky but for the moment I cannot think of a better solution if we do want to do it with decorator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TYPO: withLodingIndicator should be withLoadingIndicator

this.store.dispatch(apiActionCreators.startLoading());
return oldFn
.apply(this, arguments)
.pipe(tap(() => this.store.dispatch(apiActionCreators.stopLoading())));
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: I actially need to use finally or something like that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think observables have something like finally. The closest thing would be waiting for actual completion of the stream on the subscription.

And to explicitly mention it, you should stop loading when the call's success and failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could I have overlooked that. My mistake. But at least you know what to do there.

@@ -0,0 +1,21 @@
export const NETWORK_ERROR = 'networkError';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied from react-training app.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit redundant to have both public properties and their matching public method. But well, not that important.

export const API_BASE_URL = new InjectionToken<string>('API baseURL');

@Injectable()
export class APIInterceptor implements HttpInterceptor {
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does 2 things

  • it sets baseUrl to API server based on configration of the application
  • it dispatches errors to Api module

What I don't like - this is global, you can't have 2 two HttpClients with different interceptors. That's just not possible in Angular. You coud probably instantiate HttpClient by yourself and pass custom request handler, but I'm not sure about that.
So this might cause some troubles in the future (eg. with external libraries), but for now it's the easiest solution. Better solution would probably be to create custom ApiService that would wrap each request manually.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be ok like this.

import { CrudId } from './crud.interfaces';
import { CrudService } from './crud.service';

export abstract class CrudResolver implements Resolve<CrudId | CrudId[]> {
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reducer could reduce app complexity quite a bit! How it works:

  • reducers in feature modules should inherit from this one
  • it's enough to set few properties and one method and it will take care of loading the data, normalizing it and storing everything in the store all by itself

Properties:

  • schema: normalizr schema which should be used to normalize data
  • route: route ID (used as namespace where to save normalized data)
  • key: resolver name (used as namespace where to save normalized data)

Methods:

  • params: returns array of parameters that will be passed to API call
  • data: fires HTTP request (or fetches the data anyhow)

data$ is a stream of data fetched from server - it automatically process the data (normalize, store). Also it's possible to get instance of the resolver anywhere by DI and call resolve() directly to reload the data (the API will probably change a bit)
I have some ideas how to improve this so it would be much easier to use one resolver to do both:

  • initial resolve by the router
  • to use the same resolver in any component to handle data fetching (eg. list of users with sorting/filtering)

Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost forgot. There's another propery:

  • blocking: tells the resolver if it should let router wait for the data or transition to the destination route immediately


import { CrudStateRoot } from './crud.interfaces';

export const getCrud = (store: Store<CrudStateRoot>) => store.select('crud');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CrudModule defines it's own selector. If we want to support this, modules have to have pre-defined namespace in the store.

url: string;
params: Params;
queryParams: Params;
}
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On each route change, current router state is flattened and stored in the store. I think it would be better to store the route config the same way angular provides it - different ActivatedRouteSnapshot of each route so that child parent resolver can't access children's params. Also flattended router snapshot does not store route data, because handling conflicts would be pretty hard.

Another solution would be to store ActivatedRouteSnapshot for each resolver. That way it would be possible to have conflicting parameter names and route data, but some data would be duplicated in the store.

This is a bit broader topic and it might be benefitial to discuss it in person or on next meeting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes:

  • I would save fragment in there as well.
  • As for data, that is a bit questionable. Duplicates in store is a valid point. Right now I see two possible solutions. We could ignore data that goes directly to store and include only the rest but the problem would be to differentiate between them. The other option would be to make resolvers working with store to return just the normalized keys instead of the data itself.
  • The argument about conflicts in data between parent and children is applicable to params as well. One way to deal with it would be to require users to use more descriptive param names, e.g. /users/:userId and /skills/:skillId instead of /users/:id and /skills/:id.
  • Not using the whole Snapshot as is should be ok. I don't think we need everything from there.

As for flattening, have you considered to use paramsInheritanceStrategy set to 'always'?
See https://angular.io/api/router/ExtraOptions#paramsInheritanceStrategy for details. It might make things a bit easier.

Copy link
Contributor Author

@petrovica petrovica Jan 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other option would be to make resolvers working with store to return just the normalized keys instead of the data itself.

CrudResolver works this way - save data to store and return only IDs. And also RouterStateSerializer can't access resolved data, so we don't need to worry about that. I saw the issue in conflicting data in routes configuration, but you're right, the same could happen with params.

I would save fragment in there as well.

fragment and data (just the data in route configuation) added

As for flattening, have you considered to use paramsInheritanceStrategy set to 'always'?

Great idea, thanks 👍


import { FlatRouterStateSnapshot } from './router.interfaces';

export class CustomRouterStateSerializer implements RouterStateSerializer<FlatRouterStateSnapshot> {
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This saves route state to the store on every transition. We can change it's implementation, this one is pretty simple, since I'm not sure if we should flatten it at all.

@withLodingIndicator()
saveUser(user) {
return this.api.saveUser(user);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the usage of controversial withLoadingIndicator decorator I mentioned before.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, still not sure what is the best way to go about it. Where should we easily track the loading indicators and increase/decrease the counter. Not used to think in redux way so I will not be of much help yet on this matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the need to have Store on decorated class. withLoadingIndicator will use static ApiService method. Much cleaner and less confusing to use IMO.

return [route.params.userId];
}

data(userId: CrudId) {
Copy link
Contributor Author

@petrovica petrovica Jan 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserDetailResolver does not use withLoadingIndicator and has blocking = false - this way it possible to handle loading indication on in each component separately.

@@ -0,0 +1,11 @@
export interface SingleEntityRepository<T> {
[id: string]: T;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatelly TypeScript does not support custom types for keys. Only string or number is allowed and even though CrudId is defined as: type CrudId = string | number;, this does not work.
I'm considering using string for this on all the places instead of CrudId. Even if it would be a number in reality, we won't do any mathematical operations with it and we'll treat it just as an identifier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand it properly, the CrudId would be in format of e.g. 1 or 'user-1' or 'user-1-skill-1' etc, coming from normalizr, right? Using strings everywhere in here would indeed make it simpler. We don't care what value lives there internally, just that we can safely use it to identify the entity. Unless I misunderstand this.

Anyway, I am for keeping ids as simple strings in all instances. It would simplify the matter.

- add `husky` and `lint-staged`
- set precommit hook to run `tslint` (with no fix) and `prettier --write`
- they are not used for now but with `tslint` precommit hook we were unable to commit
- it's just a simple wrapper around `input`
- it uses `FormControl` and shows errors
- also extract `ActionCreatorFactory` from 3rd party module because it does not supports `strictNullChecks`
Copy link
Contributor

@yuqiora yuqiora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of app/core with app/shared and main.ts.

@@ -0,0 +1,11 @@
export interface SingleEntityRepository<T> {
[id: string]: T;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand it properly, the CrudId would be in format of e.g. 1 or 'user-1' or 'user-1-skill-1' etc, coming from normalizr, right? Using strings everywhere in here would indeed make it simpler. We don't care what value lives there internally, just that we can safely use it to identify the entity. Unless I misunderstand this.

Anyway, I am for keeping ids as simple strings in all instances. It would simplify the matter.

import { EntityRepositoryStateRoot } from './entity-repository.interfaces';

export const getEntityRepository = (store: Store<EntityRepositoryStateRoot>) =>
store.select('entityRepository');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better if even the namespace key for specific store state was exported as well. Easy to make a typo or not change everywhere otherwise.


normalizeAndStore(data: any | any[], schema: any): CrudId | CrudId[] {
const { entities: repository, result } = normalize(data, schema);
this.store.dispatch(entityRepositoryActionCreators.repositoryHasChanged(repository));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The renaming of entities to repository does not serve any purpose here in my opinion.

url: string;
params: Params;
queryParams: Params;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes:

  • I would save fragment in there as well.
  • As for data, that is a bit questionable. Duplicates in store is a valid point. Right now I see two possible solutions. We could ignore data that goes directly to store and include only the rest but the problem would be to differentiate between them. The other option would be to make resolvers working with store to return just the normalized keys instead of the data itself.
  • The argument about conflicts in data between parent and children is applicable to params as well. One way to deal with it would be to require users to use more descriptive param names, e.g. /users/:userId and /skills/:skillId instead of /users/:id and /skills/:id.
  • Not using the whole Snapshot as is should be ok. I don't think we need everything from there.

As for flattening, have you considered to use paramsInheritanceStrategy set to 'always'?
See https://angular.io/api/router/ExtraOptions#paramsInheritanceStrategy for details. It might make things a bit easier.

src/main.ts Outdated
}

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should include HMR setup as well. It is helpful to have for better developer experience and trainees should not be bothered to set it up. It would be better to have it as part of the example so that they can just reuse and without trying to figure out what exactly needs to be done.

https://github.com/angular/angular-cli/wiki/stories-configure-hmr (personally I would setup dev env with HMR enabled by default and not create a separate one, testing on the other hand could have it disabled)
https://github.com/gdi2290/angular-hmr

@yuqiora
Copy link
Contributor

yuqiora commented Jan 29, 2018

Some resources for testing Angular with Jest in case we want to switch or provide the alternative:
https://www.xfive.co/blog/testing-angular-faster-jest/
https://izifortune.com/unit-testing-angular-applications-with-jest/
angular/angular#12409
angular/angular-cli#4543

@petrovica
Copy link
Contributor Author

I managed to implement almost everything you mentioned here - things I reacted to with 👍 or added comment with more detailed explanation of implementation.

I have one idea how to fix ugly @withLoadingIndicator and then I would like to separate modules from core/(api|crud|entity-repository|...) into real modules. So main question - are you ok with current implementation in general or would you like to rewrite some of it and postpone the separtion?

Copy link
Contributor

@yuqiora yuqiora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest, which should be just users and maybe some other minor things that emerged along the way.

Highlights would be:

  • use exported keys/namespace to not repeat the strings everywhere
  • possibly default to OnPush change detection, should be configurable in CLI config

@@ -0,0 +1,12 @@
import { ActionCreatorFactory } from 'ngrx-action-creator-factory';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not like the +state in the path. Might be just the +. Though I guess this is generated by CLI/NX. So whatever.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's generated 😐

save$ = this.actions$
.ofType(usersActionTypes.SAVE_USER)
.pipe(
switchMap(({ payload: user }: { payload: User }) => this.saveUser(user)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to ask once again, is the renaming really necessary? You see in the type signature that payload is User. Why rename it when it is not in conflict with anything and is used as is right afterwards?

store: Store<any>;
}

export const withLodingIndicator = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TYPO: withLodingIndicator should be withLoadingIndicator

@withLodingIndicator()
saveUser(user) {
return this.api.saveUser(user);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, still not sure what is the best way to go about it. Where should we easily track the loading indicators and increase/decrease the counter. Not used to think in redux way so I will not be of much help yet on this matter.

import { usersReducer } from './users.reducer';
import { usersInitialState } from './users.init';
import { Users } from './users.interfaces';
import { DataLoaded } from './users.actions';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid imports referring either to non-existing files or exported members of those files. This is likely generated as well but I wonder if it created a reducer spec file without the reducer itself. Same with those other imports. I guess what happened is that you replaced the users.reducer with the CRUD one, probably, removed it and forgot about the test. Please cleanup properly after yourself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said, I did not touch *.spec.ts files except for muting some errors.

</div>
<br>

<router-outlet></router-outlet>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking ahead I guess you use this for the create user. If I remember right the original idea was to have this rendered as a modal. Do you rely on styling to make it so later on with some positioning etc? The other option would be to use e.g. dialog from material or even portal from CDK. However for a training app that might not be necessary.

As a side-note another thing I remember a bit. I think the idea was for create and edit to use the same component, right? But here they are actually wrappers around another component that drives the behavior while each wrapper is a bit different. Personally I don't think that is a problem. This does not need to and shouldn't be a direct copy of the react one. However if we plan to use the training app as a sort of example or showcase of some practices we should agree on what exactly it should be. Be it across the both react and angular or individually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use mat-modal for that. Actually I would prefer to use Material Design components even for basic input elements and error toasts instead of implementing them manually.

React version of training app does it the same way - shared form component. And to be honest, I prefer it this way instead of just one shared component. It gives you more options for modifications. But we can change that...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am all for using the material module. It is official (or as close to be the default as say forms or router modules) and now also keeps the same versioning I think. We do not need to automatically adopt material design itself but its utility components/services/etc would definitely be helpful. That can be done in separate PR though.

I am not against sharing the chunk that makes the most sense instead of twisting one component to fit everywhere. I agree with you that it provides more options this way in how we want to utilize it in different places.

},
children: [
{
path: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not currently sure but shouldn't one of these empty-path routes have the pathMatch: 'full'? Or whatever the setting is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathMatch: 'full' on any of those empty routes would break routing. On the first (root) one it's because it would try to math the rest of the route, but '' !== '/users'. I'm not sure why it would break the second route too, that should IMO work on leaf route.

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider having resolvers among these providers instead of the main module. However it likely does not really matter where they are between those two.

@NgModule({
imports: [CommonModule],
declarations: [],
exports: [FormsModule, ReactiveFormsModule]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommonModule should be exported as well so that we do not have to import it everywhere.


@NgModule({
imports: [
CommonModule,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommonModule should be re-exported by SharedModule.

@yuqiora
Copy link
Contributor

yuqiora commented Jan 31, 2018

First pass of the initial state done. Now to go through the updates pushed today. This should have definitely been done in smaller steps but it is too late for that now.

Generally I think it is ok. There might be some small things to improve here and there but the basic structure and content seems fine. As for the whole ngrx / redux stuff I don't think I am the right person to comment on that. Did not try to run it in any way yet and not sure when I will have the chance. Should do so though to get a full picture.

Needs unit tests of course but please save them for separate PR. We can also discuss there whether to remain with jasmine/karma or to try jest. Styling would also be a good idea to add soon. More so to showcase the difference between global vs component styles. But again, separate PR and even the react one has its styling still open.

@petrovica
Copy link
Contributor Author

This should have definitely been done in smaller steps but it is too late for that now.

Yeah, I know and sorry for that, but I had no idea how should this work from the beginning and it was more of a research.

@petrovica
Copy link
Contributor Author

Fixed most of the issues and tips you mentioned here. It helped clean up the code a bit 👍 .
What's next:

  • rewrite selectors into services
  • use Crud configuration resolvers have (route and key) in selectors
  • separate core modules into real modules

So if the architecure is ok, should we do the same Brano did with React training - create a multirepo core modules from here? Or merge this and and iterate on that?

package.json Outdated
@@ -51,11 +56,19 @@
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"lint-staged": "^6.0.1",
"ngrx-action-creator-factory": "0.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly no longer needed as it seems like you copied / recreated it directly in the project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I already removed it this morning. I also rewrote the selectors in services. I'm trying push the changes in batches, but if you want, I can push it right away.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather postpone improvements to separate PRs from now on. This gets to a size where it is hard to discuss. The PR became a repo of itself.

"src/**/*.ts": [
"precommit-lint",
"prettier --write",
"git add"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be ok like this for the training app. Or at least for this PR. We can explore later on how to combine lint and prettier more seamlessly and to avoid conflicts between the two later on.

constructor(public type: string = 'NOT_SET', public payload: T | null = null) {}
}
@Injectable()
export class ActionCreatorFactory {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is now directly in the project the previous one should be removed from dependencies in package.json.

import { apiActionCreators } from './api.actions';

@Injectable()
export class ApiService {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely seems like a more elegant solution. Good job. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also have an idea how to get rid of class inheritance in resolvers (extends CrudResolver + the need to call super(crud, ...)). This is pretty small change, but it might make the API even easier to use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, go ahead, if it makes it easier or more comfortable to use. But as mentioned, might be better to do in separate PR.

{
provide: APP_INITIALIZER,
useFactory: () => () => {},
deps: [ApiService],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this in context it is easy to understand what it does and why it is here. But for newcomers a comment explaining this would be helpful. Or as mentioned above use OnInit of the app root component. Though the timing would likely be a bit different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be hidden inside (future) ApiModule. I wanted to "autostart" this service when user imports the module.
So when we'll separate this into it's own module, it will solve itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes sense.

}

constructor(store: Store<any>) {
ApiService.requests$.subscribe(loading =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we deal with unsubscribing this as well somewhere? Though the question would be where and more importantly when. We want this subscribed for the whole lifetime of the app after all. One possible option might be to use the root app component and its OnInit and OnDestroy lifecycle hooks.

And while on the topics of observables and subscribing it would be great if we could show somewhere in here the takeUntil approach which makes it much easier to manage mutliple subscriptions. Though I don't remember anymore if you actually subscribe anywhere. There is mostly the async pipe in templates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to unsubscribe from it? Like you said, We need this the whole liffetime of the app. When user closes the app, this whole application gets destroyed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is why I am asking. I am not aware of any apparent disadvantages at the moment. If it results in some memory or performance leak and such. We can continue with it as is for now I guess. We'll see how it goes.

@Input() control: FormControl | null = null;

get errors() {
return this.control && this.control.errors && Object.keys(this.control.errors);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good for a start I guess. Though for better feedback the messages should be customized and user get more than just required or email or maxlength, etc. Not sure if we want something predefined already. Different apps would have different needs for error messages. And in training itself it might be a good exercise to let trainee expand on this and provide more verbose messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we could use Material Design components directly. Wouldn't it be better (and faster)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this relates to the comment. Unless Material components have predefined error messages. Anyway, I mentioned elsewhere that I am for using material module. Just that we do not need to conform to material design principles/rules/etc. Those are two different things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you're right. I forgot how mat-error works.

@@ -5,5 +5,6 @@

export const environment = {
production: false,
hmr: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we tackle unit tests we might actually need another environment for it where it is disabled. But that is for later and depends how we will approach those unit tests.


HMR might also need some additional setup to work correctly with store, see https://github.com/gdi2290/angular-hmr. CLI does not deal with store itself so their wiki likely skipped the other stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to implement this yesterday. It kinda works - Webpack only sends changed code, there's no real page reload, Angular just starts the application again. This means eg. resolvers are executed again. It's much faster, but it's still not real "module reload".

I tried to use the configuration from https://github.com/gdi2290/angular-hmr , but without any change in behavior. I guess I overlooked something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing that we can look into more properly later on. For now we should focus on the important stuff. As long as we have some basic tooling in place it should be fine.

@yuqiora
Copy link
Contributor

yuqiora commented Feb 1, 2018

Generally I am ok with its overall structure. Might be better to merge and continue with other changes as separate PRs. We can discuss on the call today if anyone else has more to say.

@petrovica
Copy link
Contributor Author

petrovica commented Feb 1, 2018

Last commit just cleans up package.json a bit - mainly removes ngrx-action-creator-factory. So don't worry, no new features :)

Copy link
Contributor

@yuqiora yuqiora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

At this point I think most of the issues were addressed or they were at least identified and put in queue. The basic tooling is in place and the overall structure looks sound. Unless there are any objections I would prefer to merge this and continue with additional fixes and improvements in separate PRs of more manageable size. Plus the associated discussion would be better kept in context.

@yuqiora yuqiora merged commit 4d4714b into master Feb 1, 2018
@yuqiora yuqiora deleted the initial-implementation branch February 1, 2018 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants