Development of core services and utilities in Angular 18
Core is a set of functions, configs and utilities that are used in the application.
Usually, the core is what you drag from project to project. In this case, I tried to take only what was necessary.
Briefly about everything core:
- api/params.ts - utility for setting parameters. For example, in query params there is baggage=true, but true needs to be not a string, but a boolean;
- currency/currency.ts — currency config;
- env/environment.ts - getting and using .env;
- form/form.ts - a little typing of FormGroup;
- form/extract-changes.directive.ts - change detection directive in FormControl;
- hammer/hammer.ts - hammerjs settings;
- interceptors/content-type.interceptor.ts - determining the Content-Type for HTTP requests;
- metrics - service for sending events to google analytics and yandex metrics;
- navigation/mavigation.ts - implementation of paths in the application;
- styles/extra-class.service.ts - my bike for hanging classes;
- types/type.ts - custom types;
- utils - utilities for working with dates and deadlines.
Only navigation deserves special attention.
Managing app navigation
Let's take a closer look at the implementation of navigation controls.
Suppose we have routes for the entire application:
export const PATHS = { home: '', homeAvia: '', homeHotels: 'hotels', homeTours: 'tours', homeRailways: 'railways', rules: 'rules', terms: 'terms', documents: 'documents', faq: 'faq', cards: 'cards', login: 'login', registration: 'registration', notFound: 'not-found', serverError: 'server-error', permissionDenied: 'permission-denied', search: 'search', searchAvia: 'search/avia', searchHotel: 'search/hotels', searchTour: 'search/tours', searchRailway: 'search/railways', } as const;
PathValues - a type that will represent a set of strings.
export type PathValues = (typeof PATHS)[keyof typeof PATHS]; type Filter<T extends string> = T extends `:${infer Param}` ? Param : never; type Split<Value extends string> = Value extends `${infer LValue}/${infer RValue}` ? Filter<LValue> | Split<RValue> : Filter<Value>; export type GetPathParams<T extends string> = { [key in Split<T>]: string | number; }; export interface NavigationLink<T extends PathValues = PathValues> { readonly label: string; readonly route: T; readonly params?: GetPathParams<T>; readonly suffix?: string; }
NavigationLink - link array interface.
getRoute splits the string with parameters and replaces the found keys with the passed values:
export function getRoute<T extends PathValues>(path: T, params: Record<string, string | number> = {}): (string | number)[] { const segments = path.split('/').filter((value) => value?.length); const routeWithParams: (string | number)[] = ['/']; for (const segment of segments) { if (segment.charAt(0) === ':') { const paramName = segment.slice(1); if (params && params[paramName]) { routeWithParams.push(params[paramName]); } else { routeWithParams.push(paramName); } } else { routeWithParams.push(segment); } } return routeWithParams; }
Latest functions for “shaping” paths in application routes:
export function getChildPath(path: PathValues, parent: PathValues): string { return path.substring(parent.length + 1); } export function childNavigation(route: NavigationChild, parent: PathValues): Route { const redirectTo = route.redirectTo ? `/${route.redirectTo}` : undefined; if (!route.path.length || route.path.length < parent.length + 1) { return { ...route, redirectTo }; } return { ...route, redirectTo, path: getChildPath(route.path, parent), }; } export function withChildNavigation(parent: PathValues): (route: NavigationChild) => Route { return (route: NavigationChild) => childNavigation(route, parent); }
Usage example. In app.routes:
export const routes: Routes = [ { path: '', loadComponent: () => import('@baf/ui/layout').then((m) => m.LayoutComponent), children: [ { path: PATHS.search, loadChildren: () => import('./routes/search.routes').then((m) => m.searchRoutes), }, ], }, ];
In routes/search.routes.ts:
import { Routes } from '@angular/router'; import { PATHS, withChildNavigation } from '@baf/core'; export const searchRoutes: Routes = [ { path: PATHS.searchAvia, title: $localize`:Search Page:Search for cheap flights`, loadComponent: () => import('@baf/search/page').then((m) => m.SearchPageComponent), }, ].map(withChildNavigation(PATHS.search));
Analytics
Service for sending events to google analytics and yandex metrika - metrics:
import { InjectionToken } from '@angular/core'; export interface MetricConfig { readonly ids?: string[]; readonly counter?: number; readonly domains: string[]; readonly paths: string[]; } export const METRIC_CONFIG = new InjectionToken<MetricConfig>('MetricConfig');
MetricConfig - configuration of Google and Yandex metrics:
- ids - Google IDS list;
- counter - yandex metrika counter;
- domains - list of domains for combining analytics sessions;
- paths - a set of paths when referrers should be reset.
General service for sending events:
import { inject, Injectable } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NavigationEnd, Router } from '@angular/router'; import { filter } from 'rxjs'; import { tap } from 'rxjs/operators'; import { GoogleAnalytics } from './google-analytics'; import { YandexMetrika } from './yandex.metrika'; export interface MetricOptions { readonly [key: string]: unknown; readonly ym?: Record<string, unknown>; readonly ga?: Record<string, unknown>; } export function extractOptions(options?: MetricOptions): { readonly ym?: Record<string, unknown>; readonly ga?: Record<string, unknown> } { let { ym, ga } = options ?? {}; if (options) { if (!ym && !ga) { ym = options; ga = options; } } return { ym, ga }; } @Injectable() export class MetricService { private readonly router = inject(Router); private readonly yandexMetrika = inject(YandexMetrika); private readonly googleAnalytics = inject(GoogleAnalytics); constructor() { this.router.events .pipe( filter((event): event is NavigationEnd => event instanceof NavigationEnd), tap((event) => this.navigation(event.urlAfterRedirects)), takeUntilDestroyed(), ) .subscribe(); } init(customerId?: string | number) { this.set({ ga: customerId ? { user_id: customerId } : undefined, ym: customerId ? { UserID: customerId } : undefined, }); } navigation(url: string): void { this.googleAnalytics.sendNavigation(url); this.yandexMetrika.hit(url); } send(action: string, options?: MetricOptions): void { const params = extractOptions(options); this.googleAnalytics.sendEvent(action, params.ga); this.yandexMetrika.reachGoal(action, params.ym); } set(options: MetricOptions): void { const params = extractOptions(options); if (params.ga) { this.googleAnalytics.set(params.ga); } if (params.ym) { this.yandexMetrika.set(params.ym); } } }
Service methods:
- init - initialization of counters;
- navigation - sending a page transition event;
- send - registration event;
- set - passing parameters to analytics.
Implementation of the Google Analytics service:
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { METRIC_CONFIG } from './metrica.interface'; declare global { interface Window { readonly gtag?: (...params: unknown[]) => void; } } @Injectable() export class GoogleAnalytics { private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); private readonly document = inject(DOCUMENT); private readonly config = inject(METRIC_CONFIG); private readonly title = inject(Title); readonly gtag: (...params: unknown[]) => void; constructor() { if (this.isBrowser && typeof this.document.defaultView?.gtag !== 'undefined' && this.config.ids && this.config.ids?.length > 0) { this.gtag = this.document.defaultView.gtag; } else { this.gtag = () => {}; } } set(payload: Record<string, unknown>): void { this.gtag('set', payload); } sendEvent(action: string, payload?: Record<string, unknown>): void { this.gtag('event', action, { ...payload, event_category: payload?.['eventCategory'], event_label: payload?.['eventLabel'], value: payload?.['eventValue'], }); } sendNavigation(url: string): void { if ( (this.isBrowser && !this.config.domains.every((domain) => this.document.referrer.indexOf(domain) < 0)) || !this.config.paths.every((path) => this.document.location.pathname.indexOf(path) < 0) ) { this.set({ page_referrer: this.document.defaultView?.location.origin ?? '' }); } if (this.config.ids) { for (const key of this.config.ids) { this.gtag('config', key, { page_title: this.title.getTitle(), page_path: url, }); } } } }
Yandex metrics:
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { METRIC_CONFIG } from './metrica.interface'; declare global { interface Window { readonly ym?: (...params: unknown[]) => void; } } @Injectable() export class YandexMetrika { private readonly document = inject(DOCUMENT); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); private readonly config = inject(METRIC_CONFIG); private readonly counter: (...params: unknown[]) => void; constructor() { if (this.isBrowser && typeof this.document.defaultView?.ym !== 'undefined' && !!this.config.counter) { this.counter = this.document.defaultView.ym; } else { this.counter = () => {}; } } hit(url: string, options?: Record<string, unknown>): void { let clearReferrer = false; if ( (this.isBrowser && !this.config.domains.every((domain) => this.document.referrer.indexOf(domain) < 0)) || !this.config.paths.every((path) => this.document.location.pathname.indexOf(path) < 0) ) { clearReferrer = true; } const optionsAll: { referer?: string } = { ...options }; if (clearReferrer) { optionsAll.referer = ''; } this.counter(this.config.counter, 'hit', url, optionsAll); } reachGoal(target: string, options?: Record<string, unknown>): void { this.counter(this.config.counter, 'reachGoal', target, options); } set(params: Record<string, unknown>, options?: Record<string, unknown>): void { this.counter(this.config.counter, 'userParams', params, options); } }
Core
Utility for casting request parameters - api/params.ts:
export type HttpParams = Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>; export function castParams(all: Record<string, unknown>): HttpParams { const params: HttpParams = {}; for (const [key, value] of Object.entries(all)) { if (Array.isArray(value) && value.length > 0) { params[key] = value.filter((val) => { return typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number'; }); } else if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') { params[key] = value; } } return params; }
Setting currency - currency/currency.ts:
import { DEFAULT_CURRENCY_CODE, Provider } from '@angular/core'; export function provideCurrency(currencyCode: string): Provider { return { provide: DEFAULT_CURRENCY_CODE, useValue: currencyCode, }; }
Getting and using environment variables - env/environment.ts:
import type { ApplicationConfig} from '@angular/core'; import { APP_INITIALIZER, makeStateKey, TransferState } from '@angular/core'; export const ENV_KEY = makeStateKey<Environment>('Environment'); export interface Environment { readonly aviasalesToken: string; readonly hotellookToken: string; } export const ENV_DEFAULT: Environment = { aviasalesToken: '', hotellookToken: '', }; export function provideEnv() { return [ { provide: APP_INITIALIZER, useFactory: (transferState: TransferState) => { return () => { transferState.set<Environment>(ENV_KEY, { aviasalesToken: process.env['AVIASALES_TOKEN'] ?? ENV_DEFAULT.aviasalesToken, hotellookToken: process.env['HOTELLOOK_TOKEN'] ?? ENV_DEFAULT.hotellookToken, }); }; }, deps: [TransferState], multi: true, }, ]; } export const envConfig: ApplicationConfig = { providers: [provideEnv()], };
Form typing - form/form.ts:
import type { FormControl, FormGroup } from '@angular/forms'; export type FormFor<T> = { [P in keyof T]: FormControl<T[P]>; }; export type FormWithSubFor<T> = { [P in keyof T]: T[P] extends Record<string, unknown> ? FormGroup<FormFor<T[P]>> : FormControl<T[P]>; }; export function castQueryParams(queryParams: Record<string, unknown>, props?: string[]): Record<string, unknown> { const mapped: Record<string, unknown> = {}; const keys = props ?? Object.keys(queryParams); for (const key of keys) { const value = queryParams[key]; if (typeof value === 'string' && value.length > 0) { if (['true', 'false'].includes(value)) { mapped[key] = value === 'true'; } else if (!isNaN(Number(value))) { mapped[key] = Number(value); } else { mapped[key] = value; } } else if (typeof value === 'boolean') { mapped[key] = value; } else if (typeof value === 'number' && value > 0) { mapped[key] = value; } } return mapped; }
FormControl change detection directive:
import type { FormControl, FormGroup } from '@angular/forms'; export type FormFor<T> = { [P in keyof T]: FormControl<T[P]>; }; export type FormWithSubFor<T> = { [P in keyof T]: T[P] extends Record<string, unknown> ? FormGroup<FormFor<T[P]>> : FormControl<T[P]>; }; export function castQueryParams(queryParams: Record<string, unknown>, props?: string[]): Record<string, unknown> { const mapped: Record<string, unknown> = {}; const keys = props ?? Object.keys(queryParams); for (const key of keys) { const value = queryParams[key]; if (typeof value === 'string' && value.length > 0) { if (['true', 'false'].includes(value)) { mapped[key] = value === 'true'; } else if (!isNaN(Number(value))) { mapped[key] = Number(value); } else { mapped[key] = value; } } else if (typeof value === 'boolean') { mapped[key] = value; } else if (typeof value === 'number' && value > 0) { mapped[key] = value; } } return mapped; }
hammerjs setup:
import type { EnvironmentProviders, Provider } from '@angular/core'; import { importProvidersFrom, Injectable } from '@angular/core'; import { HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerModule } from '@angular/platform-browser'; @Injectable() export class HammerConfig extends HammerGestureConfig { override overrides = { swipe: { velocity: 0.4, threshold: 20 }, pinch: { enable: false }, rotate: { enable: false }, }; } export function provideHammer(): (Provider | EnvironmentProviders)[] { return [ importProvidersFrom(HammerModule), { provide: HAMMER_GESTURE_CONFIG, useClass: HammerConfig, }, ]; }
Setting Content-Type for HTTP requests -interceptors/content-type.interceptor.ts:
import type { HttpInterceptorFn } from '@angular/common/http'; export const contentTypeInterceptor: HttpInterceptorFn = (req, next) => { if (!req.headers.has('Content-Type') && req.headers.get('enctype') !== 'multipart/form-data') { req = req.clone({ headers: req.headers.set('Content-Type', 'application/json') }); } return next(req); };
Service for adding classes to components and directives - styles/extra-class.service.ts:
import { DestroyRef, ElementRef, inject, Injectable, Renderer2 } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import type { Observable, Subscription } from 'rxjs'; import { tap } from 'rxjs'; type Styles = string | string[] | undefined | null; export function toClass(value: unknown | undefined | null, prefix = 'is'): Styles { return value ? `${prefix}-${value}` : undefined; } @Injectable() export class ExtraClassService { private readonly destroyRef = inject(DestroyRef); private readonly render = inject(Renderer2); private readonly elementRef: ElementRef<HTMLElement> = inject(ElementRef); private styles: Record<string, Styles> = {}; private subscriptions: Record<string, Subscription> = {}; update(key: string, styles: Styles): void { if (this.styles[key]) { const lastStyles = this.styles[key]; if (Array.isArray(lastStyles)) { lastStyles.map((style) => this.render.removeClass(this.elementRef.nativeElement, style)); } else if (lastStyles) { this.render.removeClass(this.elementRef.nativeElement, lastStyles); } } if (Array.isArray(styles)) { styles.map((style) => this.render.addClass(this.elementRef.nativeElement, style)); } else if (styles) { this.render.addClass(this.elementRef.nativeElement, styles); } this.styles[key] = styles; } patch(style: string, active: boolean): void { if (active) { this.render.addClass(this.elementRef.nativeElement, style); } else { this.render.removeClass(this.elementRef.nativeElement, style); } } register(key: string, observable: Observable<unknown>, callback: () => void, start = true): void { if (this.subscriptions[key]) { this.unregister(key); } if (start) { callback(); } this.subscriptions[key] = observable .pipe( tap(() => callback()), takeUntilDestroyed(this.destroyRef), ) .subscribe(); } unregister(key: string): void { this.subscriptions[key].unsubscribe(); } }
Custom types - types/type.ts:
export type ChangeFn = (value: any) => void; export type TouchedFn = () => void; export type DisplayFn = (value: any, index?: number) => string; export type MaskFn = (value: any) => string; export type StyleFn = (value?: any) => string | string[]; export type CoerceBoolean = boolean | string | undefined | null;
Utilities for working with dates and deadlines - utils:
export function getDaysBetweenDates(startDate: Date | string, endDate: Date | string): number { return Math.round((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000); }
Короткая реализация для uuid:
export function uuidv4(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); }
И функция для гуманизации:
export function camelCaseToHumanize(str: string): string { return str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); }
В следующей статье будем разрабатывать свой UI KIT.
Ссылки
Все исходники находятся на github, в репозитории - github.com/Fafnur/buy-and-fly
Демо можно посмотреть здесь - buy-and-fly.fafn.ru/
Мои группы: telegram, medium, vk, x.com, linkedin, site
The above is the detailed content of Development of core services and utilities in Angular 18. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics





Frequently Asked Questions and Solutions for Front-end Thermal Paper Ticket Printing In Front-end Development, Ticket Printing is a common requirement. However, many developers are implementing...

There is no absolute salary for Python and JavaScript developers, depending on skills and industry needs. 1. Python may be paid more in data science and machine learning. 2. JavaScript has great demand in front-end and full-stack development, and its salary is also considerable. 3. Influencing factors include experience, geographical location, company size and specific skills.

JavaScript is the cornerstone of modern web development, and its main functions include event-driven programming, dynamic content generation and asynchronous programming. 1) Event-driven programming allows web pages to change dynamically according to user operations. 2) Dynamic content generation allows page content to be adjusted according to conditions. 3) Asynchronous programming ensures that the user interface is not blocked. JavaScript is widely used in web interaction, single-page application and server-side development, greatly improving the flexibility of user experience and cross-platform development.

How to merge array elements with the same ID into one object in JavaScript? When processing data, we often encounter the need to have the same ID...

Discussion on the realization of parallax scrolling and element animation effects in this article will explore how to achieve similar to Shiseido official website (https://www.shiseido.co.jp/sb/wonderland/)...

Learning JavaScript is not difficult, but it is challenging. 1) Understand basic concepts such as variables, data types, functions, etc. 2) Master asynchronous programming and implement it through event loops. 3) Use DOM operations and Promise to handle asynchronous requests. 4) Avoid common mistakes and use debugging techniques. 5) Optimize performance and follow best practices.

In-depth discussion of the root causes of the difference in console.log output. This article will analyze the differences in the output results of console.log function in a piece of code and explain the reasons behind it. �...

Explore the implementation of panel drag and drop adjustment function similar to VSCode in the front-end. In front-end development, how to implement VSCode similar to VSCode...
