Code Standards
Coding Style
For the most part, we follow the official angular coding style guide
Git
Docs for our git workflow are in clickup
Unit Tests
Coverage expected in unit tests
Rights / Permissions
- Test any permissions checks that your component performs, assert the appropriate state of your component for lack of permissions.
- This includes popovers / tooltips are present if called out in the Acceptance Criteria (you do not have to interact with them and assert)
API Errors / Error Handling
- Test API communication failure if applicable. Intercept api calls and force error scenarios, asserting your component handles them gracefully.
- Ensure your component is still functional when an error is returned, and messaging is appropriate.
- Use Angular's
HTTPTestingControllerintercepting / mocking http calls. Doc here
Forms
- Test form submission by calling your submit function with mocked values, don't try to interact with the form or click submit.
- Ensure fields listed as required in the acceptance criteria are marked as required in your form.
- Use the form element's css states checks to assert validation is correct. List of css states here
General
-
Unit test any functions / methods that change component state to ensure the function's output is correct for the given inputs. Refactor if necessary to get as close to pure functions as possible.
-
Do not test 3rd party code, this includes functionality provided by AgGrid(sorting / filtering of columns), the Bluewater component library, etc.
E2E Tests
Docs for writing E2E tests are in clickup
Feature Flags
Needs documentation
Injection Tokens
For managing state, prefer using lightweight injection tokens over services. You can find more info on what they are and how to use them here
Dates
Needs documentation
Http Calls and SignalR Hubs
NOTE (0.30.0): These docs are out of date. We use auto generated http clients now.
Fetching Data
Use the httpResource API introduced in angular 19.2.
export class PlaygroundComponent {
search = signal('')
products = httpResource<{ products: any[] }>(() => {
const search = this.search()
if (!search) return 'https://dummyjson.com/products'
return `https://dummyjson.com/products/search?q=${search}`
})
}
If you need to fetch data using something other than a GET request, you can return a http configuration object instead of the url.
productsViaPost = httpResource<{ products: any[] }>(() => {
const search = this.search()
return {
url: 'https://dummyjson.com/products/search',
method: 'POST',
body: { q: search || null }
}
})
Mutating Data
Directly inject and use the HttpClient. Rarely do we reuse mutation endpoints, so there's no benefit to trying to consolidate them. (ie the only place we use the AddBackflowDevice endpoint is in the AddBackflowDeviceComponent)
export class PlaygroundComponent {
http = inject(HttpClient)
addProduct(title: string) {
this.http.post<{id: number, title: string}>('https://dummyjson.com/products/add', { title }).subscribe(res => {
alert(`Product ${res.title} added`)
})
}
}
Interceptors
In the http configuration object, you can pass in a HttpContext object that the interceptors utilize to know if they should run or not.
export const IS_CACHE_ENABLED = new HttpContextToken<boolean>(() => false);
...
productsViaPost = httpResource<{ products: any[] }>(() => {
const search = this.search()
return {
url: 'https://dummyjson.com/products/search',
method: 'POST',
body: { q: search },
//enable the caching interceptor
context: new HttpContext().set(IS_CACHE_ENABLED, true)
}
})
SignalR Hubs
Use the HubSubject class to manage signalr connections. It extends the rxjs subject to keep things familiar. It keeps track of the hub connections globally and reuses them.
export class GeneralTabComponent {
private ucUrl = inject(API_URLS).utilityConfigurationApiUrl
private hub$ = new HubSubject(`${this.ucUrl}/Users/Patch`)
...
public bufferedQueue$ = createBufferedQueue({
subject: this.patches$,
concurrency: signal(0),
statuses: this.statuses,
patchCallback: patches => this.hub$.invoke<PatchResponse>('SendPatchV2', this.routerData().userId, patches),
});
...
ngOnInit() {
this.hub$.subscribe()
}
ngOnDestroy() {
this.hub$.complete()
}
}
//Pass in the messages you wish to listen to on the backend
const notifications$ = new HubSubject(`${this.umUrl}/Hub`, 'ReportNotification', 'ProcessNotification')
//Send a message to the backend
const query = {
accounts: null,
groupBy: 'Account number'
}
notifications$.next('GetDepositReport', query, JSON.stringify(query))
//Will emit for any of the messages you passed in constructor
notifications$.subscribe(res => {
if (res.method == 'ReportNotification' || res.method == 'ProcessNotification') {
//show toast
}
})
//You can skip the HubSubject if you just need to do a single method invoke on the server
import { fromHub } from 'data'
...
public submitParamForm() {
//fromHub will connect to the hub and close it for you
fromHub(`${this.umUrl}`, 'GetDepositReport', this.form, JSON.stringify(this.form)).subscribe(() => {
//show toast
})
}