mirror of https://github.com/abpframework/abp.git
66 changed files with 1965 additions and 135 deletions
@ -0,0 +1,101 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Configure ABP IdentityModel clients for server-to-server access tokens, tenant-aware client selection, and request customization." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# IdentityModel Clients |
||||
|
|
||||
|
The `Volo.Abp.IdentityModel` package obtains access tokens for server-to-server HTTP calls. `AbpIdentityModelModule` binds the `IdentityClients` configuration section to `AbpIdentityClientOptions`. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
Install the `Volo.Abp.IdentityModel` NuGet package in the project that obtains the tokens: |
||||
|
|
||||
|
````shell |
||||
|
abp add-package Volo.Abp.IdentityModel |
||||
|
```` |
||||
|
|
||||
|
The command adds the package and the `AbpIdentityModelModule` dependency to the module class. |
||||
|
|
||||
|
## Configure Identity Clients |
||||
|
|
||||
|
Define a `Default` client and any named clients in the application configuration: |
||||
|
|
||||
|
````json |
||||
|
{ |
||||
|
"IdentityClients": { |
||||
|
"Default": { |
||||
|
"GrantType": "client_credentials", |
||||
|
"ClientId": "MyProject_Backend", |
||||
|
"ClientSecret": "your-client-secret", |
||||
|
"Authority": "https://localhost:44301/", |
||||
|
"Scope": "MyProject" |
||||
|
}, |
||||
|
"Reporting": { |
||||
|
"GrantType": "client_credentials", |
||||
|
"ClientId": "MyProject_Reporting", |
||||
|
"ClientSecret": "your-reporting-client-secret", |
||||
|
"Authority": "https://localhost:44301/", |
||||
|
"Scope": "Reporting" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
When a client name is requested for the current tenant, ABP selects the first available configuration in this order: |
||||
|
|
||||
|
1. `<client-name>.<tenant-id>` |
||||
|
2. `<client-name>.<tenant-name>` |
||||
|
3. `<client-name>` |
||||
|
4. `Default` |
||||
|
|
||||
|
If no client name is supplied, ABP uses `Default` as the client name. Tenant-specific entries let a tenant use different credentials without changing the consuming service. For example, `Reporting.8e6fcd0a-75ab-4d94-90f4-9a2503d0e70c` overrides the `Reporting` client for that tenant ID. |
||||
|
|
||||
|
Pass the client name to `TryAuthenticateAsync` when you authenticate an `HttpClient` directly: |
||||
|
|
||||
|
````csharp |
||||
|
var client = _httpClientFactory.CreateClient(); |
||||
|
|
||||
|
if (!await _authenticationService.TryAuthenticateAsync(client, "Reporting")) |
||||
|
{ |
||||
|
throw new InvalidOperationException( |
||||
|
"The Reporting identity client is not configured." |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
var response = await client.GetAsync("https://reporting.example.com/api/reports"); |
||||
|
```` |
||||
|
|
||||
|
Install `Volo.Abp.Http.Client.IdentityModel` when dynamic HTTP client proxies should obtain tokens automatically. Its `AbpHttpClientIdentityModelModule` integration uses the remote service's `IdentityClient` value when configured, then the remote service name, and finally the `Default` identity client fallback described above: |
||||
|
|
||||
|
````json |
||||
|
{ |
||||
|
"RemoteServices": { |
||||
|
"Reporting": { |
||||
|
"BaseUrl": "https://reporting.example.com/", |
||||
|
"IdentityClient": "Reporting" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
## Customize Discovery and Token Requests |
||||
|
|
||||
|
Use `IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage` to add headers or otherwise customize the request messages: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<IdentityModelHttpRequestMessageOptions>(options => |
||||
|
{ |
||||
|
options.ConfigureHttpRequestMessage = request => |
||||
|
{ |
||||
|
request.Headers.TryAddWithoutValidation( |
||||
|
"X-Internal-Client", |
||||
|
"MyProject" |
||||
|
); |
||||
|
}; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
The callback runs for discovery, client-credentials, password and device-authorization request messages created by the default authentication service. |
||||
@ -0,0 +1,77 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Learn how to use ABP's in-memory database provider, register a MemoryDb context and repositories, and customize entity serialization." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# In-Memory Database Provider |
||||
|
|
||||
|
The `Volo.Abp.MemoryDb` package implements ABP repositories with an in-process database. It is useful for tests and other non-durable scenarios. Data is kept in the application process and is lost when the process stops. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
Install the `Volo.Abp.MemoryDb` NuGet package in the data-access project: |
||||
|
|
||||
|
````shell |
||||
|
abp add-package Volo.Abp.MemoryDb |
||||
|
```` |
||||
|
|
||||
|
The command adds the package and the `AbpMemoryDbModule` dependency to the module class. You can also configure the dependency manually as shown in the next section. |
||||
|
|
||||
|
## Configure the Module |
||||
|
|
||||
|
Add `AbpMemoryDbModule` as a dependency of your module: |
||||
|
|
||||
|
````csharp |
||||
|
[DependsOn(typeof(AbpMemoryDbModule))] |
||||
|
public class MyDataModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
## Create a MemoryDb Context |
||||
|
|
||||
|
Derive a class from `MemoryDbContext` and return the entity types managed by the context: |
||||
|
|
||||
|
````csharp |
||||
|
public class MyMemoryDbContext : MemoryDbContext |
||||
|
{ |
||||
|
private static readonly Type[] EntityTypes = |
||||
|
[ |
||||
|
typeof(Book), |
||||
|
typeof(Author) |
||||
|
]; |
||||
|
|
||||
|
public override IReadOnlyList<Type> GetEntityTypes() |
||||
|
{ |
||||
|
return EntityTypes; |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Register the context in the `ConfigureServices` method of your module: |
||||
|
|
||||
|
````csharp |
||||
|
context.Services.AddMemoryDbContext<MyMemoryDbContext>(options => |
||||
|
{ |
||||
|
options.AddDefaultRepositories(); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
`AddDefaultRepositories()` registers default repositories for the aggregate roots returned by the context. Pass `includeAllEntities: true` when default repositories are also needed for other entity types. |
||||
|
|
||||
|
MemoryDb repositories use ABP's unit-of-work-aware database provider. Repository operations require an active [unit of work](../../architecture/domain-driven-design/unit-of-work.md). |
||||
|
|
||||
|
## JSON Serialization |
||||
|
|
||||
|
MemoryDb stores serialized entity values. Configure `Utf8JsonMemoryDbSerializerOptions` to customize the underlying `System.Text.Json` options: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<Utf8JsonMemoryDbSerializerOptions>(options => |
||||
|
{ |
||||
|
options.JsonSerializerOptions.Converters.Add( |
||||
|
new MyEntityJsonConverter() |
||||
|
); |
||||
|
}); |
||||
|
```` |
||||
@ -0,0 +1,88 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Learn how to use ABP Commercial Angular date range controls, standalone UI configuration and the public testing entrypoint." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Commercial UI Components |
||||
|
|
||||
|
The `@volo/abp.commercial.ng.ui` package provides shared ABP Commercial Angular controls in addition to the separately documented [lookup components](./lookup-components.md) and [entity filters](./entity-filters.md). The package is included in ABP Commercial Angular application templates. |
||||
|
|
||||
|
## Date Range Controls |
||||
|
|
||||
|
`DateRangePickerComponent` and `DatetimeRangePickerComponent` are Angular form controls. `startDateProp` and `endDateProp` specify the two properties updated in the bound model: |
||||
|
|
||||
|
```ts |
||||
|
import { Component } from '@angular/core'; |
||||
|
import { FormsModule } from '@angular/forms'; |
||||
|
import { |
||||
|
DateRangePickerModule, |
||||
|
DatetimeRangePickerComponent, |
||||
|
} from '@volo/abp.commercial.ng.ui'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-report-range', |
||||
|
templateUrl: './report-range.component.html', |
||||
|
imports: [ |
||||
|
FormsModule, |
||||
|
DateRangePickerModule, |
||||
|
DatetimeRangePickerComponent, |
||||
|
], |
||||
|
}) |
||||
|
export class ReportRangeComponent { |
||||
|
dateRange: { |
||||
|
startDate: string | Date | null; |
||||
|
endDate: string | Date | null; |
||||
|
} = { |
||||
|
startDate: null, |
||||
|
endDate: null, |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
```html |
||||
|
<abp-date-range-picker |
||||
|
[(ngModel)]="dateRange" |
||||
|
startDateProp="startDate" |
||||
|
endDateProp="endDate" |
||||
|
labelText="Reports::DateRange" |
||||
|
/> |
||||
|
``` |
||||
|
|
||||
|
Use `abp-datetime-range-picker` with the same inputs when the model also needs start and end times. |
||||
|
|
||||
|
## Standalone Configuration |
||||
|
|
||||
|
Register commercial UI configuration in the application providers. The following example enables flag icons: |
||||
|
|
||||
|
```ts |
||||
|
import { ApplicationConfig } from '@angular/core'; |
||||
|
import { |
||||
|
provideCommercialUiConfig, |
||||
|
withEnableFlagIcon, |
||||
|
} from '@volo/abp.commercial.ng.ui/config'; |
||||
|
|
||||
|
export const appConfig: ApplicationConfig = { |
||||
|
providers: [ |
||||
|
provideCommercialUiConfig( |
||||
|
withEnableFlagIcon(true), |
||||
|
), |
||||
|
], |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
Calling `provideCommercialUiConfig()` also registers the shared profile-picture, impersonation and tenant-switching providers. Flag icons are disabled unless `withEnableFlagIcon(true)` is supplied. |
||||
|
|
||||
|
## Testing |
||||
|
|
||||
|
`CommercialUiTestingModule` imports and exports `BaseCommercialUiModule`, making its declarations available from the public testing entrypoint. Its `withConfig()` method returns the module registration without adding test doubles or providers: |
||||
|
|
||||
|
```ts |
||||
|
import { TestBed } from '@angular/core/testing'; |
||||
|
import { CommercialUiTestingModule } from '@volo/abp.commercial.ng.ui/testing'; |
||||
|
|
||||
|
await TestBed.configureTestingModule({ |
||||
|
imports: [CommercialUiTestingModule.withConfig()], |
||||
|
}).compileComponents(); |
||||
|
``` |
||||
@ -1,41 +1,117 @@ |
|||||
```json |
```json |
||||
//[doc-seo] |
//[doc-seo] |
||||
{ |
{ |
||||
"Description": "Learn how to easily format dates in Angular using DateTime format pipes for shortDate, shortTime, and shortDateTime with culture settings." |
"Description": "Format dates and handle clock-aware timezone conversion in ABP Angular applications with pipes, TimeService, and TimezoneService." |
||||
} |
} |
||||
``` |
``` |
||||
|
|
||||
{%{ |
{%{ |
||||
# DateTime Format Pipes |
# Date and Time |
||||
|
|
||||
You can format date by Date pipe of angular. |
ABP Angular provides culture-aware format pipes, clock-aware UTC conversion, timezone selection, and date-time services. These APIs use the culture, clock, and timezone values from the application configuration. |
||||
|
|
||||
|
## Culture-Aware Format Pipes |
||||
|
|
||||
|
Angular's built-in `DatePipe` can format a date directly: |
||||
|
|
||||
Example |
|
||||
|
|
||||
```html |
```html |
||||
<span>{{today | date 'dd/mm/yy'}}</span> |
<span>{{ today | date:'dd/MM/yy' }}</span> |
||||
``` |
``` |
||||
|
|
||||
ShortDate, ShortTime and ShortDateTime format data like angular's data pipe but easier. Also the pipes get format from config service by culture. |
The ABP pipes below use the short date and time patterns returned in the application localization configuration. |
||||
|
|
||||
## ShortDate Pipe |
### `shortDate` |
||||
|
|
||||
```html |
```html |
||||
<span> {{today | shortDate }}</span> |
<span>{{ today | shortDate }}</span> |
||||
``` |
``` |
||||
|
|
||||
|
### `shortTime` |
||||
|
|
||||
## ShortTime Pipe |
```html |
||||
|
<span>{{ today | shortTime }}</span> |
||||
|
``` |
||||
|
|
||||
|
### `shortDateTime` |
||||
|
|
||||
```html |
```html |
||||
<span> {{today | shortTime }}</span> |
<span>{{ today | shortDateTime }}</span> |
||||
``` |
``` |
||||
|
|
||||
|
These pipes extend Angular's `DatePipe`. They select the format pattern from `ConfigStateService`; they do not apply ABP's clock-aware timezone selection. Use `abpUtcToLocal` when the value also needs to follow the application's clock and timezone. |
||||
|
|
||||
|
## Clock-Aware UTC Conversion |
||||
|
|
||||
## ShortDateTime Pipe |
The `abpUtcToLocal` pipe accepts `date`, `time`, or `datetime` as its format type: |
||||
|
|
||||
```html |
```html |
||||
<span> {{today | shortDateTime }}</span> |
<span>{{ order.creationTime | abpUtcToLocal:'datetime' }}</span> |
||||
|
``` |
||||
|
|
||||
|
Its behavior depends on the clock configuration returned by the backend: |
||||
|
|
||||
|
- With the UTC clock enabled, it converts the input to `TimezoneService.timezone`, including the daylight-saving-time offset for that date. |
||||
|
- With a non-UTC clock, it formats the value without applying the configured timezone conversion. |
||||
|
- Empty or invalid input produces an empty string. |
||||
|
|
||||
|
The output pattern is still taken from the current application's short date and time formats. |
||||
|
|
||||
|
## `TimezoneService` |
||||
|
|
||||
|
Inject `TimezoneService` to read or persist the timezone used by the Angular application: |
||||
|
|
||||
|
```ts |
||||
|
import { TimezoneService } from '@abp/ng.core'; |
||||
|
import { Component, inject } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-timezone-selector', |
||||
|
template: `<button type="button" (click)="selectTimezone()">Use Istanbul time</button>`, |
||||
|
}) |
||||
|
export class TimezoneSelectorComponent { |
||||
|
private readonly timezoneService = inject(TimezoneService); |
||||
|
|
||||
|
selectTimezone(): void { |
||||
|
this.timezoneService.setTimezone('Europe/Istanbul'); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`timezone` returns: |
||||
|
|
||||
|
- the browser timezone when the backend clock is not UTC; |
||||
|
- the `Abp.Timing.TimeZone` setting when the clock is UTC and the setting has a value; |
||||
|
- the browser timezone as a fallback when the UTC setting is empty. |
||||
|
|
||||
|
`setTimezone` writes the selected IANA timezone to the `__timezone` cookie only when the UTC clock is enabled. |
||||
|
|
||||
|
When you configure the application with `provideAbpCore`, its built-in `timezoneInterceptor` adds the effective timezone to outgoing `HttpClient` requests as the `__timezone` header. It does not add the header when the UTC clock is disabled. |
||||
|
|
||||
|
## `TimeService` |
||||
|
|
||||
|
`TimeService` returns [Luxon](https://moment.github.io/luxon/#/) `DateTime` values and formats dates with the current Angular locale: |
||||
|
|
||||
|
```ts |
||||
|
import { TimeService } from '@abp/ng.core'; |
||||
|
import { inject, Injectable } from '@angular/core'; |
||||
|
|
||||
|
@Injectable({ providedIn: 'root' }) |
||||
|
export class ScheduleFormatter { |
||||
|
private readonly timeService = inject(TimeService); |
||||
|
|
||||
|
formatForIstanbul(value: string): string { |
||||
|
return this.timeService.format(value, 'ff', 'Europe/Istanbul'); |
||||
|
} |
||||
|
} |
||||
``` |
``` |
||||
|
|
||||
|
| Method | Behavior | |
||||
|
| --- | --- | |
||||
|
| `now(zone = 'local')` | Returns the current time in the requested IANA timezone. | |
||||
|
| `toZone(value, zone)` | Parses an ISO string or `Date` and returns a Luxon value in the requested timezone. | |
||||
|
| `format(value, format = 'ff', zone = 'local')` | Converts to the timezone, applies its DST rules, and formats with the current locale. | |
||||
|
| `formatDateWithStandardOffset(value, format = 'ff', zone?)` | Applies the zone's January 1 offset and formats without any further timezone or DST conversion. | |
||||
|
| `formatWithoutTimeZone(value, format = 'ff')` | Formats the parsed ISO clock fields without shifting them to another timezone. | |
||||
|
|
||||
}%} |
}%} |
||||
|
|||||
@ -0,0 +1,94 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Learn how to use the generic ABP Angular lookup search component with remote searches, two-way values and custom templates." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Lookup Search Component |
||||
|
|
||||
|
`LookupSearchComponent` is a generic standalone search control exported by `@abp/ng.components/lookup`. A search function returns observable lookup items, and the component manages debouncing, loading, selection and clearing. |
||||
|
|
||||
|
This component is part of the open-source `@abp/ng.components` package. It is different from the [commercial lookup component family](./lookup-components.md), which provides form-oriented typeahead, select and table controls. |
||||
|
|
||||
|
The package is included in the Angular application templates. Install it if the application does not already reference it: |
||||
|
|
||||
|
```bash |
||||
|
npm install @abp/ng.components |
||||
|
``` |
||||
|
|
||||
|
```ts |
||||
|
import { Component, inject, signal } from '@angular/core'; |
||||
|
import { |
||||
|
LookupItem, |
||||
|
LookupSearchComponent, |
||||
|
LookupSearchFn, |
||||
|
} from '@abp/ng.components/lookup'; |
||||
|
import { map } from 'rxjs'; |
||||
|
|
||||
|
interface BookLookupItem extends LookupItem { |
||||
|
authorName: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-book-lookup', |
||||
|
templateUrl: './book-lookup.component.html', |
||||
|
imports: [LookupSearchComponent], |
||||
|
}) |
||||
|
export class BookLookupComponent { |
||||
|
private readonly bookService = inject(BookService); |
||||
|
|
||||
|
readonly selectedBookId = signal(''); |
||||
|
readonly selectedBookName = signal(''); |
||||
|
|
||||
|
readonly searchBooks: LookupSearchFn<BookLookupItem> = filter => |
||||
|
this.bookService.search(filter).pipe( |
||||
|
map(books => |
||||
|
books.map(book => ({ |
||||
|
key: book.id, |
||||
|
displayName: book.name, |
||||
|
authorName: book.authorName, |
||||
|
})), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
onBookSelected(book: BookLookupItem) { |
||||
|
console.log(book.key); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
{%{ |
||||
|
```html |
||||
|
<abp-lookup-search |
||||
|
label="Books::Book" |
||||
|
placeholder="Books::SearchBooks" |
||||
|
[searchFn]="searchBooks" |
||||
|
[minSearchLength]="2" |
||||
|
[selectedValue]="selectedBookId()" |
||||
|
(selectedValueChange)="selectedBookId.set($event)" |
||||
|
[displayValue]="selectedBookName()" |
||||
|
(displayValueChange)="selectedBookName.set($event)" |
||||
|
(itemSelected)="onBookSelected($event)" |
||||
|
> |
||||
|
<ng-template #itemTemplate let-book> |
||||
|
<strong>{{ book.displayName }}</strong> |
||||
|
<small>{{ book.authorName }}</small> |
||||
|
</ng-template> |
||||
|
</abp-lookup-search> |
||||
|
``` |
||||
|
}%} |
||||
|
|
||||
|
Each lookup item uses `key` as its selected value and `displayName` as its displayed value by default. Set `valueKey` or `displayKey` to use another item property. |
||||
|
|
||||
|
Searches use a 300 millisecond debounce by default. Configure `debounceTime` and `minSearchLength` when the remote endpoint needs different behavior. `label` and `placeholder` values are passed through ABP localization. |
||||
|
|
||||
|
`selectedValue` and `displayValue` are model inputs and support two-way binding. The component also emits `searchChanged` for input changes and `itemSelected` after selection. |
||||
|
|
||||
|
Add an `#itemTemplate` template to customize each result, as in the previous example. Add a `#noResultsTemplate` template to replace the default no-results content: |
||||
|
|
||||
|
```html |
||||
|
<ng-template #noResultsTemplate> |
||||
|
<div class="list-group-item text-muted">No matching books</div> |
||||
|
</ng-template> |
||||
|
``` |
||||
@ -0,0 +1,96 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Configure localized Angular route titles, the application-name suffix, and a custom TitleStrategy in an ABP application." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Document Title Strategy |
||||
|
|
||||
|
`provideAbpCore` registers `AbpTitleStrategy` as Angular's default `TitleStrategy`. It reads the deepest active route title, localizes it, and updates the browser document title. |
||||
|
|
||||
|
## Setting a Route Title |
||||
|
|
||||
|
Use Angular's `title` route property. The value can be an ABP localization key: |
||||
|
|
||||
|
```ts |
||||
|
import { Routes } from '@angular/router'; |
||||
|
import { BooksComponent } from './books.component'; |
||||
|
|
||||
|
export const routes: Routes = [ |
||||
|
{ |
||||
|
path: 'books', |
||||
|
component: BooksComponent, |
||||
|
title: 'BookStore::Menu:Books', |
||||
|
}, |
||||
|
]; |
||||
|
``` |
||||
|
|
||||
|
With an application name of `Book Store`, the resulting title is: |
||||
|
|
||||
|
```text |
||||
|
Books | Book Store |
||||
|
``` |
||||
|
|
||||
|
The strategy resolves the application name from the `::AppName` localization key. If a route has no title, it uses only the application name. It also recalculates the active title when the language changes. |
||||
|
|
||||
|
## Removing the Application-Name Suffix |
||||
|
|
||||
|
Provide `DISABLE_PROJECT_NAME` with `true` to omit the application name from routes that have a title: |
||||
|
|
||||
|
```ts |
||||
|
import { DISABLE_PROJECT_NAME } from '@abp/ng.core'; |
||||
|
import { ApplicationConfig } from '@angular/core'; |
||||
|
|
||||
|
export const appConfig: ApplicationConfig = { |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: DISABLE_PROJECT_NAME, |
||||
|
useValue: true, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
The route above then produces `Books`. A route without a title still falls back to the application name. |
||||
|
|
||||
|
## Replacing the Strategy |
||||
|
|
||||
|
Create an Angular `TitleStrategy` and pass it to the `withTitleStrategy` feature when the application needs a different title convention: |
||||
|
|
||||
|
```ts |
||||
|
import { Title } from '@angular/platform-browser'; |
||||
|
import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; |
||||
|
import { inject, Injectable } from '@angular/core'; |
||||
|
|
||||
|
@Injectable({ providedIn: 'root' }) |
||||
|
export class CustomTitleStrategy extends TitleStrategy { |
||||
|
private readonly title = inject(Title); |
||||
|
|
||||
|
override updateTitle(routerState: RouterStateSnapshot): void { |
||||
|
const routeTitle = this.buildTitle(routerState); |
||||
|
this.title.setTitle(routeTitle ? `My Application - ${routeTitle}` : 'My Application'); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Register it together with the normal ABP Core options: |
||||
|
|
||||
|
```ts |
||||
|
import { provideAbpCore, withOptions, withTitleStrategy } from '@abp/ng.core'; |
||||
|
import { registerLocaleForEsBuild } from '@abp/ng.core/locale'; |
||||
|
import { ApplicationConfig } from '@angular/core'; |
||||
|
import { environment } from '../environments/environment'; |
||||
|
|
||||
|
export const appConfig: ApplicationConfig = { |
||||
|
providers: [ |
||||
|
provideAbpCore( |
||||
|
withOptions({ |
||||
|
environment, |
||||
|
registerLocaleFn: registerLocaleForEsBuild(), |
||||
|
}), |
||||
|
withTitleStrategy(CustomTitleStrategy), |
||||
|
), |
||||
|
], |
||||
|
}; |
||||
|
``` |
||||
@ -0,0 +1,138 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Learn how to use the ABP Angular tree component for hierarchical data, selection, templates and drag-and-drop." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Tree Component |
||||
|
|
||||
|
`TreeComponent` is a standalone tree control exported by `@abp/ng.components/tree`. It supports selection, checkboxes, expansion, context-menu templates and drag-and-drop. |
||||
|
|
||||
|
The `@abp/ng.components` package is included in the Angular application templates. Install it if the application does not already reference it: |
||||
|
|
||||
|
```bash |
||||
|
npm install @abp/ng.components |
||||
|
``` |
||||
|
|
||||
|
Import it into a standalone component and provide nodes in the format expected by the underlying NG-ZORRO tree: |
||||
|
|
||||
|
```ts |
||||
|
import { Component, signal } from '@angular/core'; |
||||
|
import { |
||||
|
DropEvent, |
||||
|
ExpandedIconTemplateDirective, |
||||
|
TreeComponent, |
||||
|
TreeNodeTemplateDirective, |
||||
|
} from '@abp/ng.components/tree'; |
||||
|
import { of } from 'rxjs'; |
||||
|
|
||||
|
interface Category { |
||||
|
id: string; |
||||
|
name: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-category-tree', |
||||
|
templateUrl: './category-tree.component.html', |
||||
|
imports: [ |
||||
|
TreeComponent, |
||||
|
TreeNodeTemplateDirective, |
||||
|
ExpandedIconTemplateDirective, |
||||
|
], |
||||
|
}) |
||||
|
export class CategoryTreeComponent { |
||||
|
readonly nodes = signal([ |
||||
|
{ |
||||
|
key: 'books', |
||||
|
title: 'Books', |
||||
|
entity: { id: 'books', name: 'Books' } as Category, |
||||
|
children: [], |
||||
|
isLeaf: true, |
||||
|
}, |
||||
|
]); |
||||
|
|
||||
|
readonly expandedKeys = signal<string[]>([]); |
||||
|
readonly selectedCategory = signal<Category | null>(null); |
||||
|
|
||||
|
readonly allowDrop = () => of(true); |
||||
|
|
||||
|
handleDrop(event: DropEvent) { |
||||
|
console.log(event.dragNode?.key); |
||||
|
} |
||||
|
|
||||
|
edit(key: string) { |
||||
|
console.log(key); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
```html |
||||
|
<abp-tree |
||||
|
[nodes]="nodes()" |
||||
|
[expandedKeys]="expandedKeys()" |
||||
|
[selectedNode]="selectedCategory()" |
||||
|
[draggable]="true" |
||||
|
[beforeDrop]="allowDrop" |
||||
|
(expandedKeysChange)="expandedKeys.set($event)" |
||||
|
(selectedNodeChange)="selectedCategory.set($event)" |
||||
|
(dropOver)="handleDrop($event)" |
||||
|
> |
||||
|
<ng-template #menu let-node> |
||||
|
<button type="button" class="dropdown-item" (click)="edit(node.key)"> |
||||
|
Edit |
||||
|
</button> |
||||
|
</ng-template> |
||||
|
</abp-tree> |
||||
|
``` |
||||
|
|
||||
|
The main inputs are: |
||||
|
|
||||
|
- `nodes`, `checkedKeys`, `expandedKeys` and `selectedNode` for tree state. |
||||
|
- `draggable`, `checkable`, `checkStrictly` and `noAnimation` for tree behavior. |
||||
|
- `changeCheckboxWithNode` to update checked keys when a node is selected. |
||||
|
- `isNodeSelected` to replace the default selected-node comparison. |
||||
|
- `beforeDrop` to approve or reject a drag-and-drop operation. |
||||
|
|
||||
|
State changes are exposed through `checkedKeysChange`, `expandedKeysChange`, `selectedNodeChange`, `dropOver` and `nzExpandChange`. |
||||
|
|
||||
|
The default `beforeDrop` handler rejects drops. Supply a handler that returns an observable accepted by the underlying tree control when drag-and-drop is enabled. |
||||
|
|
||||
|
## Templates |
||||
|
|
||||
|
Use the `#menu` template, as in the previous example, to add a context menu for each node. Use `abpTreeNodeTemplate` and `abpTreeExpandedIconTemplate` to replace the node and expanded-icon templates: |
||||
|
|
||||
|
{%{ |
||||
|
```html |
||||
|
<abp-tree [nodes]="nodes()"> |
||||
|
<ng-template abpTreeNodeTemplate let-node> |
||||
|
<strong>{{ node.title }}</strong> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template abpTreeExpandedIconTemplate let-node> |
||||
|
<span>{{ node.isExpanded ? '−' : '+' }}</span> |
||||
|
</ng-template> |
||||
|
</abp-tree> |
||||
|
``` |
||||
|
}%} |
||||
|
|
||||
|
The component example imports `TreeNodeTemplateDirective` and `ExpandedIconTemplateDirective` because Angular must see each directive used by a standalone component template. If you use only one of these templates, import only its corresponding directive. |
||||
|
|
||||
|
## Flat-List Adapter |
||||
|
|
||||
|
`TreeAdapter<T>` converts a flat list of `BaseNode` values into tree nodes. Each item requires an `id` and a nullable `parentId`; its `displayName` or `name` becomes the default node title. Use `getTree()`, `handleDrop()`, `handleRemove()` and `handleUpdate()` to keep the flat list and tree representations synchronized. |
||||
|
|
||||
|
## Style Loading |
||||
|
|
||||
|
The component loads `ng-zorro-antd-tree.css` when it is initialized. Provide `DISABLE_TREE_STYLE_LOADING_TOKEN` with `true` only when the application already includes that stylesheet: |
||||
|
|
||||
|
```ts |
||||
|
import { DISABLE_TREE_STYLE_LOADING_TOKEN } from '@abp/ng.components/tree'; |
||||
|
|
||||
|
export const providers = [ |
||||
|
{ |
||||
|
provide: DISABLE_TREE_STYLE_LOADING_TOKEN, |
||||
|
useValue: true, |
||||
|
}, |
||||
|
]; |
||||
|
``` |
||||
@ -0,0 +1,57 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Use ABP's MVC JavaScript Clock API for time-zone detection, date normalization, localized display, and the browser time-zone cookie." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# ASP.NET Core MVC / Razor Pages UI: JavaScript Clock API |
||||
|
|
||||
|
The `abp.clock` namespace provides date, time-zone and browser-time-zone helpers for MVC / Razor Pages applications. The application configuration script sets `abp.clock.kind` from the server-side ABP clock configuration. |
||||
|
|
||||
|
## Time-Zone Support |
||||
|
|
||||
|
`abp.clock.supportsMultipleTimezone()` returns `true` when the configured clock kind is `Utc`. `abp.clock.timeZone()` returns the value of the `Abp.Timing.TimeZone` setting when it is available and otherwise returns the browser's IANA time-zone name. |
||||
|
|
||||
|
````js |
||||
|
if (abp.clock.supportsMultipleTimezone()) { |
||||
|
console.log(abp.clock.timeZone()); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
## Normalize Date Values |
||||
|
|
||||
|
Use `normalizeToString` before sending a date value to the server and `normalizeToLocaleString` before displaying a server value: |
||||
|
|
||||
|
````js |
||||
|
const requestValue = abp.clock.normalizeToString(new Date()); |
||||
|
const displayValue = abp.clock.normalizeToLocaleString(requestValue); |
||||
|
```` |
||||
|
|
||||
|
`normalizeToString` returns an ISO-compatible date-time string. For a non-UTC clock, the core implementation uses `yyyy-MM-ddTHH:mm:ss`. Both normalization methods return empty or invalid values unchanged. |
||||
|
|
||||
|
The standard shared MVC theme bundle loads Luxon and replaces the core implementations of `normalizeToString` and `normalizeToLocaleString`. When multiple time zones are supported, this Luxon implementation interprets the input in the configured IANA time zone, converts it to UTC and returns an ISO value ending in `Z`. The output can include milliseconds (for example, `2026-07-17T08:30:00.000Z`), so do not require an exact string length when consuming it. |
||||
|
|
||||
|
If an application uses the core scripts without the shared theme's Luxon contributor, the fallback implementation produces a `Z`-suffixed transport value by detecting a numeric browser offset. It is not a full IANA time-zone conversion and does not account for fractional-hour offsets or an offset change between the current date and the input date. Include the Luxon contributor when those cases must be handled. |
||||
|
|
||||
|
`normalizeToLocaleString` accepts standard `Intl.DateTimeFormat` options. When no options are supplied, it uses `abp.clock.toLocaleStringOptions`. The default options include the numeric year, long month, numeric day, hour, minute and second. You can replace them to define application-wide display defaults: |
||||
|
|
||||
|
````js |
||||
|
abp.clock.toLocaleStringOptions = { |
||||
|
year: 'numeric', |
||||
|
month: '2-digit', |
||||
|
day: '2-digit', |
||||
|
hour: '2-digit', |
||||
|
minute: '2-digit' |
||||
|
}; |
||||
|
```` |
||||
|
|
||||
|
## Browser Time-Zone Cookie |
||||
|
|
||||
|
After application configuration is initialized, ABP writes the browser's time-zone name to the `__timezone` cookie when multiple time zones are supported. Set the following flag before configuration initialization to disable this behavior: |
||||
|
|
||||
|
````js |
||||
|
abp.clock.trySetBrowserTimeZoneToCookie = false; |
||||
|
```` |
||||
|
|
||||
|
Use `abp.clock.browserTimeZone()` to read the browser time zone or `abp.clock.setBrowserTimeZoneToCookie()` to refresh the cookie explicitly. |
||||
@ -1,6 +1,99 @@ |
|||||
# Multi Lingual Entities |
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Use ABP multi-lingual objects to select translations by culture, apply fallback rules, and process translations in bulk." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
This feature is still under development. |
# Multi-Lingual Objects |
||||
Follow the below link to get information about the development status |
|
||||
|
|
||||
https://github.com/abpframework/abp/issues/11698 |
The `Volo.Abp.MultiLingualObject` package provides a contract and a selection service for objects that store one translation per language. Persistence mapping is application-specific; the package does not create a database relationship for the translations. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
Install the package in the project that defines the consuming module: |
||||
|
|
||||
|
```bash |
||||
|
abp add-package Volo.Abp.MultiLingualObject |
||||
|
``` |
||||
|
|
||||
|
Add `AbpMultiLingualObjectsModule` as a dependency of that module when the package is installed manually: |
||||
|
|
||||
|
````csharp |
||||
|
[DependsOn(typeof(AbpMultiLingualObjectsModule))] |
||||
|
public class MyApplicationModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
## Define a Multi-Lingual Object |
||||
|
|
||||
|
Implement `IMultiLingualObject<TTranslation>` on the object and `IObjectTranslation` on its translation type: |
||||
|
|
||||
|
```csharp |
||||
|
using Volo.Abp.MultiLingualObjects; |
||||
|
|
||||
|
public class Product : IMultiLingualObject<ProductTranslation> |
||||
|
{ |
||||
|
public ICollection<ProductTranslation> Translations { get; set; } = |
||||
|
new List<ProductTranslation>(); |
||||
|
} |
||||
|
|
||||
|
public class ProductTranslation : IObjectTranslation |
||||
|
{ |
||||
|
public string Language { get; set; } = string.Empty; |
||||
|
|
||||
|
public string Name { get; set; } = string.Empty; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`Language` stores a culture name such as `en`, `en-US` or `tr`. |
||||
|
|
||||
|
## Select a Translation |
||||
|
|
||||
|
Inject `IMultiLingualObjectManager` and call `GetTranslationAsync`: |
||||
|
|
||||
|
```csharp |
||||
|
public class ProductService |
||||
|
: ITransientDependency |
||||
|
{ |
||||
|
private readonly IMultiLingualObjectManager _multiLingualObjectManager; |
||||
|
|
||||
|
public ProductService( |
||||
|
IMultiLingualObjectManager multiLingualObjectManager) |
||||
|
{ |
||||
|
_multiLingualObjectManager = multiLingualObjectManager; |
||||
|
} |
||||
|
|
||||
|
public Task<ProductTranslation?> GetTranslationAsync(Product product) |
||||
|
{ |
||||
|
return _multiLingualObjectManager |
||||
|
.GetTranslationAsync<Product, ProductTranslation>(product); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
With the default arguments, the manager uses `CultureInfo.CurrentUICulture.Name`. It selects translations in this order: |
||||
|
|
||||
|
1. An exact translation for the current UI culture. |
||||
|
2. A translation for a parent of the current UI culture when `fallbackToParentCultures` is `true`. |
||||
|
3. A translation for the language configured by `LocalizationSettingNames.DefaultLanguage`. |
||||
|
4. The first available translation. |
||||
|
|
||||
|
The method returns `null` when the collection is null or empty. To disable only the parent-culture fallback, pass `culture` and set `fallbackToParentCultures` to `false`. The default-language and first-available fallbacks still apply. |
||||
|
|
||||
|
## Select Translations in Bulk |
||||
|
|
||||
|
Use `GetBulkTranslationsAsync` to select translations for multiple objects with the same culture and fallback settings: |
||||
|
|
||||
|
```csharp |
||||
|
var results = await _multiLingualObjectManager |
||||
|
.GetBulkTranslationsAsync<Product, ProductTranslation>(products); |
||||
|
|
||||
|
foreach (var (product, translation) in results) |
||||
|
{ |
||||
|
// Use product and its selected translation. |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Each result keeps the source object together with its selected translation. An object with no translations gets a `null` translation. |
||||
|
|||||
Loading…
Reference in new issue