Overview
In this article, we will go through the steps to add new "Manage Staff" feature.
Some of you may have a question about structure of project, please write it on paper. We will revise later.
As mentioned in the previous part, Web application uses client and server module. So:
- Client: will take responsibility to interact with end-user. So it will display the system information (report, data, ...) on UI or receive user action (such as: enter value, click on button to perform an action, ...)
- Server side: handling the business logic of the system (the application)
For example, with "send email" function:
- Client allows end-user to enter the recipient address, title, content of email, attachment (if any), ... and listen on "send request" from end-user by clicking on "send" button on UI.
- Server side: will handle "send email" logic, such as: receive above information, validate, send email, store in necessary repository, ...
"Staff management" was usually a feature inside HRM module. In this case, we will do the same.
For Client Side
Add "hrm" folder under modules folder and structure for this as picture below:
The starting point for module was located in "hrmModule.ts":
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { AppCommon, BaseModule, ModuleConfig, ModuleNames } from "@app/common";
import { HrmRoute } from "./hrmRoute";
import ioc from "./_share/config/ioc";
import routes from "./_share/config/routes";
import mainMenus from "./_share/config/mainMenus";
@NgModule({
imports: [CommonModule, FormsModule, AppCommon, HrmRoute],
declarations: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HrmModule extends BaseModule {
constructor() {
super(new ModuleConfig(ModuleNames.HRM, ioc, routes));
this.mainMenus = mainMenus;
}
}
In this file, we register the necessary information about HRM module, such as: name, ioc registration, routes, menus.
Route for module was defined in "<moduleName>Route.ts" file, it was hrmModule.ts in this case:
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { RouterModule } from "@angular/router";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import helperFacade, { AppCommon } from "@app/common";
import routes from "./_share/config/routes";
import { Staffs } from "./pages/staffs";
let routeConfigs = [
{ path: "", redirectTo: routes.staffs.path, pathMatch: "full" },
{ path: routes.staffs.path, component: Staffs}
];
@NgModule({
imports: [CommonModule, FormsModule, RouterModule.forChild(routeConfigs), AppCommon],
exports: [RouterModule, Staffs],
declarations: [Staffs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HrmRoute { }
This is where we map each relative URI with specified page. In this file. we map "<...>/staffs" with "Staffs" page. So when user browses to "<uri>/staffs", Staffs page will be rendered in the browser.
For "./_share/config/routes.ts", it was simple to define the object rather than using "magic string" in our app. By default, we usually define the route like this:
let routeConfigs = [
{ path: "", redirectTo: "staffs", pathMatch: "full" },
{ path: "staffs", component: Staffs}
];
With this code, we use "staffs
" as a string
. This will create potential bugs as mentioned in "Coding Technique". If your member has a typo "staff
" instead of "staffs
", the system will crash or throw an exception.
So, I prefer that we use in this format:
let routeConfigs = [
{ path: "", redirectTo: routes.staffs.path, pathMatch: "full" },
{ path: routes.staffs.path, component: Staffs}
];
This will not 100% sure, but reduce the percentage of potential bugs in your system.
For "./_share/config/route.ts", there is nothing special:
let routes: any = {
staffs: { name: "hrm.staffs", path: "staffs" }
};
export default routes;
For ioc.ts and mainMenus.ts. They were empty at the moment.
Let's continue with Staffs page in "pages" folder:
"pages/staffs.html" contains the HTML for "Staffs" page, we use the hard text at the moment:
<page>
<page-header>Manage Staffs</page-header>
<page-content>Content of "manage staffs" page</page-content>
</page>
We can see that, there are "page
" components defined. This will handle common behavior/ UI for all pages inside the system. So in each page (Staffs page in HRM module in this case). We need to provide necessary feature for "Staffs" page only and "page/staffs.ts" contains logic for "Staffs" page:
import {Component} from "@angular/core";
import {BasePage} from "@app/common";
@Component({
templateUrl:"src/modules/hrm/pages/staffs.html"
})
export class Staffs extends BasePage<any>{
}
This was pure Angular component (please have a look at "angular component" for more information. All pages in system need to inherit from BasePage<Model>
class.
Ok, up to now, we already defined new HRM module and register "staffs
" mapped to Staffs page.
The last step is to register the HRM with current application. In TinyERP, we can have multiple applications and each application can have different number of modules. "dashboard
" was default app at the moment.
Open "<root>/src/apps/dashboard/modules.ts" and register HRM module:
import { ModuleNames, IModuleConfigItem } from "@app/common";
let modules: Array<IModuleConfigItem> = [
{ name: ModuleNames.Support, urlPrefix: ModuleNames.Support, path: ModuleNames.Support},
{ name: ModuleNames.HRM, urlPrefix: ModuleNames.HRM, path: ModuleNames.HRM}
];
export default modules;
Then, declare that, which theme can use HRM module in "<root>/apps/dashboard/config/themes.ts" (see modules property):
import { ITheme, AppThemeType, ModuleNames } from "@app/common";
let themes: Array<ITheme> = [
{
name: AppThemeType.Default,
isDefault:true,
urlPrefix: AppThemeType.Default,
modules: [
{name: ModuleNames.Support, isDefault:true},
{name:ModuleNames.HRM}
]
}
];
export default themes;
Ok, if you have questions, please postpone it to the next part "Revise Manage Staffs".
Now, you can compile and run. The result in the browser is as below:
Ok, the above result just shows us that new HRM module was integrated into the current system.
Continue to add the list of staffs into Staffs page, there is available grid control for us to use (with basic feature), this is the wrapper of Jquery Table control. For more information about "Wrapping current js/ jquery control in Angular", please visit my blog, a new article about this will be published at http://tranthanhtu.vn:
<page>
<page-header>Manage Staffs</page-header>
<page-content>
<grid
[options]="model.options"
[fetch]="fetch">
</grid>
</page-content>
</page>
If you compare this page with the previous one, there is a very simple change, it is grid control with options and fetch attribute.
For [options]
: this specifies how to initialize the grid control, such as column, title, ...
For [fetch]
: this will be called to get data from remote source and bind into grid control.
If you expect more features for your grid control, you can do the same way and wrap expected current grid control into Angular if not available.
Let's see a little bit about "model
", each page should have it own model which contains necessary data and reduces complexity for "staffs.ts". This was simple:
export class StaffsModel{
public options: any = {};
constructor(){
this.options = {
columns: [
{ field: "firstName", title: "First Name"},
{ field: "lastName", title: "Last Name"},
{ field: "department", title: "Department"}
]
};
}
}
In this model, we declare 3 columns for the grid, they are: firstName
, lastName
and department
.
Each column has field and title (text to be displayed on the header of grid) and staffs.ts we also change a little bit:
export class Staffs extends BasePage<StaffsModel>{
public model: StaffsModel;
constructor() {
super();
let self = this;
self.model = new StaffsModel();
}
public fetch(): Promise {
let def: Promise = PromiseFactory.create();
let service: IStaffService = window.ioc.resolve(LocalIoCNames.IStaffService);
service.getStaffs().then(function (searchResult: any) {
def.resolve(searchResult.items || []);
});
return def;
}
}
There is a new model with type of StaffsModel
and fetch
method.
In side fetch, simply call the appropriated service to get data from remote source (defined in IStaffService
interface and StaffService
class). Currently, the grid just provides the basic feature for sample only, if you want more complex behaviors, please feel free to change in "<root>/src/modules/common/components/grid/", such as: passing paging parameter, ...
For the LocalIoCNames.IStaffService
, this was just a constant mapped to a string
as I do not prefer to use magic code. See coding technique mode for more information.
export const LocalIoCNames = {
IStaffService: "IStaffService"
};
And IStaffService
:
export interface IStaffService{
getStaffs():Promise;
}
This was only the interface, we can use this interface everywhere in the app.
and ServiceStaff
was implementation of IServiceStaff
:
import {BaseService, Promise, IConnector, IoCNames} from "@app/common";
import {IStaffService} from "./istaffService";
export class StaffService extends BaseService implements IStaffService{
public getStaffs():Promise{
let uri="/api/hrm/staffs.json";
let iconnector: IConnector = window.ioc.resolve(IoCNames.IConnector);
return iconnector.get(uri);
}
}
We can see that StaffService
inherits from BaseService
, this was defined in "app/common".
In this class, we use IConnector
and get data from "staffs.json" file, as the API for this was not available at the moment. We will replace this by the API URI.
And content of json file:
{
"errors":[],
"status":"200",
"data":{
"totalItems":"2",
"items":[
{"firstName":"Tu", "lastName":"Tran", "department":"Department 1"},
{"firstName":"Tu1", "lastName":"Tran", "department":"Department 1"}
]
}
}
This was just to simulate the response from restful web service.
Ok, let's review:
- Define staffs page with the grid: done
- Load data from remote source and bind into grid: done
- Implement
IStaffService
and StaffService
: done - Call to hard-value from json file: done
Ok, let's compile and run to see how it's going:
Oh, an exception was throw. This error is related to the ioc registration, we have IStaffService
and StaffService
, but did not map them together. Let's add this into "<module>/_share/config/ioc.ts":
import {LocalIoCNames} from "../enum";
import {IoCLifeCycle} from "@app/common";
import {StaffService} from "../services/staffService";
let ioc: Array<IIoCConfigItem> = [
{ name: LocalIoCNames.IStaffService, instance: StaffService, lifeCycle: IoCLifeCycle.Transient }
];
export default ioc;
Compile and refresh again, we will see the result as below:
Ok, cool. It works perfectly.
I expect to have "Manage Staffs" on the left panel, so, I can access to this feature by clicking on this menu. Add the following line into "<module folder>/_share/config/mainMenus.ts":
import { IResourceManager, IoCNames } from "@app/common";
let mainMenus: Array<any> = [
{ text: "Manage Staffs", url: "default/hrm/staffs", cls: "" },
];
export default mainMenus;
Let's compile and refresh again:
There is "Manage Staffs" on the left panel.
The content for the client side is a little bit long now, so let's continue with the API part in my next article, it may be long as well.
For the reference source code in this part, please have a look at https://github.com/tranthanhtu0vn/TinyERP (branch: feature/manage_staff).
Other articles in series:
Thank you for reading.
CodeProject
Note: Please like and share with your friends, I will really appreciate that.