Important Notice
While using ChatGPT, don't copy-paste exact content. Read carefully and make necessary changes.
1. Introduction
This document outlines the coding standards and best practices for developing Angular applications within SIONIQ. It covers aspects such as component architecture, services, performance, testing, and styling. Following these standards ensures consistency, maintainability, and high-quality code.
2. Project Structure
sioniqai/
├── e2e/ # End-to-end testing folder
│ ├── src/
│ ├── app.e2e-spec.ts # E2E test specification
│ └── app.po.ts # Page object model for E2E tests
├── node_modules/ # Dependencies installed by npm
├── src/ # Main source code folder
│ ├── app/ # Core application code
│ │ ├── core/ # Core module for app-wide services, utilities, etc.
│ │ ├── sioniqai/ # SioniqAI feature modules (previously features)
│ │ │ ├── admin/ # Admin module
│ │ │ ├── inventory/ # Inventory module
│ │ │ ├── master/ # Master data module
│ │ │ │ ├── country/ # Country form
│ │ │ │ │ ├── model/ # Models for country
│ │ │ │ │ │ ├── add-country.model.ts # Model for add country
│ │ │ │ │ │ ├── view-country.model.ts # Model for view country
│ │ │ │ │ │ ├── edit-country.model.ts # Model for edit country
│ │ │ │ │ │ └── delete-country.model.ts # Model for delete country
│ │ │ │ │ ├── add-country/ # add Country component
│ │ │ │ │ │ ├── add-country.component.ts
│ │ │ │ │ │ ├── add-country.component.html
│ │ │ │ │ │ ├── add-country.component.spec.ts
│ │ │ │ │ │ └── add-country.component.css
│ │ │ │ │ ├── view-country/ # View Country component
│ │ │ │ │ │ ├── view-country.component.ts
│ │ │ │ │ │ ├── view-country.component.html
│ │ │ │ │ │ ├── view-country.component.spec.ts
│ │ │ │ │ │ └── view-country.component.css
│ │ │ │ │ ├── edit-country/ # Edit Country component
│ │ │ │ │ │ ├── edit-country.component.ts
│ │ │ │ │ │ ├── edit-country.component.html
│ │ │ │ │ │ ├── edit-country.component.spec.ts
│ │ │ │ │ │ └── edit-country.component.css
│ │ │ │ │ ├── delete-country/ # Delete Country component
│ │ │ │ │ │ ├── delete-country.component.ts
│ │ │ │ │ │ ├── delete-country.component.html
│ │ │ │ │ │ ├── delete-country.component.spec.ts
│ │ │ │ │ │ └── delete-country.component.css
│ │ │ │ │ ├── services/ # Services for country
│ │ │ │ │ │ ├── country.service.ts # Service for country operations
│ │ │ │ │ │ └── country.service.spec.ts # Unit tests for country service
│ │ │ │ ├── state/ # State form (added following the country structure)
│ │ │ │ │ ├── model/ # Models for state
│ │ │ │ │ │ ├── add-state.model.ts # Model for add state
│ │ │ │ │ │ ├── view-state.model.ts # Model for view state
│ │ │ │ │ │ ├── edit-state.model.ts # Model for edit state
│ │ │ │ │ │ └── delete-state.model.ts # Model for delete state
│ │ │ │ │ ├── add-state/ # add State component
│ │ │ │ │ │ ├── add-state.component.ts
│ │ │ │ │ │ ├── add-state.component.html
│ │ │ │ │ │ ├── add-state.component.spec.ts
│ │ │ │ │ │ └── add-state.component.css
│ │ │ │ │ ├── view-state/ # View State component
│ │ │ │ │ │ ├── view-state.component.ts
│ │ │ │ │ │ ├── view-state.component.html
│ │ │ │ │ │ ├── view-state.component.spec.ts
│ │ │ │ │ │ └── view-state.component.css
│ │ │ │ │ ├── edit-state/ # Edit State component
│ │ │ │ │ │ ├── edit-state.component.ts
│ │ │ │ │ │ ├── edit-state.component.html
│ │ │ │ │ │ ├── edit-state.component.spec.ts
│ │ │ │ │ │ └── edit-state.component.css
│ │ │ │ │ ├── delete-state/ # Delete State component
│ │ │ │ │ │ ├── delete-state.component.ts
│ │ │ │ │ │ ├── delete-state.component.html
│ │ │ │ │ │ ├── delete-state.component.spec.ts
│ │ │ │ │ │ └── delete-state.component.css
│ │ │ │ │ ├── services/ # Services for state
│ │ │ │ │ │ ├── state.service.ts # Service for state operations
│ │ │ │ │ │ └── state.service.spec.ts # Unit tests for state service
│ │ │ ├── pos/ # Point of Sale module
│ │ │ ├── b2b/ # Business-to-Business module
│ │ │ ├── manufacture/ # Manufacturing module
│ │ │ ├── scheme/ # Scheme management module
│ │ │ ├── procurement/ # Procurement module
│ │ ├── app.module.ts # Root module
│ │ ├── app.component.ts # Root component
│ │ ├── app.component.html
│ │ └── app.component.scss
│ ├── assets/ # Static assets (images, fonts, etc.)
│ ├── environments/ # Environment variables for build configurations
│ │ ├── environment.ts # Development environment
│ │ └── environment.prod.ts # Production environment
│ ├── favicon.ico # Icon for the application
│ ├── index.html # Main HTML page
│ ├── main.ts # Main entry point for the application
│ ├── polyfills.ts # Polyfills for browser compatibility
│ ├── styles.scss # Global styles
│ └── tsconfig.app.json # TypeScript configuration for the app
├── angular.json # Angular CLI configuration
├── package.json # Project dependencies and scripts
├── tsconfig.json # Global TypeScript configuration
├── tsconfig.spec.json # TypeScript configuration for tests
├── tslint.json # Linter configuration
├── karma.conf.js # Karma test runner configuration
└── README.md # Project documentation
3. Component Naming and Standards
3.1 Component Naming Convention
Components should follow a clear and consistent naming convention. The file and class names should reflect the component's purpose and be named in PascalCase format. Always include the suffix Component for clarity. For example:
| Purpose | File Name | Class Name |
|---|---|---|
| Adding a country | add-country.component.ts |
AddCountryComponent |
| Viewing a country | view-country.component.ts |
ViewCountryComponent |
| Editing a country | edit-country.component.ts |
EditCountryComponent |
| Deleting a country | delete-country.component.ts |
DeleteCountryComponent |
3.2 Component Structure and Organization
Each component should follow Angular's recommended file structure. Use separate files for the component's logic, HTML template, and CSS styling. Organize the files in a folder named after the component for easy navigation and maintainability.
add-country.component.ts: Contains the component class and logic.add-country.component.html: Contains the HTML template.add-country.component.css(or.scss): Contains component-specific styles.
3.3 Selector Naming
The component selector should use kebab-case (lowercase letters with hyphens). Prefix the selector with a short, unique identifier to avoid conflicts with standard HTML elements or other libraries.
Example:
| Component | Selector |
|---|---|
AddCountryComponent |
app-add-country |
ViewCountryComponent |
app-view-country |
EditCountryComponent |
app-edit-country |
DeleteCountryComponent |
app-delete-country |
3.4 Properties and Subscriptions
In Angular components, all properties should be strongly typed to improve readability, maintainability, and type safety. Follow naming conventions to easily distinguish between public and private properties, where:
- Public properties: Use standard camelCase.
- Private properties: Prefix with an underscore (e.g.,
_propertyName) to indicate they’re not accessible outside the class.
When using subscriptions, ensure they are properly managed and unsubscribed in ngOnDestroy to avoid memory leaks. This is especially important for components that interact with services, perform API requests, or listen to route changes.
Example Properties and Subscriptions
/**
* Category ID from route parameters, representing the category being edited.
* @type {string | null}
*/
id: string | null = null;
/**
* Category data model that holds the fetched category details for editing.
* @type {Category | undefined}
*/
category?: Category;
/**
* Subscription for route parameter changes to track category ID changes.
* @type {Subscription | undefined}
*/
private _paramsSubscription?: Subscription;
/**
* Subscription for API requests to update a category.
* @type {Subscription | undefined}
*/
private _editCategorySubscription?: Subscription;
/**
* Subscription for API requests to delete a category.
* @type {Subscription | undefined}
*/
private _deleteCategorySubscription?: Subscription;
Common Naming Conventions for Properties and Subscriptions
Consistent naming conventions help differentiate properties, particularly when handling observables and managing state.
1. Naming Conventions for Properties
- Public Properties: Use camelCase without prefixes for properties accessible outside the class. They should be descriptive, e.g.,
id,category, ormodel. - Private Properties: Use an underscore prefix (e.g.,
_propertyName) for internal, non-accessible properties to clearly differentiate them from public ones. - Boolean Properties: For boolean properties, use prefixes such as
isorhasfor clarity, e.g.,isLoaded,hasError.
2. Naming Conventions for Subscriptions
- Subscription Properties: Name subscriptions with a clear suffix
Subscription(e.g.,_paramsSubscription,_editCategorySubscription) to easily identify them. - Unsubscribing from Subscriptions: Always unsubscribe from each active subscription in the
ngOnDestroylifecycle hook, especially for observables that do not complete on their own, such as those related to route parameters or user events.
3.5 Dependency Injection in Constructor
Use dependency injection for service classes and other dependencies. Each injected property should have a descriptive type and be marked as private if it’s not used outside the class.
Example:
constructor(private categoryService: CategoryService, private router: Router) {
this.model = { name: '', urlHandle: '' };
}
3.6 Method Naming and Usage
Methods should follow camelCase naming conventions, where the first letter of each word is capitalized except for the initial word. Method names should clearly describe their functionality. Standard Angular lifecycle hooks such as ngOnInit and ngOnDestroy should be used as defined, while custom methods should follow specific prefixes based on their actions.
Here are some conventions for common method types:
- Lifecycle hooks (e.g.,
ngOnInit,ngOnDestroy): Follow Angular's naming standards. - Event-handling methods (e.g.,
onFormSubmit,onDelete): Prefix withonto indicate they handle specific events. - General methods: Clearly state the purpose, e.g.,
addCategory,fetchData.
Example:
/**
* Initializes the component.
* Called once after the component's data-bound properties have been initialized.
*/
ngOnInit(): void {
// Logic to initialize component, e.g., fetching initial data
this.fetchCategories();
}
/**
* Handles the form submission for adding a category.
* Subscribes to the addCategory service and navigates to the
* categories page upon a successful response.
*
* @returns {void}
*/
onFormSubmit(): void {
this.addCategorySubscription = this.categoryService.addCategory(this.model)
.subscribe({
next: (response) => {
this.router.navigateByUrl('/admin/categories');
}
});
}
/**
* Handles the deletion of a category.
* Subscribes to the deleteCategory service and refreshes the list
* upon successful deletion.
*
* @param {string} id - The ID of the category to delete.
* @returns {void}
*/
onDelete(id: string): void {
this.categoryService.deleteCategory(id).subscribe({
next: () => this.fetchCategories() // Refreshes the list after deletion
});
}
/**
* Cleans up subscriptions and other resources when the component is destroyed.
* This is a necessary cleanup step to prevent memory leaks.
*
* @returns {void}
*/
ngOnDestroy(): void {
this.addCategorySubscription?.unsubscribe();
}
Explanation of Method Examples
ngOnInit: This lifecycle hook is triggered once the component is initialized. It’s commonly used to fetch initial data or set up necessary state for the component.onFormSubmit: This custom method is prefixed withonto indicate it handles the form submission event. It calls the service to add a new category and, upon success, navigates to the categories page.onDelete: Another custom event-handling method, prefixed withonto indicate it handles a delete event. It calls the service to delete a category and refreshes the list upon success.ngOnDestroy: This lifecycle hook is called right before the component is destroyed. It unsubscribes from theaddCategorySubscriptionto prevent memory leaks, following best practices for managing resources.
Following these naming conventions and structuring methods with clear, descriptive names ensures the component code remains readable, maintainable, and aligns with Angular’s standards.
3.7 Lifecycle Hooks
Utilize Angular lifecycle hooks such as ngOnInit for initialization and ngOnDestroy for cleanup. Make sure to unsubscribe from observables within ngOnDestroy to prevent memory leaks.
Example:
/**
* Cleans up subscriptions when the component is destroyed.
*
* @returns {void}
*/
ngOnDestroy(): void {
this.addCategorySubscription?.unsubscribe();
}
4. Angular Service Standards
4.1 Service Responsibilities
Each service should have a single responsibility. For example, a UserService should be responsible only for user-related operations, like login, registration, and profile management. A ProductService should handle product-specific operations, such as creating, updating, and retrieving products.
4.2 Service Naming
The service file should be named according to the following convention: <feature>.service.ts.
The name of the service should clearly reflect its purpose and functionality. For example, a service handling categories should be named category.service.ts, and a service handling countries should be named country.service.ts.
4.3 Service Class Naming
The service class should be named using PascalCase (i.e., starting with an uppercase letter and capitalizing each word that follows). For example, a service responsible for managing categories should be named CategoryService, and a service for managing countries should be named CountryService.
Always include the suffix Service to indicate that the class provides services for business logic or data access.
4.4 Service File Naming
Service file names should follow the format: <feature>.service.ts. This helps to ensure consistency across the application. The file name should be in lowercase with hyphens separating words if needed.
| Feature | Service File Name | Service Class Name | Purpose |
|---|---|---|---|
| Category | category.service.ts |
CategoryService |
Handles CRUD operations related to categories. |
| Country | country.service.ts |
CountryService |
Handles operations related to countries, such as retrieving country information. |
| State | state.service.ts |
StateService |
Manages state-related operations, such as fetching state data and updating state records. |
| Product | product.service.ts |
ProductService |
Provides methods to add, update, delete, and fetch products. |
| Order | order.service.ts |
OrderService |
Manages operations related to customer orders, including creation, modification, and cancellation. |
| User | user.service.ts |
UserService |
Handles user-related tasks such as authentication, registration, and profile management. |
4.5 Method Naming
Methods within the service should be named using camelCase and should clearly describe the action they perform. The names should follow standard CRUD operation patterns:
add<Entity>: Used to add a new entity. Example:addCategory.getAll<Entities>: Used to retrieve all entities of a given type. Example:getAllCategories.get<Entity>ById: Used to retrieve a specific entity by its ID. Example:getCategoryById.update<Entity>: Used to update an existing entity. Example:updateCategory.delete<Entity>: Used to delete an entity by its ID. Example:deleteCategory.
Method Naming Examples
| Method Name | Action |
|---|---|
addCategory |
Adds a new category to the system. |
getAllCategories |
Fetches all categories from the server. |
getCountryById |
Fetches a specific country by its ID. |
updateProduct |
Updates the details of an existing product. |
deleteState |
Deletes a state from the system. |
4.6 Service Injection
Services should be provided using Angular’s @Injectable decorator. The providedIn: 'root' option should be used for services that are required globally across the application.
If you need the service to be scoped to a specific module or component, you can provide it locally within that module or component instead.
@Injectable({
providedIn: 'root'
})
export class CategoryService { }
| Service | Injection Strategy | Explanation |
|---|---|---|
| CategoryService | @Injectable({ providedIn: 'root' }) |
Available globally across the application, accessible in any component, directive, or pipe. |
| UserService | @Injectable({ providedIn: 'root' }) |
Available globally throughout the application for handling user-related operations. |
| OrderService | @Injectable({ providedIn: 'root' }) |
Available globally to manage orders across the app. |
| StateService | @Injectable({ providedIn: 'any' }) |
Scoped to a particular module for managing states in that module. |
4.7 Use Observables for Asynchronous Operations
Always use Observable for handling asynchronous operations in services. For example, when making HTTP requests, the HttpClient methods return observables that can be subscribed to or piped with operators for data transformation and error handling.
getCategories(): Observable {
return this.http.get(`${this.apiUrl}/categories`)
.pipe(
catchError(this.handleError)
);
}
4.8 Error Handling
Proper error handling should be implemented in services, especially when making HTTP requests. Use RxJS operators like catchError to catch errors and return a user-friendly message or perform fallback logic.
private handleError(error: any): Observable {
console.error('An error occurred:', error);
return throwError(() => new Error('Something went wrong.'));
}
4.9 Avoid Logic in Components
Services should abstract business logic away from components. Components should handle presentation logic, and delegate business logic to services. This helps to keep components clean and improves reusability.
// Component
this.categoryService.getAllCategories().subscribe(data => {
this.categories = data;
});
4.10 Use Dependency Injection Properly
Always use Angular's Dependency Injection (DI) to inject services into components. Avoid creating instances of services manually using new to ensure proper lifecycle management and singleton pattern.
constructor(private categoryService: CategoryService) {}
4.11 Service Method Return Types
Always explicitly define the return types for service methods. This ensures better type safety and helps maintain cleaner, more predictable code.
getAllCategories(): Observable {
return this.http.get(`${this.apiUrl}/categories`);
}
4.12 Avoid Nesting Observables
Avoid nesting observables within one another. Use operators like switchMap, mergeMap, etc., to flatten observables and avoid callback hell.
getCategory(id: string): Observable {
return this.categoryService.getCategoryById(id).pipe(
switchMap((category: Category) => this.productService.getProductsByCategory(category.id))
);
}
4.13 State Management
If your application is large, consider using state management libraries like NgRx to handle complex state logic in a predictable manner. Services can interact with the state store or dispatch actions to update the state.
4.14 Keep Services Testable
Services should be easily testable. Write unit tests for services, especially those containing business logic. Use mocks or spies for dependencies and ensure that services return expected results.
it('should fetch all categories', () => {
service.getAllCategories().subscribe(data => {
expect(data.length).toBeGreaterThan(0);
});
});
4.15 Avoid Using any Type
Avoid using the any type as much as possible. Always specify the return types and parameter types explicitly to improve type safety.
4.16 Service Interface or Model Contracts
Use interfaces for models or service responses. This ensures consistency in the data structure and better integration with TypeScript's type system.
export interface Category {
id: string;
name: string;
description: string;
}
getCategories(): Observable {
return this.http.get(`${this.apiUrl}/categories`);
}
4.17 Use Constants for API URLs
Store API URLs and other constants in environment configuration files (e.g., environment.ts) to avoid hardcoding them throughout your services. This makes it easier to update the URLs later and keeps your codebase clean.
const apiUrl = environment.apiBaseUrl;
5. Angular Model Standards
5.1 Model File Structure
Organize model files by functionality to improve code readability and maintainability. Each model should reside in a dedicated file, with the file name following a consistent pattern that reflects the model's purpose and actions.
| Purpose | Suggested File Name |
|---|---|
| Model for adding a new country | add-country.model.ts |
| Model for viewing country data | view-country.model.ts |
| Model for editing an existing country | edit-country.model.ts |
| Model for deleting a country | delete-country.model.ts |
5.2 Model Class Naming Conventions
Model classes should be named using PascalCase and should clearly represent the model's functionality. The names should include an action prefix like Add, View, Edit, or Delete followed by the entity name. This naming convention improves readability and makes it clear what each model is used for.
| Purpose | Suggested Class Name |
|---|---|
| Model for adding a country | AddCountry |
| Model for viewing country data | ViewCountry |
| Model for editing an existing country | EditCountry |
| Model for deleting a country | DeleteCountry |
5.3 Defining Class Properties
Each property in a model class should have an explicit type to maintain type safety. Use @type annotations to describe each property, providing context for its usage. Additionally, optional properties should be marked with a ? to indicate they are not required.
Example:
/**
* Model for adding a new category.
*/
export class AddCategory {
/**
* The name of the category.
* @type {string}
*/
name: string;
/**
* A unique URL handle for the category, used for routing and SEO purposes.
* @type {string}
*/
urlHandle: string;
/**
* Optional description of the category.
* @type {string | undefined}
*/
description?: string;
}
Important Notes:
- Use
@typeannotations and descriptions for all properties to improve code readability and maintainability. - Optional properties should be marked with a
?, such asdescription?: string.
6. Component HTML Standards
6.1 File Naming and Structure
The naming convention for component HTML files should follow a consistent and clear pattern. This ensures easy identification of the component’s purpose and functionality. The component HTML file should be named using **kebab-case** format, which improves readability and helps avoid potential conflicts in case-sensitive file systems.
For example:
| Component Name | HTML File Name | Description |
|---|---|---|
| AddCategoryComponent | add-category.component.html | HTML file for the AddCategoryComponent, handling category creation. |
| ViewCategoryComponent | view-category.component.html | HTML file for viewing a specific category's details. |
| EditCategoryComponent | edit-category.component.html | HTML file for editing a category's data. |
| DeleteCategoryComponent | delete-category.component.html | HTML file for deleting a category. |
This consistent naming structure allows the development team to easily locate and maintain component files based on their names.
6.2 HTML Controls ID and Name Conventions
To maintain consistency and accessibility, the naming conventions for HTML control IDs and names should follow specific rules based on the type property of the control. These conventions ensure that the controls are identifiable, usable, and easily linked to the appropriate form fields or validation rules.
General Guidelines
- The ID and name attributes should follow camelCase or kebab-case naming conventions, depending on the project's overall naming strategy.
- The ID should be unique within the page to prevent conflicts, while the name should be used for form submission purposes.
- Form fields should be labeled appropriately, with IDs and names reflecting their purpose.
ID and Name Naming Based on Control Type
Below are examples of how to name IDs and names based on the type of control, along with the type and corresponding CSS classes.
| Control Type | Type Attribute | ID Naming Convention | Name Naming Convention | Example | CSS Class |
|---|---|---|---|---|---|
| Text Input | type="text" |
kebab-case, representing the field name |
kebab-case, matching the ID attribute for form submission |
id="first-name", name="first-name" |
class="form-control" |
| Password Input | type="password" |
kebab-case, prefixed with "password" for clarity |
kebab-case, matching the ID attribute for form submission |
id="password", name="password" |
class="form-control" |
| Radio Button | type="radio" |
kebab-case, with the radio group name |
kebab-case, with the radio group name |
id="gender-male", name="gender" |
class="form-check-input" |
| Checkbox | type="checkbox" |
kebab-case, with the action or item |
kebab-case, matching the ID for form submission |
id="subscribe-newsletter", name="subscribe-newsletter" |
class="form-check-input" |
| Textarea | type="textarea" |
kebab-case, based on the field description |
kebab-case, matching the ID for form submission |
id="user-feedback", name="user-feedback" |
class="form-control" |
| Dropdown (Select) | type="select" |
kebab-case, representing the field name |
kebab-case, representing the field name |
id="country-select", name="country" |
class="form-select" |
By adhering to these conventions, we ensure that the HTML controls are consistently named and accessible, making it easier to maintain and understand the forms in your application.
7. Routing Module Standards
Routing in Angular allows navigation between views or components within the application. Angular's RouterModule provides a powerful mechanism for routing and handling URLs. This section outlines the standards to follow when defining routes and setting up the routing module in an Angular application.
7.1 Defining Routes
In Angular, routes are defined in an array of objects, where each object specifies the URL path and the associated component. Each route object should include at least two properties: path and component.
Best Practices for Defining Routes
- Path Naming: Use descriptive and clear paths that follow a logical structure. For example, use
/admin/categoriesfor the category list page,/admin/categories/addfor adding a category, etc. - Use Parameters for Dynamic Data: Use route parameters (e.g.,
:id) when the route requires dynamic data, such as an ID for editing a category or blog post. - Lazy Loading: For larger applications, consider using lazy loading to load feature modules on demand. This helps reduce the initial loading time of the application.
- Redirects: Consider using the
redirectToproperty when redirecting from one path to another. - Child Routes: Use child routes to create a hierarchy of routes for nested components.
7.2 Routing Module Setup
The routing module is a feature module that imports RouterModule and defines the routes. It is typically set up in the app-routing.module.ts file.
7.3 Best Practices for Route Naming
- Use plural forms for collections: For a list of items, such as categories or blog posts, use the plural form (e.g.,
/admin/categories,/admin/blogposts). - Use descriptive and consistent naming conventions: Route paths should reflect the content they represent (e.g.,
/admin/categories/:idfor editing a specific category). - Avoid deep nesting of routes: Keep route nesting minimal to avoid complex route structures that are difficult to manage.
7.4 Route Guards
Use route guards to control navigation based on certain conditions. For example, route guards can be used to check if a user is authenticated before allowing them to navigate to certain routes. Guards should be set up in the routing configuration and can be added using the canActivate or canLoad properties.
import { AuthGuard } from './guards/auth.guard';
const routes: Routes = [
{
path: 'admin/categories',
component: CategoryListComponent,
canActivate: [AuthGuard]
}
];
7.5 Lazy Loading Routes
To improve performance, use lazy loading for feature modules. Lazy loading allows Angular to load feature modules only when needed. This can greatly improve the initial load time of your application.
const routes: Routes = [
{
path: 'admin/categories',
loadChildren: () => import('./features/category/category.module').then(m => m.CategoryModule)
}
];