-
Install and setup all dependencies needed by the library
-
Install the library
`npm install ngx-rbk-utils`
-
Import the
NgxRbkUtilsModule
in yourAppModule
and pass thebuildState()
function in theNgxsModule.forRoot()
method. Also import theToastModule
fromprimeng
. Also add a provider forNgxRbkUtilsConfig
IMPORTANT: you must import the module BEFORE all other
ngxs
modulesExample:
@NgModule({ declarations: [ ... ], imports: [ ... NgxRbkUtilsModule.forRoot(rbkConfig), // before NGXS modules ... NgxsModule.forRoot(buildState(), { developmentMode: !environment.production }), // call the buildState() NgxsRouterPluginModule.forRoot(), NgxsReduxDevtoolsPluginModule.forRoot({ disabled: environment.production}), ... ], providers: [{ provide: NgxRbkUtilsConfig, useValue: rbkConfig }], bootstrap: [] }) export class AppModule { ... }
-
In the constructor of the
AppModule
, inject the NGRXStore
and dispatch the following action:export class AppModule { constructor(private store: Store) { this.store.dispatch(new ApplicationActions.NgRxInitialized()); } }
-
Stil in
AppModule
, define the following as provider:providers: [{ provide: NgxRbkUtilsConfig, useValue: rbkConfig }
Obs: This is required for injection of NgxRbkUtilsConfig into auth.guard
-
In your
AppComponent
inject theBoilerplateService
and call the.init()
method.constructor(private boilerplateService: BoilerplateService) { this.boilerplateService.init(); }
-
In the
.forRoot()
method of the library you need to pass a configuration object. Please see the configuration section for the details. -
Define your
AppStateModel
for intellisense purposesimport { AppStateModel as RootStateModel } from 'ngx-rbk-utils'; export interface AppStateModel extends RootStateModel { database: { state1: State1StateModel, state2: State2DbStateModel, state3: State3DbStateModel, }, features: { feature1: Feature1StateModel, feature2: Feature2StateModel, }; }
-
Add
<p-toast>
and<smz-general-dialog>
to yourapp.component.html
-
Make sure you don't import
MessageService
fromprimeng
in any of modules/components or theToastActions
from the library will not work -
In the constructor of your error page, clear the local storage with (or at least clear the properties that store the accessToken and the refreshTokemn):
constructor() {
localStorage.clear();
}
import { NgxRbkUtilsConfig } from 'ngx-rbk-utils';
export const rbkConfig: NgxRbkUtilsConfig = {
// Application name, will be shown in the browser's tab name
applicationName: string,
useTitleService?: boolean, // Flag to indicate wheter the TitleService should be initialized or not.
routes: {
// Url path of the landing page of your application, this will be used to redirect the user when he tries to access a protected route and is not authenticated. Ex.: '/'
nonAuthenticatedRoute: string,
// Url path of the authenticated root page of your application, this will be used to redirect the user when he tries to access a protected route, is authenticated but does not have access to it. Ex.: '/editor'
authenticatedRoute: string,
// Url path of the login page of your application, this will be used to redirect the user if the library fails to refresh the JWT access token. Ex.: '/login'
login: string,
// Url path of the error page of your application this will be used to redirect the user if a request fail with the following status:
// 500, 0 or unmapped status. Ex.: '/error
error: string
},
authentication: {
login: {
// Full url of your login endpoint, must be POST and accepts the following object: { username: string, password: string }
url: string,
// How you want to handle the errors returned by this endpoint, possible values are 'toast', 'dialog' nad 'none'
errorHandlingType: string,
// Name of the access token property in the endpoint response
responsePropertyName: string,
// How you want to handle the loading state on the store, possible values are 'global', 'local' and 'none'
loadingBehavior: string,
},
refreshToken: {
// Full url of your login endpoint, must be POST and accepts the following object: { username: string, password: string }
url: `https://dev.meuencartedigital.com.br/auth/refresh-token`,
// How you want to handle the errors returned by this endpoint, possible values are 'toast', 'dialog' nad 'none'
loadingBehavior: 'none',
// Name of the access token property in the endpoint response
errorHandlingType: 'toast',
// How you want to handle the loading state on the store, possible values are 'global', 'local' and 'none'
responsePropertyName: 'refreshToken',
// Optional extra properties you can pass to refresh token api for validation. ex: {username: 'name', applicationId:'appId'}
extraProperties: {[name: string]: string}
},
// Array of configuration objects for claims that need to be read from the access token. Please see the authorization section for more details
accessTokenClaims: {claimName: string, propertyName: string, type: string} []
},
state: {
// Setup for the database part of the state
database: {[name: string]: DatabaseStateParameters},
// Setup for the feature part of the state
feature: {[name: string]: DatabaseStateParameters}
},
httpBehaviors: {
// Default parameters to be used in all HTTP requests on services that inherit from BaseApiService
defaultParameters: {
compression: false,
authentication: true,
needToRefreshToken: true,
loadingBehavior: 'global',
errorHandlingType: 'toast',
localLoadingTag: null,
restoreStateOnError: true
}
},
// Default contiguration for the PrimeNG Toast
toastConfig: {
severity: 'success',
life: 3000,
sticky: false,
closable: true,
successTitle: 'SUCESSO',
errorTitle: 'ERRO',
warningTitle: 'AVISO',
infoTitle: 'INFORMAÇÃO',
},
// Default dialog titles
dialogsConfig: {
errorDialogTitle: 'ERRO',
warningDialogTitle: 'ALERTA'
}
};
The library uses many internal NGXS
actions and exposes a series of them to the user. The following list constains the actions of the library, organized by namespace:
-
Clear
: cleans theDatabaseStore
, using the callbacks specified in the config file (this action is automatically called when theApplicationActions.Logout
action is dispatched). -
Restore
: re-emits the entire database store, in its current state, to force UI updates. This action can be manually dispatched but it's automatically dispatched everytime an http request returns a code different than 200. This behavior can be overrided for each individual request using therestoreStateOnError
property in thehttpBehaviors
section of the config file.
-
Clear
: cleans theFeatureStore
, using the callbacks specified in the config file. (this action is automatically called when theApplicationActions.Logout
action is dispatched). -
Restore
: re-emits the entire features store, in its current state, to force UI updates. This action can be manually dispatched but it's automatically dispatched everytime an http request returns a code different than 200. This behavior can be overrided for each individual request using therestoreStateOnError
property in thehttpBehaviors
section of the config file.
-
Clear
: cleans theGlobalStore
, using the default data (this action is automatically called when theApplicationActions.Logout
action is dispatched). -
Restore
: re-emits the entire global store, in its current state, to force UI updates. This action can be manually dispatched but it's automatically dispatched everytime an http request returns a code different than 200. This behavior can be overrided for each individual request using therestoreStateOnError
property in thehttpBehaviors
section of the config file.
The
RemoteLogin
andLogout
actions should be manually fired by the client application.
-
RemoteLogin
: this action should be fired by the client application when the user clicks a LOGIN button. It will fire the login http request using the parameters of the config file. If this succeeds theRemoteLoginSuccess
actions is dispatched. If it fails the error is handled either by showing a dialog with the error or a toast as set in the config file. The config section of the login request in the config file allows the user to pass extra static parameters with the login http request. -
Logout
: this action should be manually dispatched when the user sign out.
The following actions are automatically dispatched inside the library, in common use cases there is no need to dispatch or handle them:
-
LocalLogin
: when this action is dispatched the library tries to authenticate using the access token in the browser local storage. -
LocalLoginSuccess
: this action is dispatched when the library successfully authenticate the user using the data stored in the local storage. This action automatically starts the process of loading the database stores from your API. -
LocalLoginFailure
: this action is dispatched when the library failed to authenticate using the data in the local storage. -
RemoteLoginSuccess
: this actions is dispatched when the login http resquests returns200
and it automatically dispatched aNavigate
action to theauthenticatedRoot
route path in the config file. -
RefreshTokenSuccess
: this action is dispatched everytime an access token is successfully refreshed.
IMPORTANT
: these actions are not meant to be dispatched by the client application, they are for internal use only. But since they will show in the Redux Dev Tools, they will be described here.
-
HandleHttpErrorWithDialog
: this action is dispatched when an error handling flag in the config file set todialog
. -
HandleHttpErrorWithToast
: this action is dispatched when an error handling flag in the config file set totoast
. -
StartGlobalLoading
: this action is dispatched when an http request is fired with the loading behavior flag set toglobal
nad no other request is in progress. -
StopGlobalLoading
: this action is dispatched when the last http request in progress is finished. -
NgRxInitialized
: this actions is dispatched to flag whenNGXS
is initialized in the client application. -
PushLocalLoading
: this action is dispatched when an http request is started with the loading behavior flag set totoast
-
PopLocalLoading
: this action is dispatched when an http request with the loading behavior flag set totoast
is finished
The library automatically creates the global
piece of the store containing application and authentication related data. To use this data the user manually query the store or use the selectors already exposed by the library:
-
accessToken
: returns the access token asstring
-
refreshToken
: returns the refresh token asstring
-
isAuthenticated
: returns true if the user is authenticated -
userdata
: return the whole object constructed using the JWT token data. Since its properties are dynamic (set in the config file) this selector returns an object of typeany
and should be cast to your user model as can be seen in the example below.const data = this.store.selectSnapshot(AuthenticationSelectors.userdata) as MyUserModel
-
username
: returns the name of user as it is in the JWT access token in the claim, but only if a propertyusername
is setup in theaccessTokenClaims
property in the config file. -
hasGroupOfClaimAccess
: receives an array of claims and returns true if the user has all claims in its access token. The claims can have a domain or not. More details in theClaims Domain
section -
hasClaimAccess
: receives a claim and returns true if the user has this claim in its access token. The claim can have a domain or not. More details in theClaims Domain
section
-
globalIsLoading
: returns true if there is any http request (using theglobal
in the loading behavior flag) in progress. -
isDatabaseStateInitialized
: returns true if all pieces of thedatabase
store are initialized. -
isWaitingRequest
: receives a string containing a local loading http tag and returns true if there is any http request with this tag in progress.
One of the main features of this library is to easy the burden of loading shared states (called as database states here) and knowing if their data is already loaded.
In the concept of database stores all data that should be readibly available to the application and shared between components should be loaded before component initialization.
This way all this loading is centralized in a single part of the code and you don't have to manually fire their load actions in multiple parts of the code.
This approach works very well and make the perfect balance between performance and resources. Only the states needed for a given component will be loaded and only if they are not ready.
One of the greatest features of NGXS
is also the one that can gives more headaches in some scenarios, the selectSnapshot
function. When the users enters your application using a route that needs certain data to render something, Angular will load the component before the data arrive from the backend and you're using selectSnapshot
instead of select
from NGXS.
The obvious solution is to use the select
function. But in many cases the select
is an overkill since it returns an Observable
and it's a lot easier to work directly with the results of a variable. This is specially true for the contents of dropdown or lists. For this reason we prefer to use the selectSnapshot
feature of NGXS, but this gets the value of the state in the moment it was called and it's never updated again. So we need to be sure to call it only we are sure that the data is loaded in the store.
To solve this problem the library exposes a route guard called RbkDatabaseGuard
. You specify the states that are needed for a given route and it makes this data to be ready before letting Angular load the component.
The following code show how the setup is made in the route:
{
path: 'my-route',
canActivate: [ RbkAuthGuard, RbkDatabaseStateGuard ],
component: MyComponent,
data: { title: 'Title', breadcrumb: 'Title', requiredStates: ['accounts', 'categories'] },
}
As can be seen, the RbkDatabaseStateGuard
is being used in the canActivate
property of the route and the required states for the route are specified in the array of the requiredStates
property of the data
property.
The values used in the requiredStates
property must match the names of the states in the configuration file.
Rembember that if any of the required states needs authentication, the
RbkAuthGuard
guard must be specified before theRbkDatabaseStateGuard
guard.
There is also the DatabaseSelectors.areStatesInitialized
selector that lets you manually check if some states are initialized or not. It receives an array of strings representing the states you want to check.
With the route approach you don't have to bother with anything in your component, when Angular reaches it you can be 100% sure that all required states are ready.
For this feature to work properly the states must be set in the configuration file like this:
state: {
database: {
transactions: { // <- State name (same name used in the state class in the @State() decorator)
state: TrasactionsDbState, // <- State class
loadAction: TransactionsDbActions.LoadAll, // <- Action used to load the state
clearFunction: getTransactionsInitialState, // <- Function to restore this state to it's initial value
cacheTimeout: 5 // <- Cache timeout (in minutes)
},
categories: {
state: CategoriesDbState,
loadAction: CategoriesDbActions.LoadAll,
clearFunction: getCategoriesInitialState,
cacheTimeout: 1
},
accounts: {
state: AccountsDbState,
loadAction: AccountsDbActions.LoadAll,
clearFunction: getAccountTypesInitialState,
cacheTimeout: 1
},
accountTypes: {
state: AccountTypesDbState,
loadAction: AccountTypesDbActions.LoadAll,
clearFunction: getAccountsInitialState,
cacheTimeout: 1
},
},
// The feature state configuration only requires the state and clear function property
feature: {
categoriesManager: {
state: CategoriesManagerState,
clearFunction: getCategoriesManagerInitialState
},
accountsManager: {
state: AccountsManagerState,
clearFunction: getAccountsManagerInitialState
},
}
},
IMPORTANT:
this feature will only work if the states used as requirements for the routes have alastUpdated
property that is initialized withnull
and set with the current time when it's first populated.
The library provides an authentication route guard called RbkAuthGuard
.
The checks made by the guard are the following:
-
if the route does not need any special claim, return the value in the
AuthenticationSelectors.isAuthenticated
selector -
if the route needs any special claim, it should be passed to the router like this:
const routes: Routes = [ { path: 'authenticated-route', component: AuthenticatedRouteComponent, canActivate: [ RbkAuthGuard ], data: { title: 'My Route', breadcrumb: 'My Route', claim: 'CAN_ACCESS_MY_AUTHENTICATED_ROUTE' }, children: [ ... ] }, ];
-
when the route needs any special claim, the guard will search for that claim in the user data, if the user is authenticated and he has the needed claim the access is allowed.
-
when the route needs any special claim and the guard doesn't find that claim directly in the user data, it will try to find a claim in the format
{{domain}}|{{claim}}
. The domain is read from thedomain
property in the user data. If you need to use this domain feature, you need to set it in theaccessTokenClaims
property in the config file.
This pipe is used to restrict access of HTML components using *ngIf
. For instance, the following sample renders the div
only if the user has the IS_ADMIN
claim in the JWT token.
export class MyComponent {
public isAdminClaim = 'IS_ADMIN';
constructor() {
}
}
<div *ngIf="!(isAdminClaim | rbkCanAccess)">
</div>
All http errors are automatically handled by the library through interceptors. This behavior is set in the config file by the httpBehaviors
property and can be overrided individually for each request, like this:
return this.http.post(url, body,
this.generateDefaultHeaders({
errorHandlingType: 'toast',
}));
There are three possible values: toast
, dialog
and none
.
Every time request returns an error, all the states are emmited again, in their current state, force UI refresh. This behavior can be overrided for each individual request using the restoreStateOnError
property in the httpBehaviors
section of the config file.
The library offers three ways of handling in progress http requests:
-
GLOBAL: when am http request is started, the loading flag in the store is set. If another request is started before the previous is finished, the flag will remain set. Only when the last request is finished the flag will be set to false. This flag can be observed using the
ApplicationSelectors.globalIsLoading
selector. -
LOCAL: with this option an individual flag is set for each request and they can be observed using the
ApplicationSelectors.isWaitingRequest
selector, passing theid
used in the http request. The following sample shows how to use this option for a give request:
return this.http.post(url, body,
this.generateDefaultHeaders({
loadingBehavior: 'local',
localLoadingTag: 'my-request-id'
}));
- NONE: none of the store loading flags will be set.
You can set the default values for all http requests in the httpBehaviors
property of the config file. But this value can be individually overriden on each request like in the examples above.
To fully use all the features of the library, all the http services must inherit from the BaseApiService
. This way all requests will use the default values set in the config file, but if any value needs to be overriden it should be done using the generateDefaultHeaders
method of te base class. The following sample shows how to do this:
return this.http.post(url, body,
this.generateDefaultHeaders({
errorHandlingType: 'dialog',
loadingBehavior: 'local',
localLoadingTag: 'my-request-id',
compression: true,
authentication: true,
needToRefreshToken: true
}));
Note that there is no need to set all values, only the ones that are actually needed.
This service is used to set the title that will be displayed in the browser tab. The title needs to be set in the route, and if it's not set the service will try to infere it from the route name. The following example show how the title is set in the route.
{
path: 'my-route',
component: MyComponent,
data: { title: 'My Title' },
}
The example above will set the tab title to XX > My Title
, in which the XX
is the value of the applicationName
property in the config file.
The TitleService
is already injected by the library.
This service constructs a breadcrum object to be used in the application breadcrum (really!). It's used the same way as the TitleService
and it uses the same property of the route.
The BreadcrumService
is already injected by the library.
rbkTableClearExtension
: this directive is used to help with the cleaning of filters inTable
andTreeTable
components. It extends them by appending theclear
methods to them. It must be used in the table and every filter input that needs to be cleared must use therbkInputClearExtension
directive. Below is a sample of how it should be used.
<p-treeTable #dt [tableClearExtension]="dt" ...>
<ng-template pTemplate="colgroup">
...
</ng-template>
<ng-template pTemplate="header">
<tr>
...
</tr>
<tr>
<th><input #input1 [inputClearExtension]="{ table: dt, component: input1 }" pInputText type="text" (input)="dt.filter($event.target.value, 'productHtml', 'contains')"></th>
<th><p-multiSelect #input2 [inputClearExtension]="{ table: dt, component: input2 }" (onChange)="dt.filter($event.value, 'details.warehouse.id', 'simplenamed')" appendTo="body"></p-multiSelect></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-rowNode let-rowData="rowData">
...
</ng-template>
</p-treeTable>
-
replaceArrayItem:
: replaces an item in a given array by other with the sameid
. The items must have anid
. It's used to help with state updates. -
isWithinTime:
: verifies if the data of a specific database store is still valid given the maximum age in minutes. -
orderArrayByProperty:
: sort an array of objects by a given property. -
deepClone:
: make a deep clone of a javascript object (only serializable properties are supported) -
isEmpty:
: checks wheter a string isnull
,undefined
or''
. -
clearArray:
: removes all elements of an array. -
flattenObject:
: when dialog data is returned by thengx-smz
dialogs, it may have many properties of typeSimpleNamedEntity
, and this data is usually used to be send to an API, which probably needs a propertyemployeeId
of typestring
instead of a propertyemployee
of type{ id: string, name: string}
. This function does this kind of transformation in the object, it finds all object propertyes that have anid
property and converts it to{{propertyName}}id
. It also has a generic type to cast the data to the specified type.
The following example shows the input of the function the what the output would be.
// Use (result will be of type 'MyType')
const result = flattenObject<MyType>(input);
// input
{
id: '2',
color: '#4d3a8f',
category: {
id: '9',
name: 'My category',
icon: 'fa fas-home'
},
order: 19,
parent: {
id: '3',
name: 'My parent'
}
}
// output
{
id: '2',
color: '#4d3a8f',
categoryId: '9',
order: 19,
parentId: '3'
}
fixDateProperties:
: converts any date property stored as string or number to a Date object. The property name must starts withdate
or ends withDate
. The operation is done in the same object passed to the function as a parameter. If the date is stored as epoch it can be either in seconds or miliseconds.
fixDates
: this operator do the same as thefixDateProperties
, but wrapped in a more convenient way for objects coming from an http request.