Portal Development Guidelines - global-121/121-platform GitHub Wiki
Portal Development Guidelines
All of the below are merely guidelines. If you are in doubt about something, ask :)
File & folder structure
This is our desired file & folder structure:
app
βββ components
β βββ component-name
β βββ component-name.component.html
β βββ component-name.component.spec.ts
β βββ component-name.component.ts
βββ domains
β βββ domain-name
β βββ domain-name.api.service.ts
β βββ domain-name.helpers.ts
β βββ domain-name.model.ts
βββ guards
β βββ guard-name.guard.ts
βββ pages
β βββ page-name
β βββ components
β β βββ page-specific-component
β β βββ ...
β βββ page-name.page.html
β βββ page-name.page.ts
βββ pipes
β βββ pipe-name.pipe.spec.ts
β βββ pipe-name.pipe.ts
βββ services
βββ service-name.service.spec.ts
βββ service-name.service.ts
- No new top-level (ie. direct descendants of
app
) folders should be added. - Page-specific folders should be inside
app/pages
- The
.model.ts
files should be the only ones containing the representation of entities that come from the backend - A page-specific folder (such as
app/pages/payment
) or a top-level component folder (such asapp/components/data-table
) can replicate app's top-level folder structure. For example, we could have acomponents
andservices
folder withinapp/pages/payments
.
When creating a new component/enum/directive, where do I put it?
By default, create it close to where it would be used. (ie. create a component that is specific to the payments
page within the payments
page folder).
Only move it to the equivalent top-level folder when
- It will inevitably be used by more than one domain (eg. a design atom like
Button
) - It becomes used by more than one page/domain
In other words, do not abstract by default.
Naming Consistency Between Frontend and Backend
To reduce confusion and avoid costly refactors, frontend naming should closely match backend naming, unless there is a very good reason not to. If a name differs in design vs. code, raise it early during refinement or development. A conscious decision should then be made to rename it in both the frontend and backend, based on the estimated effort. Consider third-party API integrations as part of this effort estimation.
Components
Creating components
Items with the π€ emoji happen automatically when using ng generate component
.
- Delete auto-generated spec files unless they are meaningful
- π€ All components must be standalone
- π€ All components must use the "OnPush" change detection strategy, to support zoneless change detection
- π€ Do not create a (S)CSS file per component
- Pages should not have helper files.
General component guidelines
- Keep custom components/CSS to a minimum.
- Use PrimeNG components whenever possible
- A design might contain something that, with a few tweaks, could use a PrimeNG component instead of a custom one. Always prefer to push for and suggest those tweaks.
- Use PrimeNG components whenever possible
- Use the new syntax for control flow rather than structural directives
- ie. prefer
@if
and@for
overNgIf
andNgFor
- ie. prefer
- Do not abstract by default
- ie. only pull out a component into a separate file when you are certain it will be re-used, or when you are trying to re-use it in a different component
- Inline templates (rather than a separate HTML file) can be used, but they have a maximum length enforced by eslint. In other words, use them only for "small" components.
Splitting components: when and why
Use child components to improve readability, reusability, testability, and performance. Do not abstract by default; prefer incremental extraction when the need is clear.
It is difficult to give clear guidelines that will always make this an easy decision. Below is an attempt to do so, but the general guideline is to avoid splitting up large components by default, and to do so only when there is a good reason.
Recommended reasons to split
-
Reuse Within the Same Page
When the same UI block appears multiple times within a page with minor variations, extract it into a child component with parameters.
-
Potential Cross-Page Reuse
When a UI element (status pill, filters panel, data table toolbar) is likely to be reused in other pages. Keep it close to initial usage, then move to app/components when actually reused elsewhere.
-
Template/Logic Size and Complexity
When the template exceeds approximately 150 lines, contains deep nesting, or the component has many UI-only conditional branches. Split along visual or functional sections.
-
Signals and Effects Complexity
When too many signals or effects become tightly coupled in a single file. Split to isolate stateful parts from presentational parts.
Reasons to avoid splitting
-
One-time use, small UI
Short, simple, page-specific markup with minimal logic. Keep inline in the page component.
-
Premature abstraction
Only split when either reuse or complexity is evident. Do not introduce indirection βjust in caseβ.
-
Leaky interfaces
If a child needs multiple services from the parent or many tightly coupled inputs/outputs to function, leave it in the parent component rather than trying to abstract at all costs.
Decision checklist
- Is there more than one high-level concern? If yes, split by concern, if doing so doesn't introduce architectural complexity.
- Is the template over ~150 lines or deeply nested? If yes, split into visual sections.
- Is a UI pattern used 2+ times? If yes, extract a child component.
- Will this block likely be reused on another page soon? If yes, extract but keep it close to current page first.
Using signals
We use different kinds of signals to add interactivity to our components. Below is an overview of what you will typically find, and when we tend to use each kind of signal.
Before reading this section, it is recommended that you read the angular guide on signals.
signal
A signal in it's most basic form. This is used when a component needs to store information that, when updated, should trigger a change detection elsewhere in the component.
input
This is used to pass information from parent to child component. The child component can only read the value, and never set it.
model
This is used when we want to pass information from parent to child component, but we also want to give the child component the ability to update the value.
computed
This is used when we want to create a signal that is the result of a calculation on one or more signals. It is a special read-only signal, that will automatically update it's value whenever one of the signals it relies on is updated.
effect
This is used to trigger "side effects" (eg. event tracking, or adding query parameters to the URL). Similarly to computed
, it will fire whenever one of the signals it relies on is updated.
viewChild
/ viewChildren
This is used to access a child of the component defined in the template of the component itself. It is defines in the component via a hashtag symbol.
contentChild
/ contentChildren
This is used to access a child of the component defined by the parent component.
injectQuery
This is used to get data from an API endpoint (typically the 121-service). This function returns an object containing several signals, such as data()
, isPending()
, failureReason()
etc.
Styling
- Use Tailwind utility classes instead of (S)CSS-files-per-component
- Follow the Tailwind recommendations for reusing styles
- Keep (any) custom CSS (even with Tailwind) to a minimum
- Make sure to use "
*-start
/*-end
" instead of "*-left
/*-right
" in positioning/margin properties/classes- This increases support for RTL in the future
- eg.
ms-0 ps-0
instead ofml-0 pl-0
Theming
Theming is done in multiple places:
tailwind.config.ts
This is our first point of entry, and the source of truth for design tokens (eg. colors). All other files import from this and use this as a reference.
app.theme.ts
Thie file defines the theme used by primeng. This imports tokens from tailwind.config.ts
, and uses them to customise primeng components and general theming.
styles.css
This file does the remaining general styling. We do a few things here:
- Define some tailwind helpers that cannot be defines in tailwind v4 (mostly typography helpers such as
txt-system-s
) - Define some sensible defaults for selectors such as
h1
,p
,a
, etc. - Override some primeng styles that cannot be tweaked via the
app.theme.ts
file
Changes and additions to this css file should be kept to a minimum, because more difficult to maintain (especially with major primeng / tailwind upgrades) and preferably applied via the two other files wherever possible.
Interacting with the 121-service
API Service Files
All API calls to the 121-service should go through an .api.service.ts
file. These files typically export three kinds of functions:
- Functions that return
queryOptions
- These are to be used in conjuction with
injectQuery
in components, toGET
data - These are typically prefixed with the verb
get
- These are to be used in conjuction with
- Functions that return a direct call to
httpWrapperService.perform121ServiceRequest
- These are to be used in conjuction with
injectMutation
in components, to performPOST
/PATCH
/PUT
/DELETE
operations - There are typically prefixed with the verb
create
/edit
/delete
etc.
- These are to be used in conjuction with
- An
invalidateCache
function- This is typically used in the
success
callback of mutations, to invalidate cache after performing an action in the backend
- This is typically used in the
Using types exported from the 121-service
Enums can be imported and used directly throughout the codebase.
DTOs should be used in conjunction with the Dto
TS wrapper type, and should only be imported in the domains
folders. The model.ts
files in these folders should then expose the FE-specific types to the rest of the application.
Localising Backend Enums
Whenever we need a "localisation" mapping for backend enums, we create this in a domain helper file, using CAPITAL_CASING to indicate that this is a constant.
For examnple:
export const TRANSACTION_STATUS_LABELS: Record<TransactionStatusEnum, string> =
{
[TransactionStatusEnum.waiting]: $localize`:@@transaction-status-waiting:Pending`,
[TransactionStatusEnum.error]: $localize`:@@transaction-status-error:Failed`,
[TransactionStatusEnum.success]: $localize`:@@transaction-status-success:Successful`,
};