Overview
In this article, we will got through step by step to add new "Manage Staff" feature.
Some of you may have the question about structure of project, please write it on the paper. We will revise later.
As mentioned in 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 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 staring 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 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 exception.
So, I prefer, 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 continue with Staffs page in "pages" folder:
"pages/staffs.html" conaints 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" component was defined. this will handle common behavior/ ui for all page inside the system. So in each page (Staffs page in HRM module in this case). We only need to provide necessary feature for "Staffs" page only.
and "page/staffs.ts" containts 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 define new HRM module and register "staffs" mapped to Staffs page.
the last step to register the HRM with current application. In TinyERP we can have multiple applicaitons and each application can have different number of modules. "dashboard" was default app at the momemnt.
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 question, please postpone it to next part "Revise Manage Staffs".
Now, You can compile and run. the result on browser as below:
Ok, Above result just shows us that new HRM module was integrated into current system.
Continue 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, 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, there is a very simple change, it is grid control with options and fetch attribute.
For [options]: this specify how to initialize the grid control, such as column, title, ...
For [fetch]: 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 see a little bit about "model", each page should have it owns model which contains necessary data and reduce 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 column for the grid, it were: fristName, 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, just simple to call to 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 was not prefer to use magic code. See coding technique mode 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 inherit 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 a simulate the response from restful web service.
Ok, let 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 compile and run to see how's it going:
Oh, exception was throw. This error related to the ioc registration, we have IStaffService and StaffService, but did not map them together. Let 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 compile and refresh again:
there is "Manage Staffs" on left panel.
The content for the client side a little bit long now, so let continue with the api part in next article, I may be long also.
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