@ -0,0 +1,30 @@ |
|||||
|
name: "Main" |
||||
|
on: |
||||
|
pull_request: |
||||
|
paths: |
||||
|
- "framework/**" |
||||
|
- "modules/**" |
||||
|
- "templates/**" |
||||
|
push: |
||||
|
paths: |
||||
|
- "framework/**" |
||||
|
- "modules/**" |
||||
|
- "templates/**" |
||||
|
jobs: |
||||
|
build-test: |
||||
|
runs-on: windows-latest |
||||
|
steps: |
||||
|
- uses: actions/checkout@v2 |
||||
|
- uses: actions/setup-dotnet@master |
||||
|
with: |
||||
|
dotnet-version: 3.1.100 |
||||
|
|
||||
|
- name: Build All |
||||
|
run: .\build-all.ps1 |
||||
|
working-directory: .\build |
||||
|
shell: powershell |
||||
|
|
||||
|
- name: Test All |
||||
|
run: .\test-all.ps1 |
||||
|
working-directory: .\build |
||||
|
shell: powershell |
||||
@ -0,0 +1,13 @@ |
|||||
|
{ |
||||
|
"culture": "ru", |
||||
|
"texts": { |
||||
|
"Account": "Аккаунт", |
||||
|
"Welcome": "Добро пожаловать", |
||||
|
"UseOneOfTheFollowingLinksToContinue": "Для продолжения используйте одну из следующих ссылок", |
||||
|
"FrameworkHomePage": "Главная страница фреймворка", |
||||
|
"FrameworkDocumentation": "Документация фреймворка", |
||||
|
"OfficialBlog": "Официальный блог", |
||||
|
"CommercialHomePage": "Главная страница коммерческой версии", |
||||
|
"CommercialSupportWebSite": "Сайт коммерческой поддержки" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
{ |
||||
|
"culture": "ru", |
||||
|
"texts": { |
||||
|
"Permission:Organizations": "Организации", |
||||
|
"Permission:Manage": "Управление организациями", |
||||
|
"Permission:NpmPackages": "Пакеты NPM", |
||||
|
"Permission:NugetPackages": "Пакеты NuGet", |
||||
|
"Permission:Maintenance": "Обслуживание", |
||||
|
"Permission:Maintain": "Обслуживать", |
||||
|
"Permission:ClearCaches": "Очистить кэш", |
||||
|
"Permission:Modules": "Модули", |
||||
|
"Permission:Packages": "Пакеты", |
||||
|
"Permission:Edit": "Редактировать", |
||||
|
"Permission:Delete": "Удалить", |
||||
|
"Permission:Create": "Создать", |
||||
|
"Menu:Organizations": "Организации", |
||||
|
"Menu:Packages": "Пакеты", |
||||
|
"NpmPackageDeletionWarningMessage": "Этот пакет NPM будет удален. Вы подтверждаете это?", |
||||
|
"NugetPackageDeletionWarningMessage": "Этот пакет NuGet будет удален. Вы подтверждаете это?", |
||||
|
"ModuleDeletionWarningMessage": "Этот модуль будет удален. Вы подтверждаете это?", |
||||
|
"Name": "Имя", |
||||
|
"DisplayName": "Отображаемое имя", |
||||
|
"ShortDescription": "Краткое описание", |
||||
|
"NameFilter": "Имя", |
||||
|
"CreationTime": "Время создания", |
||||
|
"IsPro": "Is pro", |
||||
|
"EfCoreConfigureMethodName": "Настроить имя метода", |
||||
|
"IsProFilter": "Is pro", |
||||
|
"ApplicationType": "Тип приложения", |
||||
|
"Target": "Цель", |
||||
|
"TargetFilter": "Цель", |
||||
|
"ModuleClass": "Класс модуля", |
||||
|
"NugetPackageTarget.DomainShared": "Domain Shared", |
||||
|
"NugetPackageTarget.Domain": "Domain", |
||||
|
"NugetPackageTarget.Application": "Application", |
||||
|
"NugetPackageTarget.ApplicationContracts": "Application Contracts", |
||||
|
"NugetPackageTarget.HttpApi": "Http Api", |
||||
|
"NugetPackageTarget.HttpApiClient": "Http Api Client", |
||||
|
"NugetPackageTarget.Web": "Web", |
||||
|
"NugetPackageTarget.EntityFrameworkCore": "DeleteAllEntityFramework Core", |
||||
|
"NugetPackageTarget.MongoDB": "MongoDB", |
||||
|
"Edit": "Редактировать", |
||||
|
"Delete": "Удалить", |
||||
|
"Refresh": "Обновить", |
||||
|
"NpmPackages": "NPM пакеты", |
||||
|
"NugetPackages": "NuGet пакеты", |
||||
|
"NpmPackageCount": "Количество пакетов NPM", |
||||
|
"NugetPackageCount": "Количество пакетов NuGet", |
||||
|
"Module": "Модули", |
||||
|
"ModuleInfo": "Информация о модуле", |
||||
|
"CreateANpmPackage": "Создать пакет NPM", |
||||
|
"CreateAModule": "Создать модуль", |
||||
|
"CreateANugetPackage": "Создать пакет NuGet", |
||||
|
"AddNew": "Добавить новый", |
||||
|
"PackageAlreadyExist{0}": "\"{0}\" пакет уже существует.", |
||||
|
"ModuleAlreadyExist{0}": "\"{0}\" модуль уже добавлен.", |
||||
|
"ClearCache": "Очистить кэш", |
||||
|
"SuccessfullyCleared": "Успешно очищено", |
||||
|
"Menu:NpmPackages": "Пакеты NPM", |
||||
|
"Menu:Modules": "Модули", |
||||
|
"Menu:Maintenance": "Поддержка", |
||||
|
"Menu:NugetPackages": "Пакеты NuGet", |
||||
|
"CreateAnOrganization": "Создать организацию", |
||||
|
"Organizations": "Организации", |
||||
|
"LongName": "Полное название", |
||||
|
"LicenseType": "Тип лицензии", |
||||
|
"LicenseStartTime": "Время начала действия лицензии", |
||||
|
"LicenseEndTime": "Время окончания действия лицензии", |
||||
|
"AllowedDeveloperCount": "Разрешенное количество разработчиков", |
||||
|
"UserNameOrEmailAddress": "Имя пользователя или адрес электронной почты", |
||||
|
"AddOwner": "Добавить владельца", |
||||
|
"UserName": "Имя пользователя", |
||||
|
"Email": "Электронная почта", |
||||
|
"Developers": "Разработчики", |
||||
|
"AddDeveloper": "Добавить разработчика", |
||||
|
"Create": "Создать", |
||||
|
"UserNotFound": "Пользователь не обнаружен", |
||||
|
"{0}WillBeRemovedFromMembers": "{0} будет удален из членов", |
||||
|
"Computers": "Компьютеры", |
||||
|
"UniqueComputerId": "Уникальный id компьютера", |
||||
|
"LastSeenDate": "Дата последнего визита", |
||||
|
"{0}Computer{1}WillBeRemovedFromRecords": "Компьютер {0} ({1}) будет удален из записей", |
||||
|
"OrganizationDeletionWarningMessage": "Организация будет удалена", |
||||
|
"This{0}AlreadyExistInThisOrganization": "{0} уже существует в данной организации", |
||||
|
"AreYouSureYouWantToDeleteAllComputers": "Вы уверены, что хотите удалить все компьютеры?", |
||||
|
"DeleteAll": "Удалить все", |
||||
|
"DoYouWantToCreateNewUser": "Вы хотите создать нового пользователя?", |
||||
|
"MasterModules": "Мастер модулей" |
||||
|
} |
||||
|
} |
||||
@ -1,3 +0,0 @@ |
|||||
## AutoMapper Integration |
|
||||
|
|
||||
TODO |
|
||||
@ -1,126 +1,8 @@ |
|||||
## Getting Started With the Angular Application Template |
# Getting Started with the Startup Templates |
||||
|
|
||||
This tutorial explains how to create a new Angular application using the startup template, configure and run it. |
See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: |
||||
|
|
||||
### Creating a New Project |
* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) |
||||
|
* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) |
||||
|
|
||||
This tutorial uses **ABP CLI** to create a new project. See the [Get Started](https://abp.io/get-started) page for other options. |
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
||||
|
|
||||
Install the ABP CLI using a command line window, if you've not installed before: |
|
||||
|
|
||||
````bash |
|
||||
dotnet tool install -g Volo.Abp.Cli |
|
||||
```` |
|
||||
|
|
||||
Use `abp new` command in an empty folder to create your project: |
|
||||
|
|
||||
````bash |
|
||||
abp new Acme.BookStore -u angular |
|
||||
```` |
|
||||
|
|
||||
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. |
|
||||
|
|
||||
`-u angular` option specifies the UI framework to be Angular. Default database provider is EF Core. See the [CLI documentation](CLI.md) for all available options. |
|
||||
|
|
||||
#### Pre Requirements |
|
||||
|
|
||||
The created solution requires; |
|
||||
|
|
||||
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) |
|
||||
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) |
|
||||
* [Node v12+](https://nodejs.org) |
|
||||
* [Yarn v1.19+](https://classic.yarnpkg.com/) |
|
||||
|
|
||||
### The Solution Structure |
|
||||
|
|
||||
Open the solution in **Visual Studio**: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
The solution has a layered structure (based on [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects properly configured to work with **EF Core** & **SQLite in-memory** database. |
|
||||
|
|
||||
> See the [Application Template Document](Startup-Templates/Application.md) to understand the solution structure in details. |
|
||||
|
|
||||
### Database Connection String |
|
||||
|
|
||||
Check the **connection string** in the `appsettings.json` file under the `.HttpApi.Host` project: |
|
||||
|
|
||||
````json |
|
||||
{ |
|
||||
"ConnectionStrings": { |
|
||||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|
||||
} |
|
||||
} |
|
||||
```` |
|
||||
|
|
||||
The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use another DBMS if you want. Change the connection string if you need. |
|
||||
|
|
||||
### Create Database & Apply Database Migrations |
|
||||
|
|
||||
You have two options to create the database. |
|
||||
|
|
||||
#### Using the DbMigrator Application |
|
||||
|
|
||||
The solution contains a console application (named `Acme.BookStore.DbMigrator` in this sample) that can create database, apply migrations and seed initial data. It is useful on development as well as on production environment. |
|
||||
|
|
||||
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
|
||||
|
|
||||
Right click to the `.DbMigrator` project and select **Set as StartUp Project**: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
#### Using EF Core Update-Database Command |
|
||||
|
|
||||
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
This will create a new database based on the configured connection string. |
|
||||
|
|
||||
> Using the `.DbMigrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application. |
|
||||
|
|
||||
### Running the Application |
|
||||
|
|
||||
#### Run the API Host (Server Side) |
|
||||
|
|
||||
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. |
|
||||
|
|
||||
##### Authorization for the Swagger UI |
|
||||
|
|
||||
Most of the application APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too. |
|
||||
|
|
||||
#### Run the Angular Application (Client Side) |
|
||||
|
|
||||
Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest the [yarn](https://yarnpkg.com) package manager while `npm install` will also work in most cases) |
|
||||
|
|
||||
````bash |
|
||||
yarn |
|
||||
```` |
|
||||
|
|
||||
Once all node modules are loaded, execute `yarn start` or `npm start` command: |
|
||||
|
|
||||
````bash |
|
||||
yarn start |
|
||||
```` |
|
||||
|
|
||||
Open your favorite browser and go to `localhost:4200` URL. Initial username is `admin` and password is `1q2w3E*`. |
|
||||
|
|
||||
The startup template includes the **identity management** and **tenant management** modules. Once you login, the Administration menu will be available where you can manage **tenants**, **roles**, **users** and their **permissions**. |
|
||||
|
|
||||
> We recommend [Visual Studio Code](https://code.visualstudio.com/) as the editor for the Angular project, but you are free to use your favorite editor. |
|
||||
|
|
||||
### What's Next? |
|
||||
|
|
||||
* [Application development tutorial](Tutorials/Part-1) |
|
||||
@ -1,104 +1,8 @@ |
|||||
## Getting Started With the ASP.NET Core MVC Template |
# Getting Started with the Startup Templates |
||||
|
|
||||
This tutorial explains how to create a new ASP.NET Core MVC web application using the startup template, configure and run it. |
See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: |
||||
|
|
||||
### Creating a New Project |
* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) |
||||
|
* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) |
||||
|
|
||||
This tutorial uses **ABP CLI** to create a new project. See the [Get Started](https://abp.io/get-started) page for other options. |
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
||||
|
|
||||
Install the ABP CLI using a command line window, if you've not installed before: |
|
||||
|
|
||||
````bash |
|
||||
dotnet tool install -g Volo.Abp.Cli |
|
||||
```` |
|
||||
|
|
||||
Use `abp new` command in an empty folder to create your project: |
|
||||
|
|
||||
````bash |
|
||||
abp new Acme.BookStore |
|
||||
```` |
|
||||
|
|
||||
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. |
|
||||
|
|
||||
`new` command creates a **layered MVC application** with **Entity Framework Core** as the database provider. However, it has additional options. See the [CLI documentation](CLI.md) for all available options. |
|
||||
|
|
||||
#### Pre Requirements |
|
||||
|
|
||||
The created solution requires; |
|
||||
|
|
||||
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) |
|
||||
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) |
|
||||
* [Node v12+](https://nodejs.org) |
|
||||
* [Yarn v1.19+](https://classic.yarnpkg.com/) |
|
||||
|
|
||||
### The Solution Structure |
|
||||
|
|
||||
Open the solution in **Visual Studio**: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
The solution has a layered structure (based on [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects properly configured to work with **EF Core** & **SQLite in-memory** database. |
|
||||
|
|
||||
> See [Application template document](Startup-Templates/Application.md) to understand the solution structure in details. |
|
||||
|
|
||||
### Database Connection String |
|
||||
|
|
||||
Check the **connection string** in the `appsettings.json` file under the `.Web` project: |
|
||||
|
|
||||
````json |
|
||||
{ |
|
||||
"ConnectionStrings": { |
|
||||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|
||||
} |
|
||||
} |
|
||||
```` |
|
||||
|
|
||||
The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use another DBMS if you want. Change the connection string if you need. |
|
||||
|
|
||||
### Create Database & Apply Database Migrations |
|
||||
|
|
||||
You have two options to create the database. |
|
||||
|
|
||||
#### Using the DbMigrator Application |
|
||||
|
|
||||
The solution contains a console application (named `Acme.BookStore.DbMigrator` in this sample) that can create database, apply migrations and seed initial data. It is useful on development as well as on production environment. |
|
||||
|
|
||||
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
|
||||
|
|
||||
Right click to the `.DbMigrator` project and select **Set as StartUp Project**: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
#### Using EF Core Update-Database Command |
|
||||
|
|
||||
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. Right click to the `.Web` project and select **Set as StartUp Project**: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
This will create a new database based on the configured connection string. |
|
||||
|
|
||||
> Using the `.Migrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application. |
|
||||
|
|
||||
### Running the Application |
|
||||
|
|
||||
Ensure that the `.Web` project is the startup project. Run the application which will open the **home** page in your browser: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
Click the **Login** button, enter `admin` as the username and `1q2w3E*` as the password to login to the application. |
|
||||
|
|
||||
The startup template includes the **identity management** and **tenant management** modules. Once you login, the Administration menu will be available where you can manage **tenants**, **roles**, **users** and their **permissions**. User management page is shown below: |
|
||||
|
|
||||
 |
|
||||
|
|
||||
### What's Next? |
|
||||
|
|
||||
* [Application development tutorial](Tutorials/Part-1.md) |
|
||||
@ -0,0 +1,8 @@ |
|||||
|
# Getting Started with the Startup Templates |
||||
|
|
||||
|
See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: |
||||
|
|
||||
|
* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) |
||||
|
* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) |
||||
|
|
||||
|
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
||||
@ -0,0 +1,407 @@ |
|||||
|
# Getting started |
||||
|
|
||||
|
````json |
||||
|
//[doc-params] |
||||
|
{ |
||||
|
"UI": ["MVC","NG"], |
||||
|
"DB": ["EF", "Mongo"], |
||||
|
"Tiered": ["Yes", "No"] |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
This tutorial explains how to create a new {{if UI == "MVC"}} ASP.NET Core MVC web {{else if UI == "NG"}} Angular {{end}} application using the startup template, configure and run it. |
||||
|
|
||||
|
|
||||
|
## Setup your development environment |
||||
|
|
||||
|
First things first! Let's setup your development environment before creating the first project. |
||||
|
|
||||
|
### Pre-requirements |
||||
|
|
||||
|
The following tools should be installed on your development machine: |
||||
|
|
||||
|
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). |
||||
|
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) |
||||
|
|
||||
|
* [Node v12+](https://nodejs.org) |
||||
|
* [Yarn v1.19+](https://classic.yarnpkg.com/) |
||||
|
|
||||
|
> You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core. |
||||
|
|
||||
|
### Install the ABP CLI |
||||
|
|
||||
|
[ABP CLI](./CLI.md) is a command line interface that is used to authenticate and automate some tasks for ABP based applications. |
||||
|
|
||||
|
> ABP CLI is a free & open source tool for [the ABP framework](https://abp.io/). |
||||
|
|
||||
|
First, you need to install the ABP CLI using the following command: |
||||
|
|
||||
|
````shell |
||||
|
dotnet tool install -g Volo.Abp.Cli |
||||
|
```` |
||||
|
|
||||
|
If you've already installed, you can update it using the following command: |
||||
|
|
||||
|
````shell |
||||
|
dotnet tool update -g Volo.Abp.Cli |
||||
|
```` |
||||
|
|
||||
|
## Create a new project |
||||
|
|
||||
|
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. |
||||
|
|
||||
|
### Using the ABP CLI to create a new project |
||||
|
|
||||
|
Use the `new` command of the ABP CLI to create a new project: |
||||
|
|
||||
|
````shell |
||||
|
abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes" && UI != "NG"}} --tiered {{else if Tiered == "Yes" && UI == "NG"}}--separate-identity-server{{end}} |
||||
|
```` |
||||
|
|
||||
|
* `-t` argument specifies the [startup template](Startup-Templates/Application.md) name. `app` is the startup template that contains the essential [ABP Modules](Modules/Index.md) pre-installed and configured for you. |
||||
|
|
||||
|
{{ if UI == "NG" }} |
||||
|
|
||||
|
* `-u` argument specifies the UI framework, `angular` in this case. |
||||
|
|
||||
|
{{ if Tiered == "Yes" }} |
||||
|
|
||||
|
* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
{{ if DB == "Mongo" }} |
||||
|
|
||||
|
* `-d` argument specifies the database provider, `mongodb` in this case. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
{{ if Tiered == "Yes" && UI != "NG" }} |
||||
|
|
||||
|
* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. |
||||
|
|
||||
|
#### ABP CLI commands & options |
||||
|
|
||||
|
[ABP CLI document](./CLI.md) covers all of the available commands and options for the ABP CLI. See the [ABP Startup Templates](Startup-Templates/Index.md) document for other templates. |
||||
|
|
||||
|
## The solution structure |
||||
|
|
||||
|
{{ if UI == "MVC" }} |
||||
|
|
||||
|
After creating your project, you will have the following solution folders & files: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
You will see the following solution structure when you open the `.sln` file in the Visual Studio: |
||||
|
|
||||
|
{{if DB == "Mongo"}} |
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{else}} |
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{end}} |
||||
|
|
||||
|
{{ else if UI == "NG" }} |
||||
|
There are three folders in the created solution: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
* `angular` folder contains the Angular UI application. |
||||
|
* `aspnet-core` folder contains the backend solution. |
||||
|
* `react-native` folder contains the React Native UI application. |
||||
|
|
||||
|
Open the `.sln` (Visual Studio solution) file under the `aspnet-core` folder: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
> ###### About the projects in your solution |
||||
|
> |
||||
|
> Your solution may have slightly different structure based on your **UI**, **database** and other preferences. |
||||
|
|
||||
|
The solution has a layered structure (based on [Domain Driven Design](./Domain-Driven-Design.md)) and also contains unit & integration test projects. |
||||
|
|
||||
|
{{ if DB == "EF" }} |
||||
|
|
||||
|
Integration tests projects are properly configured to work with **EF Core** & **SQLite in-memory** database. |
||||
|
|
||||
|
{{ else if DB == "Mongo" }} |
||||
|
|
||||
|
Integration tests projects are properly configured to work with in-memory **MongoDB** database created per test (used [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library). |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
> See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. |
||||
|
|
||||
|
## Create the database |
||||
|
|
||||
|
### Database connection string |
||||
|
|
||||
|
Check the **connection string** in the `appsettings.json` file under the {{if UI == "MVC"}}{{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}`.Web` project{{end}}{{else if UI == "NG" }}`.HttpApi.Host` project{{end}}: |
||||
|
|
||||
|
{{ if DB == "EF" }} |
||||
|
|
||||
|
````json |
||||
|
"ConnectionStrings": { |
||||
|
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](https://docs.abp.io/en/abp/latest/Entity-Framework-Core) to learn how to switch to another DBMS. |
||||
|
|
||||
|
### Apply the migrations |
||||
|
|
||||
|
The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). So, you need to apply migrations to create the database. There are two ways of applying the database migrations. |
||||
|
|
||||
|
#### Apply migrations using the DbMigrator |
||||
|
|
||||
|
The solution comes with a `.DbMigrator` console application which applies migrations and also seed the initial data. It is useful on development as well as on production environment. |
||||
|
|
||||
|
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
||||
|
|
||||
|
Right click to the `.DbMigrator` project and select **Set as StartUp Project** |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
> Initial seed data creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. |
||||
|
|
||||
|
#### Using EF Core Update-Database command |
||||
|
|
||||
|
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. |
||||
|
|
||||
|
{{ if UI == "MVC" }} |
||||
|
|
||||
|
Right click to the {{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}} project and select **Set as StartUp project**: |
||||
|
|
||||
|
{{ else if UI != "MVC" }} |
||||
|
|
||||
|
Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**: |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
This will create a new database based on the configured connection string. |
||||
|
|
||||
|
> Using the `.Migrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application. |
||||
|
|
||||
|
{{ else if DB == "Mongo" }} |
||||
|
|
||||
|
````json |
||||
|
"ConnectionStrings": { |
||||
|
"Default": "mongodb://localhost:27017/BookStore" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. |
||||
|
|
||||
|
### Seed initial data |
||||
|
|
||||
|
The solution comes with a `.DbMigrator` console application which seeds the initial data. It is useful on development as well as on production environment. |
||||
|
|
||||
|
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
||||
|
|
||||
|
Right click to the `.DbMigrator` project and select **Set as StartUp Project** |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
> Initial seed data creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
## Run the application |
||||
|
|
||||
|
{{ if UI == "MVC" }} |
||||
|
|
||||
|
{{ if Tiered == "Yes" }} |
||||
|
|
||||
|
Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. |
||||
|
|
||||
|
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
||||
|
|
||||
|
You can login, but you cannot enter to the main application here. This is just the authentication server. |
||||
|
|
||||
|
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
This is the API application that is used by the web application. |
||||
|
|
||||
|
Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Click to the **login** button which will redirect you to the `Identity Server` to login to the application: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{ else }} |
||||
|
|
||||
|
Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser: |
||||
|
|
||||
|
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
{{ else if UI != "MVC" }} |
||||
|
|
||||
|
#### Running the HTTP API Host (server-side) |
||||
|
|
||||
|
{{ if Tiered == "Yes" }} |
||||
|
|
||||
|
Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. |
||||
|
|
||||
|
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
||||
|
|
||||
|
You can login, but you cannot enter to the main application here. This is just the authentication server. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: |
||||
|
|
||||
|
{{ if Tiered == "No" }} |
||||
|
|
||||
|
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
 |
||||
|
|
||||
|
You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. |
||||
|
|
||||
|
> ##### Authorization for the Swagger UI |
||||
|
> |
||||
|
> Most of the HTTP APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
{{ if UI == "NG" }} |
||||
|
#### Running the Angular application (client-side) |
||||
|
|
||||
|
Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work in most cases) |
||||
|
|
||||
|
```bash |
||||
|
yarn |
||||
|
``` |
||||
|
|
||||
|
Once all node modules are loaded, execute `yarn start` (or `npm start`) command: |
||||
|
|
||||
|
```bash |
||||
|
yarn start |
||||
|
``` |
||||
|
|
||||
|
Wait `Angular CLI` to launch `Webpack` dev-server with `BrowserSync`. |
||||
|
This will take care of compiling your `TypeScript` code, and automatically reloading your browser. |
||||
|
After it finishes, `Angular Live Development Server` will be listening on localhost:4200, |
||||
|
open your web browser and navigate to [localhost:4200](http://localhost:4200/) |
||||
|
|
||||
|
|
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
Enter **admin** as the username and **1q2w3E*** as the password to login to the application: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
The application is up and running. You can start developing your application based on this startup template. |
||||
|
|
||||
|
#### Mobile Development |
||||
|
|
||||
|
ABP platform provide [React Native](https://reactnative.dev/) template to develop mobile applications. |
||||
|
|
||||
|
>The solution includes the React Native application in the `react-native` folder as default. If you don't plan to develop a mobile application with React Native, you can ignore this step and delete the `react-native` folder. |
||||
|
|
||||
|
The React Native application running on an Android emulator or a physical phone cannot connect to the backend on `localhost`. To fix this problem, it is necessary to run backend on the local IP. |
||||
|
|
||||
|
{{ if Tiered == "No"}} |
||||
|
 |
||||
|
|
||||
|
* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `SelfUrl` and `Authority` properties with your local IP address. |
||||
|
* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. |
||||
|
|
||||
|
{{ else if Tiered == "Yes" }} |
||||
|
|
||||
|
 |
||||
|
|
||||
|
* Open the `appsettings.json` in the `.IdentityServer` folder. Replace the `localhost` address on the `SelfUrl` property with your local IP address. |
||||
|
* Open the `launchSettings.json` in the `.IdentityServer/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. |
||||
|
* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `Authority` property with your local IP address. |
||||
|
* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
Run the backend as described in the [**Running the HTTP API Host (server-side)**](#running-the-http-api-host-server-side) section. |
||||
|
|
||||
|
> React Native application does not trust the auto-generated .NET HTTPS certificate, you should use the HTTP during development. |
||||
|
|
||||
|
Go to the `react-native` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work in most cases): |
||||
|
|
||||
|
```bash |
||||
|
yarn |
||||
|
``` |
||||
|
|
||||
|
* Open the `Environment.js` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
{{ if Tiered == "Yes" }} |
||||
|
|
||||
|
> Make sure that `issuer` matches the running address of the `.IdentityServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` project. |
||||
|
|
||||
|
{{else}} |
||||
|
|
||||
|
> Make sure that `issuer` and `apiUrl` matches the running address of the `.HttpApi.Host` project. |
||||
|
|
||||
|
{{ end }} |
||||
|
|
||||
|
Once all node modules are loaded, execute `yarn start` (or `npm start`) command: |
||||
|
|
||||
|
```bash |
||||
|
yarn start |
||||
|
``` |
||||
|
|
||||
|
Wait Expo CLI to start. Expo CLI opens the management interface on the `http://localhost:19002/` address. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
In the above management interface, you can start the application with an Android emulator, an iOS simulator or a physical phone by the scan the QR code with the [Expo Client](https://expo.io/tools#client). |
||||
|
|
||||
|
> See the [Android Studio Emulator](https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/), [iOS Simulator](https://docs.expo.io/versions/v36.0.0/workflow/ios-simulator/) documents on expo.io. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Enter **admin** as the username and **1q2w3E*** as the password to login to the application. |
||||
|
|
||||
|
The application is up and running. You can continue to develop your application based on this startup template. |
||||
|
|
||||
|
> The [application startup template](Startup-Templates/Application.md) includes the TenantManagement and Identity modules. |
||||
|
|
||||
|
## What's next? |
||||
|
|
||||
|
[Application development tutorial](Tutorials/Part-1.md) |
||||
@ -0,0 +1,201 @@ |
|||||
|
# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications |
||||
|
|
||||
|
This guide demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. |
||||
|
|
||||
|
Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly. |
||||
|
|
||||
|
Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage. |
||||
|
|
||||
|
1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application. |
||||
|
|
||||
|
2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections. |
||||
|
|
||||
|
> There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings. |
||||
|
> |
||||
|
> However there are key differences in integration to ABP applications because of default configurated signin schemes which will be explained below. |
||||
|
|
||||
|
## 1. AddAzureAD |
||||
|
|
||||
|
This approach uses the most common way to integrate AzureAD by using the [Microsoft AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/). |
||||
|
|
||||
|
If you choose this approach, you will need to install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** project. Also, since AddAzureAD extension uses [configuration binding](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration), you need to update your appsettings.json file located in your **.Web** project. |
||||
|
|
||||
|
#### **Updating `appsettings.json`** |
||||
|
|
||||
|
You need to add a new section to your `appsettings.json` which will be binded to configuration when configuring the `OpenIdConnectOptions`: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> Important configuration here is the CallbackPath. This value must be the same with one of your Azure AD-> app registrations-> Authentication -> RedirectUri. |
||||
|
|
||||
|
Then, you need to configure the `OpenIdConnectOptions` to complete the integration. |
||||
|
|
||||
|
#### Configuring OpenIdConnectOptions |
||||
|
|
||||
|
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "Acme.BookStore"; |
||||
|
}) |
||||
|
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
||||
|
|
||||
|
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
||||
|
{ |
||||
|
options.Authority = options.Authority + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
|
||||
|
options.TokenValidationParameters.ValidateIssuer = false; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
options.SaveTokens = true; |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> **Don't forget to:** |
||||
|
> |
||||
|
> * Add `.AddAzureAD(options => configuration.Bind("AzureAd", options))` after `.AddAuthentication()`. This binds your AzureAD appsettings and easy to miss out. |
||||
|
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping. |
||||
|
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information. |
||||
|
> * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
||||
|
> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](../Modules/Account.md) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
||||
|
|
||||
|
You are done and integration is completed. |
||||
|
|
||||
|
## 2. Alternative Approach: AddOpenIdConnect |
||||
|
|
||||
|
If you don't want to use an extra nuget package in your application, you can use the straight default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for all OpenId connections including AzureAD external authentication. |
||||
|
|
||||
|
You don't have to use `appsettings.json` configuration but it is a good practice to set AzureAD information in the `appsettings.json`. |
||||
|
|
||||
|
To get the AzureAD information from `appsettings.json`, which will be used in `OpenIdConnectOptions` configuration, simply add a new section to `appsettings.json` located in your **.Web** project: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Then, In your **.Web** project; you can modify the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
|
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "BookStore"; |
||||
|
}) |
||||
|
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
||||
|
{ |
||||
|
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.SaveTokens = true; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers. |
||||
|
|
||||
|
## The Source Code |
||||
|
|
||||
|
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
||||
|
|
||||
|
# FAQ |
||||
|
|
||||
|
* Help! `GetExternalLoginInfoAsync` returns `null`! |
||||
|
|
||||
|
* There can be 2 reasons for this; |
||||
|
|
||||
|
1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`: |
||||
|
|
||||
|
````csharp |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
```` |
||||
|
|
||||
|
2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping: |
||||
|
|
||||
|
````csharp |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
```` |
||||
|
|
||||
|
|
||||
|
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! |
||||
|
|
||||
|
* If you set your **CallbackPath** in appsettings as: |
||||
|
|
||||
|
````csharp |
||||
|
"AzureAd": { |
||||
|
... |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
your **Redirect URI** of your application in azure portal must be with <u>domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. |
||||
|
|
||||
|
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! |
||||
|
|
||||
|
|
||||
|
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add |
||||
|
|
||||
|
````csharp |
||||
|
options.Scope.Add("email"); |
||||
|
```` |
||||
|
|
||||
|
to your openid configuration. |
||||
|
|
||||
|
* How can I **debug/watch** which claims I get before they get mapped? |
||||
|
|
||||
|
* You can add a simple event under openid configuration to debug before mapping like: |
||||
|
|
||||
|
````csharp |
||||
|
options.Events.OnTokenValidated = (async context => |
||||
|
{ |
||||
|
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
||||
|
await Task.CompletedTask; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
||||
|
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md). |
||||
@ -0,0 +1,113 @@ |
|||||
|
# How to Customize the Login Page for MVC / Razor Page Applications |
||||
|
|
||||
|
When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account.md) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. |
||||
|
|
||||
|
This document explains how to customize the login page for your own application. |
||||
|
|
||||
|
## Create a Login PageModel |
||||
|
|
||||
|
Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module. |
||||
|
|
||||
|
````csharp |
||||
|
public class CustomLoginModel : LoginModel |
||||
|
{ |
||||
|
public CustomLoginModel( |
||||
|
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, |
||||
|
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions) |
||||
|
: base(schemeProvider, accountOptions) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system. |
||||
|
|
||||
|
Then you can override any method you need and add new methods and properties needed by the UI. |
||||
|
|
||||
|
## Overriding the Login Page UI |
||||
|
|
||||
|
Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module thanks to the [Virtual File System](../Virtual-File-System.md). |
||||
|
|
||||
|
A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below: |
||||
|
|
||||
|
````xml |
||||
|
@page |
||||
|
@using Volo.Abp.Account.Settings |
||||
|
@using Volo.Abp.Settings |
||||
|
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel |
||||
|
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage |
||||
|
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
||||
|
@if (Model.EnableLocalLogin) |
||||
|
{ |
||||
|
<div class="card mt-3 shadow-sm rounded"> |
||||
|
<div class="card-body p-5"> |
||||
|
<h4>@L["Login"]</h4> |
||||
|
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
||||
|
{ |
||||
|
<strong> |
||||
|
@L["AreYouANewUser"] |
||||
|
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
||||
|
</strong> |
||||
|
} |
||||
|
<form method="post" class="mt-4"> |
||||
|
<input asp-for="ReturnUrl" /> |
||||
|
<input asp-for="ReturnUrlHash" /> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> |
||||
|
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
||||
|
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="LoginInput.Password"></label> |
||||
|
<input asp-for="LoginInput.Password" class="form-control" /> |
||||
|
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-check"> |
||||
|
<label asp-for="LoginInput.RememberMe" class="form-check-label"> |
||||
|
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> |
||||
|
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) |
||||
|
</label> |
||||
|
</div> |
||||
|
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button> |
||||
|
</form> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-footer text-center border-0"> |
||||
|
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@ |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (Model.VisibleExternalProviders.Any()) |
||||
|
{ |
||||
|
<div class="col-md-6"> |
||||
|
<h4>@L["UseAnotherServiceToLogIn"]</h4> |
||||
|
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
||||
|
<input asp-for="ReturnUrl" /> |
||||
|
<input asp-for="ReturnUrlHash" /> |
||||
|
@foreach (var provider in Model.VisibleExternalProviders) |
||||
|
{ |
||||
|
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> |
||||
|
} |
||||
|
</form> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
||||
|
{ |
||||
|
<div class="alert alert-warning"> |
||||
|
<strong>@L["InvalidLoginRequest"]</strong> |
||||
|
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
||||
|
</div> |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs. |
||||
|
|
||||
|
## The Source Code |
||||
|
|
||||
|
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface.md). |
||||
@ -0,0 +1,101 @@ |
|||||
|
# How to Customize the SignIn Manager for ABP Applications |
||||
|
|
||||
|
After creating a new application using the [application startup template](../Startup-Templates/Application.md), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](../Modules/Account.md) uses the [Identity Management Module](../Modules/Identity.md) for SignIn Manager and the [Identity Management Module](../Modules/Identity.md) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). |
||||
|
|
||||
|
To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. |
||||
|
|
||||
|
This document explains how to customize the SignIn Manager for your own application. |
||||
|
|
||||
|
## Create a CustomSignInManager |
||||
|
|
||||
|
Create a new class inheriting the [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) of Microsoft Identity package. |
||||
|
|
||||
|
````csharp |
||||
|
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
||||
|
{ |
||||
|
public CustomSignInManager( |
||||
|
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
||||
|
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
||||
|
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
||||
|
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
||||
|
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
||||
|
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
||||
|
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
||||
|
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. |
||||
|
|
||||
|
Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow. |
||||
|
|
||||
|
## Overriding the GetExternalLoginInfoAsync Method |
||||
|
|
||||
|
In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is invoked when a third party authentication is implemented. |
||||
|
|
||||
|
A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept. |
||||
|
|
||||
|
````csharp |
||||
|
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
||||
|
{ |
||||
|
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
||||
|
var items = auth?.Properties?.Items; |
||||
|
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (expectedXsrf != null) |
||||
|
{ |
||||
|
if (!items.ContainsKey(XsrfKey)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
var userId = items[XsrfKey] as string; |
||||
|
if (userId != expectedXsrf) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
||||
|
var provider = items[LoginProviderKey] as string; |
||||
|
if (providerKey == null || provider == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
||||
|
?? provider; |
||||
|
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
||||
|
{ |
||||
|
AuthenticationTokens = auth.Properties.GetTokens() |
||||
|
}; |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](../Dependency-Injection.md). |
||||
|
|
||||
|
## Register to Dependency Injection |
||||
|
|
||||
|
Registering `CustomSignInManager` should be done with adding **AddSignInManager** extension method of the [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) of the [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs). |
||||
|
|
||||
|
Inside your `.Web` project, locate the `YourProjectNameWebModule` and add the following code under the `PreConfigureServices` method to replace the old `SignInManager` with your customized one: |
||||
|
|
||||
|
````csharp |
||||
|
PreConfigure<IdentityBuilder>(identityBuilder => |
||||
|
{ |
||||
|
identityBuilder.AddSignInManager<CustomSignInManager>(); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## The Source Code |
||||
|
|
||||
|
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
||||
|
* [Identity Management Module](../Modules/Identity.md). |
||||
@ -0,0 +1,9 @@ |
|||||
|
# "How To" Guides |
||||
|
|
||||
|
This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. |
||||
|
|
||||
|
## Authentication |
||||
|
|
||||
|
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) |
||||
|
* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) |
||||
|
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md) |
||||
@ -0,0 +1,365 @@ |
|||||
|
# Object Extensions |
||||
|
|
||||
|
ABP Framework provides an **object extension system** to allow you to **add extra properties** to an existing object **without modifying** the related class. This allows to extend functionalities implemented by a depended [application module](Modules/Index.md), especially when you want to [extend entities](Customizing-Application-Modules-Extending-Entities.md) and [DTOs](Customizing-Application-Modules-Overriding-Services.md) defined by the module. |
||||
|
|
||||
|
> Object extension system is not normally not needed for your own objects since you can easily add regular properties to your own classes. |
||||
|
|
||||
|
## IHasExtraProperties Interface |
||||
|
|
||||
|
This is the interface to make a class extensible. It simply defines a `Dictionary` property: |
||||
|
|
||||
|
````csharp |
||||
|
Dictionary<string, object> ExtraProperties { get; } |
||||
|
```` |
||||
|
|
||||
|
Then you can add or get extra properties using this dictionary. |
||||
|
|
||||
|
### Base Classes |
||||
|
|
||||
|
`IHasExtraProperties` interface is implemented by several base classes by default: |
||||
|
|
||||
|
* Implemented by the `AggregateRoot` class (see [entities](Entities.md)). |
||||
|
* Implemented by `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... base [DTO](Data-Transfer-Objects.md) classes. |
||||
|
* Implemented by the `ExtensibleObject`, which is a simple base class can be inherited for any type of object. |
||||
|
|
||||
|
So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually. |
||||
|
|
||||
|
### Fundamental Extension Methods |
||||
|
|
||||
|
While you can directly use the `ExtraProperties` property of a class, it is suggested to use the following extension methods while working with the extra properties. |
||||
|
|
||||
|
#### SetProperty |
||||
|
|
||||
|
Used to set the value of an extra property: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetProperty("Title", "My Title"); |
||||
|
user.SetProperty("IsSuperUser", true); |
||||
|
```` |
||||
|
|
||||
|
`SetProperty` returns the same object, so you can chain it: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetProperty("Title", "My Title") |
||||
|
.SetProperty("IsSuperUser", true); |
||||
|
```` |
||||
|
|
||||
|
#### GetProperty |
||||
|
|
||||
|
Used to read the value of an extra property: |
||||
|
|
||||
|
````csharp |
||||
|
var title = user.GetProperty<string>("Title"); |
||||
|
|
||||
|
if (user.GetProperty<bool>("IsSuperUser")) |
||||
|
{ |
||||
|
//... |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* `GetProperty` is a generic method and takes the object type as the generic parameter. |
||||
|
* Returns the default value if given property was not set before (default value is `0` for `int`, `false` for `bool`... etc). |
||||
|
|
||||
|
##### Non Primitive Property Types |
||||
|
|
||||
|
If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the `GetProperty` which returns an `object`. |
||||
|
|
||||
|
#### HasProperty |
||||
|
|
||||
|
Used to check if the object has a property set before. |
||||
|
|
||||
|
#### RemoveProperty |
||||
|
|
||||
|
Used to remove a property from the object. Use this methods instead of setting a `null` value for the property. |
||||
|
|
||||
|
### Some Best Practices |
||||
|
|
||||
|
Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead; |
||||
|
|
||||
|
* Define a constant for your extra property names |
||||
|
* Create extension methods to easily set your extra properties. |
||||
|
|
||||
|
Example: |
||||
|
|
||||
|
````csharp |
||||
|
public static class IdentityUserExtensions |
||||
|
{ |
||||
|
private const string TitlePropertyName = "Title"; |
||||
|
|
||||
|
public static void SetTitle(this IdentityUser user, string title) |
||||
|
{ |
||||
|
user.SetProperty(TitlePropertyName, title); |
||||
|
} |
||||
|
|
||||
|
public static string GetTitle(this IdentityUser user) |
||||
|
{ |
||||
|
return user.GetProperty<string>(TitlePropertyName); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Then you can easily set or get the `Title` property: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetTitle("My Title"); |
||||
|
var title = user.GetTitle(); |
||||
|
```` |
||||
|
|
||||
|
## Object Extension Manager |
||||
|
|
||||
|
While you can set arbitrary properties to an extensible object (which implements the `IHasExtraProperties` interface), `ObjectExtensionManager` is used to explicitly define extra properties for extensible classes. |
||||
|
|
||||
|
Explicitly defining an extra property has some use cases: |
||||
|
|
||||
|
* Allows to control how the extra property is handled on object to object mapping (see the section below). |
||||
|
* Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the [EF Core](Entity-Framework-Core.md). |
||||
|
|
||||
|
> `ObjectExtensionManager` implements the singleton pattern (`ObjectExtensionManager.Instance`) and you should define object extensions before your application startup. The [application startup template](Startup-Templates/Application.md) has some pre-defined static classes to safely define object extensions inside. |
||||
|
|
||||
|
### AddOrUpdate |
||||
|
|
||||
|
`AddOrUpdate` is the main method to define a extra properties or update extra properties for an object. |
||||
|
|
||||
|
Example: Define extra properties for the `IdentityUser` entity: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdate<IdentityUser>(options => |
||||
|
{ |
||||
|
options.AddOrUpdateProperty<string>("SocialSecurityNumber"); |
||||
|
options.AddOrUpdateProperty<bool>("IsSuperUser"); |
||||
|
} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
### AddOrUpdateProperty |
||||
|
|
||||
|
While `AddOrUpdateProperty` can be used on the `options` as shown before, if you want to define a single extra property, you can use the shortcut extension method too: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>("SocialSecurityNumber"); |
||||
|
```` |
||||
|
|
||||
|
Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<string>( |
||||
|
new[] |
||||
|
{ |
||||
|
typeof(IdentityUserDto), |
||||
|
typeof(IdentityUserCreateDto), |
||||
|
typeof(IdentityUserUpdateDto) |
||||
|
}, |
||||
|
"SocialSecurityNumber" |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
### Property Configuration |
||||
|
|
||||
|
`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
//Configure options... |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
> `options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document. |
||||
|
|
||||
|
The following sections explain the fundamental property configuration options. |
||||
|
|
||||
|
#### CheckPairDefinitionOnMapping |
||||
|
|
||||
|
Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better. |
||||
|
|
||||
|
## Validation |
||||
|
|
||||
|
You may want to add some **validation rules** for the extra properties you've defined. `AddOrUpdateProperty` method options allows two ways of performing validation: |
||||
|
|
||||
|
1. You can add **data annotation attributes** for a property. |
||||
|
2. You can write an action (code block) to perform a **custom validation**. |
||||
|
|
||||
|
Validation works when you use the object in a method that is **automatically validated** (e.g. controller actions, page handler methods, application service methods...). So, all extra properties are validated whenever the extended object is being validated. |
||||
|
|
||||
|
### Data Annotation Attributes |
||||
|
|
||||
|
All of the standard data annotation attributes are valid for extra properties. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUserCreateDto, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.ValidationAttributes.Add(new RequiredAttribute()); |
||||
|
options.ValidationAttributes.Add( |
||||
|
new StringLengthAttribute(32) { |
||||
|
MinimumLength = 6 |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided. |
||||
|
|
||||
|
### Custom Validation |
||||
|
|
||||
|
If you need, you can add a custom action that is executed to validate the extra properties. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUserCreateDto, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.Validators.Add(context => |
||||
|
{ |
||||
|
var socialSecurityNumber = context.Value as string; |
||||
|
|
||||
|
if (socialSecurityNumber == null || |
||||
|
socialSecurityNumber.StartsWith("X")) |
||||
|
{ |
||||
|
context.ValidationErrors.Add( |
||||
|
new ValidationResult( |
||||
|
"Invalid social security number: " + socialSecurityNumber, |
||||
|
new[] { "SocialSecurityNumber" } |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
`context.ServiceProvider` can be used to resolve a service dependency for advanced scenarios. |
||||
|
|
||||
|
In addition to add custom validation logic for a single property, you can add a custom validation logic that is executed in object level. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdate<IdentityUserCreateDto>(objConfig => |
||||
|
{ |
||||
|
//Define two properties with their own validation rules |
||||
|
|
||||
|
objConfig.AddOrUpdateProperty<string>("Password", propertyConfig => |
||||
|
{ |
||||
|
propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); |
||||
|
}); |
||||
|
|
||||
|
objConfig.AddOrUpdateProperty<string>("PasswordRepeat", propertyConfig => |
||||
|
{ |
||||
|
propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); |
||||
|
}); |
||||
|
|
||||
|
//Write a common validation logic works on multiple properties |
||||
|
|
||||
|
objConfig.Validators.Add(context => |
||||
|
{ |
||||
|
if (context.ValidatingObject.GetProperty<string>("Password") != |
||||
|
context.ValidatingObject.GetProperty<string>("PasswordRepeat")) |
||||
|
{ |
||||
|
context.ValidationErrors.Add( |
||||
|
new ValidationResult( |
||||
|
"Please repeat the same password!", |
||||
|
new[] { "Password", "PasswordRepeat" } |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## Object to Object Mapping |
||||
|
|
||||
|
Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients. |
||||
|
|
||||
|
This section offers some **good practices** to control your extra properties on object mapping. |
||||
|
|
||||
|
### MapExtraPropertiesTo |
||||
|
|
||||
|
`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo(identityUserDto); |
||||
|
```` |
||||
|
|
||||
|
`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction. |
||||
|
|
||||
|
#### MappingPropertyDefinitionChecks |
||||
|
|
||||
|
`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
MappingPropertyDefinitionChecks.None |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too. |
||||
|
|
||||
|
If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.CheckPairDefinitionOnMapping = false; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
#### Ignored Properties |
||||
|
|
||||
|
You may want to ignore some properties on a specific mapping operation: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
ignoredProperties: new[] {"MySensitiveProp"} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
Ignored properties are not copied to the target object. |
||||
|
|
||||
|
#### AutoMapper Integration |
||||
|
|
||||
|
If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. |
||||
|
|
||||
|
You can use the `MapExtraProperties()` method inside your mapping profile. |
||||
|
|
||||
|
````csharp |
||||
|
public class MyProfile : Profile |
||||
|
{ |
||||
|
public MyProfile() |
||||
|
{ |
||||
|
CreateMap<IdentityUser, IdentityUserDto>() |
||||
|
.MapExtraProperties(); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
It has the same parameters with the `MapExtraPropertiesTo` method. |
||||
|
|
||||
|
## Entity Framework Core Database Mapping |
||||
|
|
||||
|
If you're using the EF Core, you can map an extra property to a table field in the database. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.MapEfCore(b => b.HasMaxLength(32)); |
||||
|
} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. |
||||
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 128 KiB |
@ -0,0 +1,185 @@ |
|||||
|
# Confirmation Popup |
||||
|
|
||||
|
You can use the `ConfirmationService` in @abp/ng.theme.shared package to display a confirmation popup by placing at the root level in your project. |
||||
|
|
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `ConfirmationService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
|
||||
|
```js |
||||
|
import { ConfirmationService } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private confirmation: ConfirmationService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `success`, `warn`, `error`, and `info` methods of `ConfirmationService` to display a confirmation popup. |
||||
|
|
||||
|
### How to Display a Confirmation Popup |
||||
|
|
||||
|
```js |
||||
|
const confirmationStatus$ = this.confirmation.success('Message', 'Title'); |
||||
|
``` |
||||
|
|
||||
|
- The `ConfirmationService` methods accept three parameters that are `message`, `title`, and `options`. |
||||
|
- `success`, `warn`, `error`, and `info` methods return an [RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject) to listen to confirmation popup closing event. The type of event value is [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24) that is an enum. |
||||
|
|
||||
|
### How to Listen Closing Event |
||||
|
|
||||
|
You can subscribe to the confirmation closing event like below: |
||||
|
|
||||
|
```js |
||||
|
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
constructor(private confirmation: ConfirmationService) {} |
||||
|
|
||||
|
this.confirmation |
||||
|
.warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' }) |
||||
|
.subscribe((status: Confirmation.Status) => { |
||||
|
// your code here |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
- The `message` and `title` parameters accept a string, localization key or localization object. See the [localization document](./Localization.md) |
||||
|
- `Confirmation.Status` is an enum and has three properties; |
||||
|
- `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button. |
||||
|
- `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button. |
||||
|
- `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape. |
||||
|
|
||||
|
|
||||
|
If you are not interested in the confirmation status, you do not have to subscribe to the returned observable: |
||||
|
|
||||
|
```js |
||||
|
this.confirmation.error('You are not authorized.', 'Error'); |
||||
|
``` |
||||
|
|
||||
|
### How to Display a Confirmation Popup With Given Options |
||||
|
|
||||
|
Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: |
||||
|
|
||||
|
```js |
||||
|
const options: Partial<Confirmation.Options> = { |
||||
|
hideCancelBtn: false, |
||||
|
hideYesBtn: false, |
||||
|
cancelText: 'Close', |
||||
|
yesText: 'Confirm', |
||||
|
messageLocalizationParams: ['Demo'], |
||||
|
titleLocalizationParams: [], |
||||
|
}; |
||||
|
|
||||
|
this.confirmation.warn( |
||||
|
'AbpIdentity::RoleDeletionConfirmationMessage', |
||||
|
'Are you sure?', |
||||
|
options, |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false` |
||||
|
- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false` |
||||
|
- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel` |
||||
|
- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes` |
||||
|
- `messageLocalizationParams` is the interpolation parameters for the localization of the message. |
||||
|
- `titleLocalizationParams` is the interpolation parameters for the localization of the title. |
||||
|
|
||||
|
With the options above, the confirmation popup looks like this: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
You are able to pass in an HTML string as title, message, or button texts. Here is an example: |
||||
|
|
||||
|
```js |
||||
|
const options: Partial<Confirmation.Options> = { |
||||
|
yesText: '<i class="fa fa-trash mr-1"></i>Yes, delete it', |
||||
|
}; |
||||
|
|
||||
|
this.confirmation.warn( |
||||
|
` |
||||
|
<strong>Role Demo</strong> will be <strong>deleted</strong> |
||||
|
<br> |
||||
|
Do you confirm that? |
||||
|
`, |
||||
|
'<span class="my-custom-title">Are you sure?</span>', |
||||
|
options, |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
Since the values are HTML now, localization should be handled manually. Check out the [LocalizationService](./Localization#using-the-localization-service) to see how you can accomplish that. |
||||
|
|
||||
|
> Please note that all strings will be sanitized by Angular and not every HTML string will work. Only values that are considered as "safe" by Angular will be displayed. |
||||
|
|
||||
|
### How to Remove a Confirmation Popup |
||||
|
|
||||
|
The open confirmation popup can be removed manually via the `clear` method: |
||||
|
|
||||
|
```js |
||||
|
this.confirmation.clear(); |
||||
|
``` |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### success |
||||
|
|
||||
|
```js |
||||
|
success( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Confirmation.Options>, |
||||
|
): Observable<Confirmation.Status> |
||||
|
``` |
||||
|
|
||||
|
> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts) |
||||
|
|
||||
|
|
||||
|
### warn |
||||
|
|
||||
|
```js |
||||
|
warn( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Confirmation.Options>, |
||||
|
): Observable<Confirmation.Status> |
||||
|
``` |
||||
|
|
||||
|
### error |
||||
|
|
||||
|
```js |
||||
|
error( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Confirmation.Options>, |
||||
|
): Observable<Confirmation.Status> |
||||
|
``` |
||||
|
|
||||
|
### info |
||||
|
|
||||
|
```js |
||||
|
info( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Confirmation.Options>, |
||||
|
): Observable<Confirmation.Status> |
||||
|
``` |
||||
|
|
||||
|
### clear |
||||
|
|
||||
|
```js |
||||
|
clear( |
||||
|
status: Confirmation.Status = Confirmation.Status.dismiss |
||||
|
): void |
||||
|
``` |
||||
|
|
||||
|
- `status` parameter is the value of the confirmation closing event. |
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [Toast Overlay](./Toaster-Service.md) |
||||
@ -0,0 +1,101 @@ |
|||||
|
# ContainerStrategy |
||||
|
|
||||
|
`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public containerRef: ViewContainerRef, |
||||
|
private index?: number, // works only in InsertIntoContainerStrategy |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `containerRef` is the `ViewContainerRef` that will be used when projecting the content. |
||||
|
|
||||
|
|
||||
|
### getIndex |
||||
|
|
||||
|
```js |
||||
|
getIndex(): number |
||||
|
``` |
||||
|
|
||||
|
This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`. |
||||
|
|
||||
|
|
||||
|
### prepare |
||||
|
|
||||
|
```js |
||||
|
prepare(): void |
||||
|
``` |
||||
|
|
||||
|
This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop). |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ClearContainerStrategy |
||||
|
|
||||
|
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## InsertIntoContainerStrategy |
||||
|
|
||||
|
`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Container Strategies |
||||
|
|
||||
|
Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### Clear |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Clears given container before content projection. |
||||
|
|
||||
|
|
||||
|
### Append |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be appended to the container. |
||||
|
|
||||
|
|
||||
|
### Prepend |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be prepended to the container. |
||||
|
|
||||
|
|
||||
|
### Insert |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Insert( |
||||
|
containerRef: ViewContainerRef, |
||||
|
index: number, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`). |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [ProjectionStrategy](./Projection-Strategy.md) |
||||
@ -0,0 +1,78 @@ |
|||||
|
# Content Projection |
||||
|
|
||||
|
You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
```js |
||||
|
import { ContentProjectionService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private contentProjectionService: ContentProjectionService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project. |
||||
|
|
||||
|
### How to Project Components to Root Level |
||||
|
|
||||
|
If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context. |
||||
|
|
||||
|
```js |
||||
|
const strategy = PROJECTION_STRATEGY.AppendComponentToBody( |
||||
|
SomeOverlayComponent, |
||||
|
{ someOverlayProp: "SOME_VALUE" } |
||||
|
); |
||||
|
|
||||
|
const componentRef = this.contentProjectionService.projectContent(strategy); |
||||
|
``` |
||||
|
|
||||
|
In the example above, `SomeOverlayComponent` component will placed at the **end** of `<body>` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`. |
||||
|
|
||||
|
> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. |
||||
|
|
||||
|
### How to Project Components and Templates into a Container |
||||
|
|
||||
|
If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context. |
||||
|
|
||||
|
```js |
||||
|
const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( |
||||
|
SomeComponent, |
||||
|
viewContainerRefOfTarget, |
||||
|
{ someProp: "SOME_VALUE" } |
||||
|
); |
||||
|
|
||||
|
const componentRef = this.contentProjectionService.projectContent(strategy); |
||||
|
``` |
||||
|
|
||||
|
In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will be placed inside it. In addition, the given context will be applied and `someProp` of the component will be set to `SOME_VALUE`. |
||||
|
|
||||
|
> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary. |
||||
|
|
||||
|
Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### projectContent |
||||
|
|
||||
|
```js |
||||
|
projectContent<T extends Type<any> | TemplateRef<any>>( |
||||
|
projectionStrategy: ProjectionStrategy<T>, |
||||
|
injector = this.injector, |
||||
|
): ComponentRef<C> | EmbeddedViewRef<C> |
||||
|
``` |
||||
|
|
||||
|
- `projectionStrategy` parameter is the primary focus here and is explained above. |
||||
|
- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. |
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [TrackByService](./Track-By-Service.md) |
||||
@ -0,0 +1,74 @@ |
|||||
|
# ContentSecurityStrategy |
||||
|
|
||||
|
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor(public nonce?: string) |
||||
|
``` |
||||
|
|
||||
|
- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives. |
||||
|
|
||||
|
|
||||
|
### applyCSP |
||||
|
|
||||
|
```js |
||||
|
applyCSP(element: HTMLScriptElement | HTMLStyleElement): void |
||||
|
``` |
||||
|
|
||||
|
This method maps the aforementioned properties to the given `element`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## LooseContentSecurityPolicy |
||||
|
|
||||
|
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `<script>` or `<style>` tag with it. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## NoContentSecurityPolicy |
||||
|
|
||||
|
`NoContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Content Security Strategies |
||||
|
|
||||
|
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### Loose |
||||
|
|
||||
|
```js |
||||
|
CONTENT_SECURITY_STRATEGY.Loose(nonce: string) |
||||
|
``` |
||||
|
|
||||
|
`nonce` will be set. |
||||
|
|
||||
|
|
||||
|
### None |
||||
|
|
||||
|
```js |
||||
|
CONTENT_SECURITY_STRATEGY.None() |
||||
|
``` |
||||
|
|
||||
|
Nothing will be done. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
|
- [ContentStrategy](./Content-Strategy.md) |
||||
|
|
||||
@ -0,0 +1,95 @@ |
|||||
|
# ContentStrategy |
||||
|
|
||||
|
`ContentStrategy` is an abstract class exposed by @abp/ng.core package. It helps you create inline scripts or styles. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public content: string, |
||||
|
protected domStrategy?: DomStrategy, |
||||
|
protected contentSecurityStrategy?: ContentSecurityStrategy |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `content` is set to `<script>` and `<style>` elements as `textContent` property. |
||||
|
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_) |
||||
|
- `contentSecurityStrategy` is the `ContentSecurityStrategy` that will be used on the created element before inserting it. (_default: None_) |
||||
|
|
||||
|
Please refer to [DomStrategy](./Dom-Strategy.md) and [ContentSecurityStrategy](./Content-Security-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### createElement |
||||
|
|
||||
|
```js |
||||
|
createElement(): HTMLScriptElement | HTMLStyleElement |
||||
|
``` |
||||
|
|
||||
|
This method creates and returns a `<script>` or `<style>` element with `content` set as `textContent`. |
||||
|
|
||||
|
|
||||
|
### insertElement |
||||
|
|
||||
|
```js |
||||
|
insertElement(): void |
||||
|
``` |
||||
|
|
||||
|
This method creates and inserts a `<script>` or `<style>` element. |
||||
|
|
||||
|
|
||||
|
## ScriptContentStrategy |
||||
|
|
||||
|
`ScriptContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<script>` element to the DOM**. |
||||
|
|
||||
|
## StyleContentStrategy |
||||
|
|
||||
|
`StyleContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<style>` element to the DOM**. |
||||
|
|
||||
|
|
||||
|
## Predefined Content Strategies |
||||
|
|
||||
|
Predefined content strategies are accessible via `CONTENT_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendScriptToBody |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.AppendScriptToBody(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<script>` element with the given content and places it at the **end** of `<body>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendScriptToHead |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.AppendScriptToHead(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<script>` element with the given content and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendStyleToHead |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.AppendStyleToHead(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<style>` element with the given content and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### PrependStyleToHead |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.PrependStyleToHead(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<style>` element with the given content and places it at the **beginning** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
@ -0,0 +1,117 @@ |
|||||
|
# ContextStrategy |
||||
|
|
||||
|
`ContextStrategy` is an abstract class exposed by @abp/ng.core package. There are three context strategies extending it: `ComponentContextStrategy`, `TemplateContextStrategy`, and `NoContextStrategy`. Implementing the same methods and properties, all of these strategies help you define how projected content will get their context. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ComponentContextStrategy |
||||
|
|
||||
|
`ComponentContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected component**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor(public context: Partial<InferredInstanceOf<T>>) {} |
||||
|
``` |
||||
|
|
||||
|
- `T` refers to component type here, i.e. `Type<C>`. |
||||
|
- `InferredInstanceOf` is a utility type exposed by @abp/ng.core package. It infers component shape. |
||||
|
- `context` will be mapped to properties of the projected component. |
||||
|
|
||||
|
|
||||
|
### setContext |
||||
|
|
||||
|
```js |
||||
|
setContext(componentRef: ComponentRef<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>> |
||||
|
``` |
||||
|
|
||||
|
This method maps each prop of the context to the component property with the same name and calls change detection. It returns the context after mapping. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## TemplateContextStrategy |
||||
|
|
||||
|
`TemplateContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected template**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor(public context: Partial<InferredContextOf<T>>) {} |
||||
|
``` |
||||
|
|
||||
|
- `T` refers to template context type here, i.e. `TemplateRef<C>`. |
||||
|
- `InferredContextOf` is a utility type exposed by @abp/ng.core package. It infers context shape. |
||||
|
- `context` will be mapped to properties of the projected template. |
||||
|
|
||||
|
|
||||
|
### setContext |
||||
|
|
||||
|
```js |
||||
|
setContext(): Partial<InferredContextOf<T>> |
||||
|
``` |
||||
|
|
||||
|
This method does nothing and only returns the context, because template context is not mapped but passed in as parameter to `createEmbeddedView` method. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## NoContextStrategy |
||||
|
|
||||
|
`NoContextStrategy` is a class that extends `ContextStrategy`. It lets you **skip passing any context to projected content**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor() |
||||
|
``` |
||||
|
|
||||
|
Unlike other context strategies, `NoContextStrategy` contructor takes no parameters. |
||||
|
|
||||
|
|
||||
|
### setContext |
||||
|
|
||||
|
```js |
||||
|
setContext(): undefined |
||||
|
``` |
||||
|
|
||||
|
Since there is no context, this method gets no parameters and will return `undefined`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Context Strategies |
||||
|
|
||||
|
Predefined context strategies are accessible via `CONTEXT_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### None |
||||
|
|
||||
|
```js |
||||
|
CONTEXT_STRATEGY.None() |
||||
|
``` |
||||
|
|
||||
|
This strategy will not pass any context to the projected content. |
||||
|
|
||||
|
|
||||
|
### Component |
||||
|
|
||||
|
```js |
||||
|
CONTEXT_STRATEGY.Component(context: Partial<InferredContextOf<T>>) |
||||
|
``` |
||||
|
|
||||
|
This strategy will help you pass the given context to the projected component. |
||||
|
|
||||
|
|
||||
|
### Template |
||||
|
|
||||
|
```js |
||||
|
CONTEXT_STRATEGY.Template(context: Partial<InferredContextOf<T>>) |
||||
|
``` |
||||
|
|
||||
|
This strategy will help you pass the given context to the projected template. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [ProjectionStrategy](./Projection-Strategy.md) |
||||
@ -0,0 +1,60 @@ |
|||||
|
# CrossOriginStrategy |
||||
|
|
||||
|
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`. |
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public crossorigin: 'anonymous' | 'use-credentials', |
||||
|
public integrity?: string |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin). |
||||
|
- `integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity). |
||||
|
|
||||
|
|
||||
|
### setCrossOrigin |
||||
|
|
||||
|
```js |
||||
|
setCrossOrigin(element: HTMLElement): void |
||||
|
``` |
||||
|
|
||||
|
This method maps the aforementioned properties to the given `element`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Cross-Origin Strategies |
||||
|
|
||||
|
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### Anonymous |
||||
|
|
||||
|
```js |
||||
|
CROSS_ORIGIN_STRATEGY.Anonymous(integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
`crossorigin` will be set as `"anonymous"` and `integrity` is optional. |
||||
|
|
||||
|
|
||||
|
### UseCredentials |
||||
|
|
||||
|
```js |
||||
|
CROSS_ORIGIN_STRATEGY.UseCredentials(integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [LoadingStrategy](./Loading-Strategy.md) |
||||
@ -0,0 +1,140 @@ |
|||||
|
# Dom Insertion (of Scripts and Styles) |
||||
|
|
||||
|
You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `DomInsertionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
```js |
||||
|
import { DomInsertionService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private domInsertionService: DomInsertionService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `insertContent` method of `DomInsertionService` to create a `<script>` or `<style>` element with given content in the DOM at the desired position. There is also the `projectContent` method for dynamically rendering components and templates. |
||||
|
|
||||
|
### How to Insert Scripts |
||||
|
|
||||
|
The first parameter of `insertContent` method expects a `ContentStrategy`. If you pass a `ScriptContentStrategy` instance, the `DomInsertionService` will create a `<script>` element with given `content` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private domInsertionService: DomInsertionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
const scriptElement = this.domInsertionService.insertContent( |
||||
|
CONTENT_STRATEGY.AppendScriptToBody('alert()') |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In the example above, `<script>alert()</script>` element will place at the **end** of `<body>` and `scriptElement` will be an `HTMLScriptElement`. |
||||
|
|
||||
|
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
||||
|
|
||||
|
> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. |
||||
|
|
||||
|
### How to Insert Styles |
||||
|
|
||||
|
If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `<style>` element with given `content` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private domInsertionService: DomInsertionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
const styleElement = this.domInsertionService.insertContent( |
||||
|
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In the example above, `<style>body {margin: 0;}</style>` element will place at the **end** of `<head>` and `styleElement` will be an `HTMLStyleElement`. |
||||
|
|
||||
|
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
||||
|
|
||||
|
> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. |
||||
|
|
||||
|
### How to Remove Inserted Scripts & Styles |
||||
|
|
||||
|
If you pass the inserted `HTMLScriptElement` or `HTMLStyleElement` element as the first parameter of `removeContent` method, the `DomInsertionService` will remove the given element. |
||||
|
|
||||
|
```js |
||||
|
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
private styleElement: HTMLStyleElement; |
||||
|
|
||||
|
constructor(private domInsertionService: DomInsertionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.styleElement = this.domInsertionService.insertContent( |
||||
|
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.domInsertionService.removeContent(this.styleElement); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In the example above, `<style>body {margin: 0;}</style>` element **will be removed** from `<head>` when the component is destroyed. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### insertContent |
||||
|
|
||||
|
```js |
||||
|
insertContent<T extends HTMLScriptElement | HTMLStyleElement>( |
||||
|
contentStrategy: ContentStrategy<T>, |
||||
|
): T |
||||
|
``` |
||||
|
|
||||
|
- `contentStrategy` parameter is the primary focus here and is explained above. |
||||
|
- returns `HTMLScriptElement` or `HTMLStyleElement` based on given strategy. |
||||
|
|
||||
|
### removeContent |
||||
|
|
||||
|
```js |
||||
|
removeContent(element: HTMLScriptElement | HTMLStyleElement): void |
||||
|
``` |
||||
|
|
||||
|
- `element` parameter is the inserted `HTMLScriptElement` or `HTMLStyleElement` element, which was returned by `insertContent` method. |
||||
|
|
||||
|
### has |
||||
|
|
||||
|
```js |
||||
|
has(content: string): boolean |
||||
|
``` |
||||
|
|
||||
|
The `has` method returns a boolean value that indicates the given content has already been added to the DOM or not. |
||||
|
|
||||
|
- `content` parameter is the content of the inserted `HTMLScriptElement` or `HTMLStyleElement` element. |
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [ContentProjectionService](./Content-Projection-Service.md) |
||||
@ -0,0 +1,90 @@ |
|||||
|
# DomStrategy |
||||
|
|
||||
|
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`. |
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public target?: HTMLElement, |
||||
|
public position?: InsertPosition |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `target` is an HTMLElement (_default: document.head_). |
||||
|
- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_). |
||||
|
|
||||
|
|
||||
|
### insertElement |
||||
|
|
||||
|
```js |
||||
|
insertElement(element: HTMLElement): void |
||||
|
``` |
||||
|
|
||||
|
This method inserts given `element` to `target` based on the `position`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Dom Strategies |
||||
|
|
||||
|
Predefined dom strategies are accessible via `DOM_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendToBody |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.AppendToBody() |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` at the end of `<body>`. |
||||
|
|
||||
|
|
||||
|
### AppendToHead |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.AppendToHead() |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` at the end of `<head>`. |
||||
|
|
||||
|
|
||||
|
### PrependToHead |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.PrependToHead() |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` at the beginning of `<head>`. |
||||
|
|
||||
|
|
||||
|
### AfterElement |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.AfterElement(target: HTMLElement) |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` after (as a sibling to) the `target`. |
||||
|
|
||||
|
|
||||
|
### BeforeElement |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.BeforeElement(target: HTMLElement) |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` before (as a sibling to) the `target`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
|
- [LazyLoadService](./Lazy-Load-Service.md) |
||||
|
- [LoadingStrategy](./Loading-Strategy.md) |
||||
|
- [ContentStrategy](./Content-Strategy.md) |
||||
|
- [ProjectionStrategy](./Projection-Strategy.md) |
||||
@ -0,0 +1,209 @@ |
|||||
|
# How to Make HTTP Requests |
||||
|
|
||||
|
|
||||
|
|
||||
|
## About HttpClient |
||||
|
|
||||
|
Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project. |
||||
|
|
||||
|
However, `HttpClient` leaves error handling to the caller (method). In other words, HTTP errors are handled manually and by hooking into the observer of the `Observable` returned. |
||||
|
|
||||
|
```js |
||||
|
getConfig() { |
||||
|
this.http.get(this.configUrl).subscribe( |
||||
|
config => this.updateConfig(config), |
||||
|
error => { |
||||
|
// Handle error here |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Although clear and flexible, handling errors this way is repetitive work, even when error processing is delegated to the store or any other injectable. |
||||
|
|
||||
|
An `HttpInterceptor` is able to catch `HttpErrorResponse` and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## RestService |
||||
|
|
||||
|
ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Getting Started with RestService |
||||
|
|
||||
|
In order to use the `RestService`, you must inject it in your class as a dependency. |
||||
|
|
||||
|
```js |
||||
|
import { RestService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoService { |
||||
|
constructor(private rest: RestService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Make a Request with RestService |
||||
|
|
||||
|
You can use the `request` method of the `RestService` is for HTTP requests. Here is an example: |
||||
|
|
||||
|
```js |
||||
|
getFoo(id: number) { |
||||
|
const request: Rest.Request<null> = { |
||||
|
method: 'GET', |
||||
|
url: '/api/some/path/to/foo/' + id, |
||||
|
}; |
||||
|
|
||||
|
return this.rest.request<null, FooResponse>(request); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
The `request` method always returns an `Observable<T>`. Therefore you can do the following wherever you use `getFoo` method: |
||||
|
|
||||
|
```js |
||||
|
doSomethingWithFoo(id: number) { |
||||
|
this.demoService.getFoo(id).subscribe( |
||||
|
foo => { |
||||
|
// Do something with foo. |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error. |
||||
|
|
||||
|
|
||||
|
|
||||
|
As you see, `request` method gets a request options object with `Rest.Request<T>` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one: |
||||
|
|
||||
|
```js |
||||
|
postFoo(body: Foo) { |
||||
|
const request: Rest.Request<Foo> = { |
||||
|
method: 'POST', |
||||
|
url: '/api/some/path/to/foo', |
||||
|
body |
||||
|
}; |
||||
|
|
||||
|
return this.rest.request<Foo, FooResponse>(request); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request<T>` type, which has only a few chages compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Disable Default Error Handler of RestService |
||||
|
|
||||
|
The `request` method, used with defaults, always handles errors. Let's see how you can change that behavior and handle errors yourself: |
||||
|
|
||||
|
```js |
||||
|
deleteFoo(id: number) { |
||||
|
const request: Rest.Request<null> = { |
||||
|
method: 'DELETE', |
||||
|
url: '/api/some/path/to/foo/' + id, |
||||
|
}; |
||||
|
|
||||
|
return this.rest.request<null, void>(request, { skipHandleError: true }); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription. |
||||
|
|
||||
|
```js |
||||
|
removeFooFromList(id: number) { |
||||
|
this.demoService.deleteFoo(id).subscribe( |
||||
|
foo => { |
||||
|
// Do something with foo. |
||||
|
}, |
||||
|
error => { |
||||
|
// Do something with error. |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Get a Specific API Endpoint From Application Config |
||||
|
|
||||
|
Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration. |
||||
|
|
||||
|
|
||||
|
|
||||
|
```js |
||||
|
putFoo(body: Foo, id: string) { |
||||
|
const request: Rest.Request<Foo> = { |
||||
|
method: 'PUT', |
||||
|
url: '/' + id, |
||||
|
body |
||||
|
}; |
||||
|
|
||||
|
return this.rest.request<Foo, void>(request, {apiName: 'foo'}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows: |
||||
|
|
||||
|
```js |
||||
|
// environment.ts |
||||
|
|
||||
|
export const environment = { |
||||
|
apis: { |
||||
|
default: { |
||||
|
url: 'https://localhost:44305', |
||||
|
}, |
||||
|
foo: { |
||||
|
url: 'https://localhost:44305/api/some/path/to/foo', |
||||
|
}, |
||||
|
}, |
||||
|
|
||||
|
/* rest of the environment variables here */ |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Observe Response Object or HTTP Events Instead of Body |
||||
|
|
||||
|
`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object. |
||||
|
|
||||
|
```js |
||||
|
getSomeCustomHeaderValue() { |
||||
|
const request: Rest.Request<null> = { |
||||
|
method: 'GET', |
||||
|
url: '/api/some/path/that/sends/some-custom-header', |
||||
|
}; |
||||
|
|
||||
|
return this.rest.request<null, HttpResponse<any>>( |
||||
|
request, |
||||
|
{observe: Rest.Observe.Response}, |
||||
|
).pipe( |
||||
|
map(response => response.headers.get('Some-Custom-Header')) |
||||
|
); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). |
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
* [Localization](./Localization.md) |
||||
@ -0,0 +1,213 @@ |
|||||
|
# How to Lazy Load Scripts and Styles |
||||
|
|
||||
|
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<link>` element in the DOM at the desired position and force the browser to download the target resource. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Load Scripts |
||||
|
|
||||
|
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="libraryLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
libraryLoaded$ = this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'), |
||||
|
); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**. |
||||
|
|
||||
|
> You can subscribe multiple times in your template with `async` pipe. The Scripts will only be loaded once. |
||||
|
|
||||
|
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Load Styles |
||||
|
|
||||
|
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="stylesLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
stylesLoaded$ = this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'), |
||||
|
); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**. |
||||
|
|
||||
|
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. |
||||
|
|
||||
|
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Advanced Usage |
||||
|
|
||||
|
You have quite a bit of **freedom to define how your lazy load will work**. Here is an example: |
||||
|
|
||||
|
```js |
||||
|
const domStrategy = DOM_STRATEGY.PrependToHead(); |
||||
|
|
||||
|
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous( |
||||
|
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh', |
||||
|
); |
||||
|
|
||||
|
const loadingStrategy = new StyleLoadingStrategy( |
||||
|
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css', |
||||
|
domStrategy, |
||||
|
crossOriginStrategy, |
||||
|
); |
||||
|
|
||||
|
this.lazyLoad.load(loadingStrategy, 1, 2000); |
||||
|
``` |
||||
|
|
||||
|
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails. |
||||
|
|
||||
|
|
||||
|
A common usecase is **loading multiple scripts and/or styles before using a feature**: |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
import { frokJoin } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
private stylesLoaded$ = forkJoin( |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'), |
||||
|
), |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
private scriptsLoaded$ = forkJoin( |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'), |
||||
|
), |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
RxJS `forkJoin` will load all scripts and styles in parallel and emit only when all of them are loaded. So, when `<some-component>` is placed, all required dependencies will be available. |
||||
|
|
||||
|
> Noticed we have prepended styles to the document head? This is sometimes necessary, because your application styles may be overriding some of the library styles. In such a case, you must be careful about the order of prepended styles. They will be placed one-by-one and, **when prepending, the last one placed will be on top**. |
||||
|
|
||||
|
|
||||
|
Another frequent usecase is **loading dependent scripts in order**: |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
import { concat } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="scriptsLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
scriptsLoaded$ = concat( |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'), |
||||
|
), |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In this example, the second file needs the first one to be loaded beforehand. RxJS `concat` function will let you load all scripts one-by-one in the given order and emit only when all of them are loaded. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
|
||||
|
### loaded |
||||
|
|
||||
|
```js |
||||
|
loaded: Set<string> |
||||
|
``` |
||||
|
|
||||
|
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). |
||||
|
|
||||
|
|
||||
|
|
||||
|
### load |
||||
|
|
||||
|
```js |
||||
|
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event> |
||||
|
``` |
||||
|
|
||||
|
- `strategy` parameter is the primary focus here and is explained above. |
||||
|
- `retryTimes` defines how many times the loading will be tried again before fail (_default: 2_). |
||||
|
- `retryDelay` defines how much delay there will be between retries (_default: 1000_). |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
@ -0,0 +1,110 @@ |
|||||
|
# LoadingStrategy |
||||
|
|
||||
|
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. There are two loading strategies extending it: `ScriptLoadingStrategy` and `StyleLoadingStrategy`. Implementing the same methods and properties, both of these strategies help you define how your lazy loading will work. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public path: string, |
||||
|
protected domStrategy?: DomStrategy, |
||||
|
protected crossOriginStrategy?: CrossOriginStrategy |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute. |
||||
|
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_) |
||||
|
- `crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_) |
||||
|
|
||||
|
Please refer to [DomStrategy](./Dom-Strategy.md) and [CrossOriginStrategy](./Cross-Origin-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### createElement |
||||
|
|
||||
|
```js |
||||
|
createElement(): HTMLScriptElement | HTMLLinkElement |
||||
|
``` |
||||
|
|
||||
|
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`. |
||||
|
|
||||
|
|
||||
|
### createStream |
||||
|
|
||||
|
```js |
||||
|
createStream(): Observable<Event> |
||||
|
``` |
||||
|
|
||||
|
This method creates and returns an observable stream that emits on success and throws on error. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ScriptLoadingStrategy |
||||
|
|
||||
|
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a script**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## StyleLoadingStrategy |
||||
|
|
||||
|
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a style**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Loading Strategies |
||||
|
|
||||
|
Predefined loading strategies are accessible via `LOADING_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendAnonymousScriptToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead(src: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### PrependAnonymousScriptToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.PrependAnonymousScriptToHead(src: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendAnonymousScriptToBody |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToBody(src: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendAnonymousStyleToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.AppendAnonymousStyleToHead(href: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### PrependAnonymousStyleToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.PrependAnonymousStyleToHead(href: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [LazyLoadService](./Lazy-Load-Service.md) |
||||
@ -0,0 +1,199 @@ |
|||||
|
# Modifying the Menu |
||||
|
|
||||
|
The menu is inside the `ApplicationLayoutComponent` in the @abp/ng.theme.basic package. There are several methods for modifying the menu elements. This document covers these methods. If you would like to replace the menu completely, please refer to [Component Replacement documentation](./Component-Replacement.md) and learn how to replace a layout. |
||||
|
|
||||
|
|
||||
|
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.--> |
||||
|
|
||||
|
|
||||
|
## How to Add a Logo |
||||
|
|
||||
|
The `logoUrl` property in the environment variables is the url of the logo. |
||||
|
|
||||
|
You can add your logo to `src/assets` folder and set the `logoUrl` as shown below: |
||||
|
|
||||
|
```js |
||||
|
export const environment = { |
||||
|
// other configurations |
||||
|
application: { |
||||
|
name: 'MyProjectName', |
||||
|
logoUrl: 'assets/logo.png', |
||||
|
}, |
||||
|
// other configurations |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## How to Add a Navigation Element |
||||
|
|
||||
|
### Via `routes` Property in `AppRoutingModule` |
||||
|
|
||||
|
You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app-routing.module`. The `@abp/ng.core` package organizes your routes and stores them in the `ConfigState`. `ApplicationLayoutComponent` gets routes from store and displays them on the menu. |
||||
|
|
||||
|
You can add the `routes` property like below: |
||||
|
|
||||
|
```js |
||||
|
{ |
||||
|
path: 'your-path', |
||||
|
data: { |
||||
|
routes: { |
||||
|
name: 'Your navigation', |
||||
|
order: 3, |
||||
|
iconClass: 'fas fa-question-circle', |
||||
|
requiredPolicy: 'permission key here', |
||||
|
children: [ |
||||
|
{ |
||||
|
path: 'child', |
||||
|
name: 'Your child navigation', |
||||
|
order: 1, |
||||
|
requiredPolicy: 'permission key here', |
||||
|
}, |
||||
|
], |
||||
|
} as ABP.Route, // can be imported from @abp/ng.core |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `name` is the label of the navigation element. A localization key or a localization object can be passed. |
||||
|
- `order` is the order of the navigation element. |
||||
|
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label. |
||||
|
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md) |
||||
|
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property. |
||||
|
|
||||
|
After adding the `routes` property as described above, the navigation menu looks like this: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Via ConfigState |
||||
|
|
||||
|
The `dispatchAddRoute` method of `ConfigStateService` adds a new navigation element to the menu. |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
|
||||
|
const newRoute: ABP.Route = { |
||||
|
name: 'My New Page', |
||||
|
iconClass: 'fa fa-dashboard', |
||||
|
path: 'page', |
||||
|
invisible: false, |
||||
|
order: 2, |
||||
|
requiredPolicy: 'MyProjectName.MyNewPage', |
||||
|
} as Omit<ABP.Route, 'children'>; |
||||
|
|
||||
|
this.config.dispatchAddRoute(newRoute); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
The `newRoute` will be placed as at root level, i.e. without any parent routes, and its url will be stored as `'/path'`. |
||||
|
|
||||
|
If you want **to add a child route, you can do this:** |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
||||
|
|
||||
|
const newRoute: ABP.Route = { |
||||
|
parentName: eIdentityRouteNames.IdentityManagement, |
||||
|
name: 'My New Page', |
||||
|
iconClass: 'fa fa-dashboard', |
||||
|
path: 'page', |
||||
|
invisible: false, |
||||
|
order: 3, |
||||
|
requiredPolicy: 'MyProjectName.MyNewPage' |
||||
|
} as Omit<ABP.Route, 'children'>; |
||||
|
|
||||
|
this.config.dispatchAddRoute(newRoute); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
The `newRoute` will then be placed as a child of the parent route named `eIdentityRouteNames.IdentityManagement` and its url will be set as `'/identity/page'`. |
||||
|
|
||||
|
The new route will be added like below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## How to Patch a Navigation Element |
||||
|
|
||||
|
The `dispatchPatchRouteByName` method finds a route by its name and replaces its configuration in the store with the new configuration passed as the second parameter. |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
||||
|
|
||||
|
const newRouteConfig: Partial<ABP.Route> = { |
||||
|
iconClass: 'fas fa-home', |
||||
|
parentName: eIdentityRouteNames.Administration, |
||||
|
order: 0, |
||||
|
children: [ |
||||
|
{ |
||||
|
name: 'Dashboard', |
||||
|
path: 'dashboard', |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
* Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`. |
||||
|
* Added an icon. |
||||
|
* Specified the order. |
||||
|
* Added a child route named _Dashboard_. |
||||
|
|
||||
|
After the patch above, navigation elements looks like below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
|
||||
|
## How to Add an Element to Right Part of the Menu |
||||
|
|
||||
|
The right part elements are stored in the `LayoutState` that is in the @abp/ng.theme.basic package. |
||||
|
|
||||
|
The `dispatchAddNavigationElement` method of the `LayoutStateService` adds an element to the right part of the menu. |
||||
|
|
||||
|
You can insert an element by adding your template to `app.component` and calling the `dispatchAddNavigationElement` method: |
||||
|
|
||||
|
```js |
||||
|
import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-root', |
||||
|
template: ` |
||||
|
|
||||
|
<!-- Added below content --> |
||||
|
<ng-template #search |
||||
|
><input type="search" placeholder="Search" class="bg-transparent border-0" |
||||
|
/></ng-template> |
||||
|
`, |
||||
|
}) |
||||
|
export class AppComponent { |
||||
|
// Added ViewChild |
||||
|
@ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef<any>; |
||||
|
|
||||
|
constructor(private layout: LayoutStateService) {} // injected LayoutStateService |
||||
|
|
||||
|
// Added ngAfterViewInit |
||||
|
ngAfterViewInit() { |
||||
|
const newElement = { |
||||
|
name: 'Search', |
||||
|
element: this.searchElementRef, |
||||
|
order: 1, |
||||
|
} as Layout.NavigationElement; |
||||
|
|
||||
|
this.layout.dispatchAddNavigationElement(newElement); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
This inserts a search input to the menu. The final UI looks like below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## How to Remove an Element From Right Part of the Menu |
||||
|
|
||||
|
TODO |
||||
|
|
||||
|
|
||||
|
## What's Next |
||||
|
|
||||
|
* [Component Replacement](./Component-Replacement.md) |
||||
@ -0,0 +1,200 @@ |
|||||
|
# ProjectionStrategy |
||||
|
|
||||
|
`ProjectionStrategy` is an abstract class exposed by @abp/ng.core package. There are three projection strategies extending it: `ComponentProjectionStrategy`, `RootComponentProjectionStrategy`, and `TemplateProjectionStrategy`. Implementing the same methods and properties, all of these strategies help you define how your content projection will work. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ComponentProjectionStrategy |
||||
|
|
||||
|
`ComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into a container**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
component: T, |
||||
|
private containerStrategy: ContainerStrategy, |
||||
|
private contextStrategy?: ContextStrategy, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `component` is class of the component you would like to project. |
||||
|
- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. |
||||
|
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) |
||||
|
|
||||
|
Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### injectContent |
||||
|
|
||||
|
```js |
||||
|
injectContent(injector: Injector): ComponentRef<T> |
||||
|
``` |
||||
|
|
||||
|
This method prepares the container, resolves the component, sets its context, and projects it to the container. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## RootComponentProjectionStrategy |
||||
|
|
||||
|
`RootComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into the document**, such as appending it to `<body>`. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
component: T, |
||||
|
private contextStrategy?: ContextStrategy, |
||||
|
private domStrategy?: DomStrategy, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `component` is class of the component you would like to project. |
||||
|
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) |
||||
|
- `domStrategy` is the `DomStrategy` that will be used when inserting component. (_default: AppendToBody_) |
||||
|
|
||||
|
Please refer to [ContextStrategy](./Context-Strategy.md) and [DomStrategy](./Dom-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### injectContent |
||||
|
|
||||
|
```js |
||||
|
injectContent(injector: Injector): ComponentRef<T> |
||||
|
``` |
||||
|
|
||||
|
This method resolves the component, sets its context, and projects it to the document. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## TemplateProjectionStrategy |
||||
|
|
||||
|
`TemplateProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a template into a container**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
template: T, |
||||
|
private containerStrategy: ContainerStrategy, |
||||
|
private contextStrategy?: ContextStrategy, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `template` is `TemplateRef` you would like to project. |
||||
|
- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. |
||||
|
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) |
||||
|
|
||||
|
Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### injectContent |
||||
|
|
||||
|
```js |
||||
|
injectContent(): EmbeddedViewRef<T> |
||||
|
``` |
||||
|
|
||||
|
This method prepares the container, and projects the template together with the defined context to it. It returns an `EmbeddedViewRef`, which you should keep in order to clear projected templates later on. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Projection Strategies |
||||
|
|
||||
|
Predefined projection strategies are accessible via `PROJECTION_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendComponentToBody |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.AppendComponentToBody( |
||||
|
component: T, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the component and places it at the **end** of `<body>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendComponentToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.AppendComponentToContainer( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the component and places it at the **end** of the container. |
||||
|
|
||||
|
|
||||
|
### AppendTemplateToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.AppendTemplateToContainer( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the template and places it at the **end** of the container. |
||||
|
|
||||
|
|
||||
|
### PrependComponentToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.PrependComponentToContainer( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the component and places it at the **beginning** of the container. |
||||
|
|
||||
|
|
||||
|
### PrependTemplateToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.PrependTemplateToContainer( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the template and places it at the **beginning** of the container. |
||||
|
|
||||
|
|
||||
|
### ProjectComponentToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.ProjectComponentToContainer( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Clears the container, sets given context to the component, and places it **in the cleared** the container. |
||||
|
|
||||
|
|
||||
|
### ProjectTemplateToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.ProjectTemplateToContainer( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Clears the container, sets given context to the template, and places it **in the cleared** the container. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
@ -0,0 +1,67 @@ |
|||||
|
## Service Proxies |
||||
|
|
||||
|
It is common to call a REST endpoint in the server from our Angular applications. In this case, we generally create **services** (those have methods for each service method on the server side) and **model objects** (matches to [DTOs](../../Data-Transfer-Objects) in the server side). |
||||
|
|
||||
|
In addition to manually creating such server-interacting services, we could use tools like [NSWAG](https://github.com/RicoSuter/NSwag) to generate service proxies for us. But NSWAG has the following problems we've experienced: |
||||
|
|
||||
|
* It generates a **big, single** .ts file which has some problems; |
||||
|
* It get **too large** when your application grows. |
||||
|
* It doesn't fit into the **[modular](../../Module-Development-Basics) approach** of the ABP framework. |
||||
|
* It creates a bit **ugly code**. We want to have a clean code (just like if we write manually). |
||||
|
* It can not generate the same **method signature** declared in the server side (because swagger.json doesn't exactly reflect the method signature of the backend service). We've created an endpoint that exposes server side method contacts to allow clients generate a better aligned client proxies. |
||||
|
|
||||
|
ABP CLI `generate-proxies` command automatically generates the typescript client proxies by creating folders which separated by module names in the `src/app` folder. |
||||
|
Run the following command in the **root folder** of the angular application: |
||||
|
|
||||
|
```bash |
||||
|
abp generate-proxy |
||||
|
``` |
||||
|
|
||||
|
It only creates proxies only for your own application's services. It doesn't create proxies for the services of the application modules you're using (by default). There are several options. See the [CLI documentation](../../CLI). |
||||
|
|
||||
|
The files generated with the `--module all` option like below: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Services |
||||
|
|
||||
|
Each generated service matches a back-end controller. The services methods call back-end APIs via [RestService](./Http-Requests#restservice). |
||||
|
|
||||
|
A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's RemoteServiceName. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests#how-to-get-a-specific-api-endpoint-from-application-config) |
||||
|
|
||||
|
The `providedIn` property of the services is defined as `'root'`. Therefore no need to add a service as a provider to a module. You can use a service by injecting it into a constructor as shown below: |
||||
|
|
||||
|
```js |
||||
|
import { AbpApplicationConfigurationService } from '../app/shared/services'; |
||||
|
|
||||
|
//... |
||||
|
export class HomeComponent{ |
||||
|
constructor(private appConfigService: AbpApplicationConfigurationService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.appConfigService.get().subscribe() |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The Angular compiler removes the services that have not been injected anywhere from the final output. See the [tree-shakable providers documentation](https://angular.io/guide/dependency-injection-providers#tree-shakable-providers). |
||||
|
|
||||
|
### Models |
||||
|
|
||||
|
The generated models match the DTOs in the back-end. Each model is generated as a class under the `src/app/*/shared/models` folder. |
||||
|
|
||||
|
There are a few [base classes](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/dtos.ts) in the `@abp/ng.core` package. Some models extend these classes. |
||||
|
|
||||
|
A class instance can be created as shown below: |
||||
|
|
||||
|
```js |
||||
|
import { IdentityRoleCreateDto } from '../identity/shared/models'; |
||||
|
//... |
||||
|
const instance = new IdentityRoleCreateDto({name: 'Role 1', isDefault: false, isPublic: true}) |
||||
|
``` |
||||
|
|
||||
|
Initial values can optionally be passed to each class constructor. |
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
* [HTTP Requests](./Http-Requests) |
||||
@ -0,0 +1,158 @@ |
|||||
|
# Toast Overlay |
||||
|
|
||||
|
You can use the `ToasterService` in @abp/ng.theme.shared package to display messages in an overlay by placing at the root level in your project. |
||||
|
|
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
|
||||
|
```js |
||||
|
import { ToasterService } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private toaster: ToasterService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `success`, `warn`, `error`, and `info` methods of `ToasterService` to display an overlay. |
||||
|
|
||||
|
### How to Display a Toast Overlay |
||||
|
|
||||
|
```js |
||||
|
this.toast.success('Message', 'Title'); |
||||
|
``` |
||||
|
|
||||
|
- The `ToasterService` methods accept three parameters that are `message`, `title`, and `options`. |
||||
|
- `success`, `warn`, `error`, and `info` methods return the id of opened toast overlay. The toast can be removed with this id. |
||||
|
|
||||
|
### How to Display a Toast Overlay With Given Options |
||||
|
|
||||
|
Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods: |
||||
|
|
||||
|
```js |
||||
|
import { Toaster, ToasterService } from '@abp/ng.theme.shared'; |
||||
|
//... |
||||
|
|
||||
|
constructor(private toaster: ToasterService) {} |
||||
|
|
||||
|
//... |
||||
|
const options: Partial<Toaster.ToastOptions> = { |
||||
|
life: 10000, |
||||
|
sticky: false, |
||||
|
closable: true, |
||||
|
tapToDismiss: true, |
||||
|
messageLocalizationParams: ['Demo', '1'], |
||||
|
titleLocalizationParams: [] |
||||
|
}; |
||||
|
|
||||
|
this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options); |
||||
|
``` |
||||
|
|
||||
|
- `life` option is the closing time in milliseconds. Default value is `5000`. |
||||
|
- `sticky` option keeps toast overlay on the screen by ignoring the `life` option when `true`. Default value is `false`. |
||||
|
- `closable` option displays the close icon on the toast overlay when it is `true`. Default value is `true`. |
||||
|
- `tapToDismiss` option, when `true`, allows closing the toast overlay by clicking over it. Default value is `false`. |
||||
|
- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. |
||||
|
- `messageLocalizationParams` is the interpolation parameters for the localization of the message. |
||||
|
- `titleLocalizationParams` is the interpolation parameters for the localization of the title. |
||||
|
|
||||
|
With the options above, the toast overlay looks like this: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### How to Remove a Toast Overlay |
||||
|
|
||||
|
The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast: |
||||
|
|
||||
|
```js |
||||
|
const toastId = this.toast.success('Message', 'Title') |
||||
|
|
||||
|
this.toast.remove(toastId); |
||||
|
``` |
||||
|
|
||||
|
### How to Remove All Toasts |
||||
|
|
||||
|
The all open toasts can be removed manually via the `clear` method: |
||||
|
|
||||
|
```js |
||||
|
this.toast.clear(); |
||||
|
``` |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### success |
||||
|
|
||||
|
```js |
||||
|
success( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Toaster.ToastOptions>, |
||||
|
): number |
||||
|
``` |
||||
|
|
||||
|
- `Config` namespace can be imported from `@abp/ng.core`. |
||||
|
- `Toaster` namespace can be imported from `@abp/ng.theme.shared`. |
||||
|
|
||||
|
> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Toaster` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts) |
||||
|
|
||||
|
|
||||
|
### warn |
||||
|
|
||||
|
```js |
||||
|
warn( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Toaster.ToastOptions>, |
||||
|
): number |
||||
|
``` |
||||
|
|
||||
|
### error |
||||
|
|
||||
|
```js |
||||
|
error( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Toaster.ToastOptions>, |
||||
|
): number |
||||
|
``` |
||||
|
|
||||
|
### info |
||||
|
|
||||
|
```js |
||||
|
info( |
||||
|
message: Config.LocalizationParam, |
||||
|
title: Config.LocalizationParam, |
||||
|
options?: Partial<Toaster.ToastOptions>, |
||||
|
): number |
||||
|
``` |
||||
|
|
||||
|
### remove |
||||
|
|
||||
|
```js |
||||
|
remove(id: number): void |
||||
|
``` |
||||
|
|
||||
|
Removes an open toast by the given id. |
||||
|
|
||||
|
### clear |
||||
|
|
||||
|
```js |
||||
|
clear(): void |
||||
|
``` |
||||
|
|
||||
|
Removes all open toasts. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [Confirmation Popup](./Confirmation-Service.md) |
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [Config State](./Config-State.md) |
||||
@ -0,0 +1,113 @@ |
|||||
|
# Easy TrackByFunction Implementation |
||||
|
|
||||
|
`TrackByService` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `TrackByService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components. For better type support, you may pass in the type of the iterated item to it. |
||||
|
|
||||
|
```js |
||||
|
import { TrackByService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
list: Item[]; |
||||
|
|
||||
|
constructor(public readonly track: TrackByService<Item>) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
> Noticed `track` is `public` and `readonly`? That is because we will see some examples where methods of `TrackByService` instance are directly called in the component's template. That may be considered as an anti-pattern, but it has its own advantage, especially when component inheritance is leveraged. You can always use public component properties instead. |
||||
|
|
||||
|
|
||||
|
|
||||
|
**The members are also exported as separate functions.** If you do not want to inject `TrackByService`, you can always import and use those functions directly in your classes. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
There are two approaches available. |
||||
|
|
||||
|
1. You may inject `TrackByService` to your component and use its members. |
||||
|
2. You may use exported higher-order functions directly on component properties. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Track Items by a Key |
||||
|
|
||||
|
You can use `by` to get a `TrackByFunction` that tracks the iterated object based on one of its keys. For type support, you may pass in the type of the iterated item to it. |
||||
|
|
||||
|
```html |
||||
|
<!-- template of DemoComponent --> |
||||
|
|
||||
|
<div *ngFor="let item of list; trackBy: track.by('id')">{%{{{ item.name }}}%}</div> |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
`by` is exported as a stand-alone function and is named `trackBy`. |
||||
|
|
||||
|
```js |
||||
|
import { trackBy } from "@abp/ng.core"; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<div |
||||
|
*ngFor="let item of list; trackBy: trackById" |
||||
|
> |
||||
|
{%{{{ item.name }}}%} |
||||
|
</div> |
||||
|
`, |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
list: Item[]; |
||||
|
|
||||
|
trackById = trackBy<Item>('id'); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Track by a Deeply Nested Key |
||||
|
|
||||
|
You can use `byDeep` to get a `TrackByFunction` that tracks the iterated object based on a deeply nested key. For type support, you may pass in the type of the iterated item to it. |
||||
|
|
||||
|
```html |
||||
|
<!-- template of DemoComponent --> |
||||
|
|
||||
|
<div |
||||
|
*ngFor="let item of list; trackBy: track.byDeep('tenant', 'account', 'id')" |
||||
|
> |
||||
|
{%{{{ item.tenant.name }}}%} |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
`byDeep` is exported as a stand-alone function and is named `trackByDeep`. |
||||
|
|
||||
|
```js |
||||
|
import { trackByDeep } from "@abp/ng.core"; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<div |
||||
|
*ngFor="let item of list; trackBy: trackByTenantAccountId" |
||||
|
> |
||||
|
{%{{{ item.name }}}%} |
||||
|
</div> |
||||
|
`, |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
list: Item[]; |
||||
|
|
||||
|
trackByTenantAccountId = trackByDeep<Item>('tenant', 'account', 'id'); |
||||
|
} |
||||
|
``` |
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 20 KiB |
@ -1,3 +1,277 @@ |
|||||
## Dynamic Forms |
# Dynamic Forms |
||||
|
|
||||
|
`Warning:` Before getting into this document, be sure that you have clearly understood [abp form elements](Form-elements.md) document. |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
`abp-dynamic-form` creates a bootstrap form for a given c# model. |
||||
|
|
||||
|
Basic usage: |
||||
|
|
||||
|
````xml |
||||
|
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> |
||||
|
```` |
||||
|
Model: |
||||
|
````csharp |
||||
|
public class DynamicFormsModel : PageModel |
||||
|
{ |
||||
|
[BindProperty] |
||||
|
public DetailedModel MyDetailedModel { get; set; } |
||||
|
|
||||
|
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> |
||||
|
{ |
||||
|
new SelectListItem { Value = "CA", Text = "Canada"}, |
||||
|
new SelectListItem { Value = "US", Text = "USA"}, |
||||
|
new SelectListItem { Value = "UK", Text = "United Kingdom"}, |
||||
|
new SelectListItem { Value = "RU", Text = "Russia"} |
||||
|
}; |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
MyDetailedModel = new DetailedModel |
||||
|
{ |
||||
|
Name = "", |
||||
|
Description = "Lorem ipsum dolor sit amet.", |
||||
|
IsActive = true, |
||||
|
Age = 65, |
||||
|
Day = DateTime.Now, |
||||
|
MyCarType = CarType.Coupe, |
||||
|
YourCarType = CarType.Sedan, |
||||
|
Country = "RU", |
||||
|
NeighborCountries = new List<string>() { "UK", "CA" } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public class DetailedModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[Placeholder("Enter your name...")] |
||||
|
[Display(Name = "Name")] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[TextArea(Rows = 4)] |
||||
|
[Display(Name = "Description")] |
||||
|
[InputInfoText("Describe Yourself")] |
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[DataType(DataType.Password)] |
||||
|
[Display(Name = "Password")] |
||||
|
public string Password { get; set; } |
||||
|
|
||||
|
[Display(Name = "Is Active")] |
||||
|
public bool IsActive { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[Display(Name = "Age")] |
||||
|
public int Age { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[Display(Name = "My Car Type")] |
||||
|
public CarType MyCarType { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[AbpRadioButton(Inline = true)] |
||||
|
[Display(Name = "Your Car Type")] |
||||
|
public CarType YourCarType { get; set; } |
||||
|
|
||||
|
[DataType(DataType.Date)] |
||||
|
[Display(Name = "Day")] |
||||
|
public DateTime Day { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
[Display(Name = "Country")] |
||||
|
public string Country { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
[Display(Name = "Neighbor Countries")] |
||||
|
public List<string> NeighborCountries { get; set; } |
||||
|
} |
||||
|
|
||||
|
public enum CarType |
||||
|
{ |
||||
|
Sedan, |
||||
|
Hatchback, |
||||
|
StationWagon, |
||||
|
Coupe |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
## Demo |
||||
|
|
||||
|
See the [dynamic forms demo page](https://bootstrap-taghelpers.abp.io/Components/DynamicForms) to see it in action. |
||||
|
|
||||
|
## Attributes |
||||
|
|
||||
|
### abp-model |
||||
|
|
||||
|
Sets the c# model for dynamic form. Properties of this modal are turned into inputs in the form. |
||||
|
|
||||
|
### submit-button |
||||
|
|
||||
|
Can be `True` or `False`. |
||||
|
|
||||
|
If `True`, a submit button will be generated at the bottom of the form. |
||||
|
|
||||
|
Default value is `False`. |
||||
|
|
||||
|
### required-symbols |
||||
|
|
||||
|
Can be `True` or `False`. |
||||
|
|
||||
|
If `True`, required inputs will have a symbol (*) that indicates they are required. |
||||
|
|
||||
|
Default value is `True`. |
||||
|
|
||||
|
## Form Content Placement |
||||
|
|
||||
|
By default, `abp-dynamicform` clears the inner html and places the inputs into itself. If you want to add additional content to dynamic form or place the inputs to some specific area, you can use ` <abp-form-content />` tag. This tag will be replaced by form content and rest of the inner html of `abp-dynamic-form` tag will be unchanged. |
||||
|
|
||||
|
Usage: |
||||
|
|
||||
|
````xml |
||||
|
<abp-dynamic-form abp-model="@Model.MyExampleModel"> |
||||
|
<div> |
||||
|
Some content.... |
||||
|
</div> |
||||
|
<div class="input-area"> |
||||
|
<abp-form-content /> |
||||
|
</div> |
||||
|
<div> |
||||
|
Some more content.... |
||||
|
</div> |
||||
|
</abp-dynamic-form> |
||||
|
```` |
||||
|
|
||||
|
## Input Order |
||||
|
|
||||
|
`abp-dynamic-form` orders the properties by their `DisplayOrder` attribute and then their property order in model class. |
||||
|
|
||||
|
Default `DisplayOrder` attribute number is 10000 for every property. |
||||
|
|
||||
|
See example below: |
||||
|
|
||||
|
````csharp |
||||
|
public class OrderExampleModel |
||||
|
{ |
||||
|
[DisplayOrder(10004)] |
||||
|
public string Name{ get; set; } |
||||
|
|
||||
|
[DisplayOrder(10005)] |
||||
|
public string Surname{ get; set; } |
||||
|
|
||||
|
//Default 10000 |
||||
|
public string EmailAddress { get; set; } |
||||
|
|
||||
|
[DisplayOrder(10003)] |
||||
|
public string PhoneNumber { get; set; } |
||||
|
|
||||
|
[DisplayOrder(9999)] |
||||
|
public string City { get; set; } |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
In this example, input fields will be displayed with this order: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`. |
||||
|
|
||||
|
## Ignoring a property |
||||
|
|
||||
|
By default, `abp-dynamic-form` generates input for every property in model class. If you want to ignore a property, use `DynamicFormIgnore` attribute. |
||||
|
|
||||
|
See example below: |
||||
|
|
||||
|
````csharp |
||||
|
public class MyModel |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[DynamicFormIgnore] |
||||
|
public string Surname { get; set; } |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
In this example, no input will be generated for `Surname` property. |
||||
|
|
||||
|
## Indicating Text box, Radio Group and Combobox |
||||
|
|
||||
|
If you have read the [Form elements document](Form-elements.md), you noticed that `abp-radio` and `abp-select` tags are very similar on c# model. So we have to use `[AbpRadioButton()]` attribute to tell `abp-dynamic-form` which of your properties will be radio group and which will be combobox. See example below: |
||||
|
|
||||
|
````xml |
||||
|
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/> |
||||
|
```` |
||||
|
Model: |
||||
|
````csharp |
||||
|
public class DynamicFormsModel : PageModel |
||||
|
{ |
||||
|
[BindProperty] |
||||
|
public DetailedModel MyDetailedModel { get; set; } |
||||
|
|
||||
|
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem> |
||||
|
{ |
||||
|
new SelectListItem { Value = "CA", Text = "Canada"}, |
||||
|
new SelectListItem { Value = "US", Text = "USA"}, |
||||
|
new SelectListItem { Value = "UK", Text = "United Kingdom"}, |
||||
|
new SelectListItem { Value = "RU", Text = "Russia"} |
||||
|
}; |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
MyDetailedModel = new DetailedModel |
||||
|
{ |
||||
|
ComboCarType = CarType.Coupe, |
||||
|
RadioCarType = CarType.Sedan, |
||||
|
ComboCountry = "RU", |
||||
|
RadioCountry = "UK" |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public class DetailedModel |
||||
|
{ |
||||
|
public CarType ComboCarType { get; set; } |
||||
|
|
||||
|
[AbpRadioButton(Inline = true)] |
||||
|
public CarType RadioCarType { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
public string ComboCountry { get; set; } |
||||
|
|
||||
|
[AbpRadioButton()] |
||||
|
[SelectItems(nameof(CountryList))] |
||||
|
public string RadioCountry { get; set; } |
||||
|
} |
||||
|
|
||||
|
public enum CarType |
||||
|
{ |
||||
|
Sedan, |
||||
|
Hatchback, |
||||
|
StationWagon, |
||||
|
Coupe |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
As you see in example above: |
||||
|
|
||||
|
* If `[AbpRadioButton()]` are used on a **Enum** property, it will be a radio group. Otherwise, combobox. |
||||
|
* If `[SelectItems()]` and `[AbpRadioButton()]` are used on a property, it will be a radio group. |
||||
|
* If just `[SelectItems()]` is used on a property, it will be a combobox. |
||||
|
* If none of these attributes are used on a property, it will be a text box. |
||||
|
|
||||
|
## Localization |
||||
|
|
||||
|
`abp-dynamic-form` handles localization as well. |
||||
|
|
||||
|
By default, it will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys and set the localization value as input label. |
||||
|
|
||||
|
You can set it yourself by using `[Display()]` attribute of Asp.Net Core. You can use a localization key in this attribute. See example below: |
||||
|
|
||||
|
````csharp |
||||
|
[Display(Name = "Name")] |
||||
|
public string Name { get; set; } |
||||
|
```` |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. |
|
||||
@ -0,0 +1,261 @@ |
|||||
|
# Form Elements |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
Abp provides form input tag helpers to make building forms easier. |
||||
|
|
||||
|
## Demo |
||||
|
|
||||
|
See the [form elements demo page](https://bootstrap-taghelpers.abp.io/Components/FormElements) to see it in action. |
||||
|
|
||||
|
## abp-input |
||||
|
|
||||
|
`abp-input` tag creates a Bootstrap form input for a given c# property. It uses [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper) in background, so every data annotation attribute of `input` tag helper of Asp.Net Core is also valid for `abp-input`. |
||||
|
|
||||
|
Usage: |
||||
|
|
||||
|
````xml |
||||
|
<abp-input asp-for="@Model.MyModel.Name"/> |
||||
|
<abp-input asp-for="@Model.MyModel.Description"/> |
||||
|
<abp-input asp-for="@Model.MyModel.Password"/> |
||||
|
<abp-input asp-for="@Model.MyModel.IsActive"/> |
||||
|
```` |
||||
|
|
||||
|
Model: |
||||
|
|
||||
|
````csharp |
||||
|
public class FormElementsModel : PageModel |
||||
|
{ |
||||
|
public SampleModel MyModel { get; set; } |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
MyModel = new SampleModel(); |
||||
|
} |
||||
|
|
||||
|
public class SampleModel |
||||
|
{ |
||||
|
[Required] |
||||
|
[Placeholder("Enter your name...")] |
||||
|
[InputInfoText("What is your name?")] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[FormControlSize(AbpFormControlSize.Large)] |
||||
|
public string SurName { get; set; } |
||||
|
|
||||
|
[TextArea(Rows = 4)] |
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
[DataType(DataType.Password)] |
||||
|
public string Password { get; set; } |
||||
|
|
||||
|
public bool IsActive { get; set; } |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
### Attributes |
||||
|
|
||||
|
You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes. |
||||
|
|
||||
|
#### Property Attributes |
||||
|
|
||||
|
- `[TextArea()]`: Converts the input into a text area. |
||||
|
|
||||
|
* `[Placeholder()]`: Sets placeholder for input. You can use a localization key directly. |
||||
|
* `[InputInfoText()]`: Sets a small info text for input. You can use a localization key directly. |
||||
|
* `[FormControlSize()]`: Sets size of form-control wrapper element. Available values are |
||||
|
- `AbpFormControlSize.Default` |
||||
|
- `AbpFormControlSize.Small` |
||||
|
- `AbpFormControlSize.Medium` |
||||
|
- `AbpFormControlSize.Large` |
||||
|
* `[DisabledInput]` : Input is disabled. |
||||
|
* `[ReadOnlyInput]`: Input is read-only. |
||||
|
|
||||
|
#### Tag Attributes |
||||
|
|
||||
|
* `info`: Sets a small info text for input. You can use a localization key directly. |
||||
|
* `auto-focus`: If true, browser auto focuses on the element. |
||||
|
* `size`: Sets size of form-control wrapper element. Available values are |
||||
|
- `AbpFormControlSize.Default` |
||||
|
- `AbpFormControlSize.Small` |
||||
|
- `AbpFormControlSize.Medium` |
||||
|
- `AbpFormControlSize.Large` |
||||
|
* `disabled`: Input is disabled. |
||||
|
* `readonly`: Input is read-only. |
||||
|
* `label`: Sets the label for input. |
||||
|
* `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`. |
||||
|
|
||||
|
### Label & Localization |
||||
|
|
||||
|
You can set label of your input in different ways: |
||||
|
|
||||
|
- You can use `Label` attribute and directly set the label. But it doesn't auto localize your localization key. So use it as `label="@L["{LocalizationKey}"].Value"`. |
||||
|
- You can set it using `[Display(name="{LocalizationKey}")]` attribute of Asp.Net Core. |
||||
|
- You can just let **abp** find the localization key for the property. It will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys, if `label` or `[DisplayName]` attributes are not set. |
||||
|
|
||||
|
## abp-select |
||||
|
|
||||
|
`abp-select` tag creates a Bootstrap form select for a given c# property. It uses [Asp.Net Core Select Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-select-tag-helper) in background, so every data annotation attribute of `select` tag helper of Asp.Net Core is also valid for `abp-select`. |
||||
|
|
||||
|
`abp-select` tag needs a list of `Microsoft.AspNetCore.Mvc.Rendering.SelectListItem ` to work. It can be provided by `asp-items` attriube on the tag or `[SelectItems()]` attribute on c# property. (if you are using [abp-dynamic-form](Dynamic-forms.md), c# attribute is the only way.) |
||||
|
|
||||
|
`abp-select` supports multiple selection. |
||||
|
|
||||
|
`abp-select` auto-creates a select list for **Enum** properties. No extra data is needed. If property is nullable, an empty key and value is added to top of the auto-generated list. |
||||
|
|
||||
|
Usage: |
||||
|
|
||||
|
````xml |
||||
|
<abp-select asp-for="@Model.MyModel.City" asp-items="@Model.CityList"/> |
||||
|
|
||||
|
<abp-select asp-for="@Model.MyModel.AnotherCity"/> |
||||
|
|
||||
|
<abp-select asp-for="@Model.MyModel.MultipleCities" asp-items="@Model.CityList"/> |
||||
|
|
||||
|
<abp-select asp-for="@Model.MyModel.MyCarType"/> |
||||
|
|
||||
|
<abp-select asp-for="@Model.MyModel.MyNullableCarType"/> |
||||
|
```` |
||||
|
|
||||
|
Model: |
||||
|
|
||||
|
````csharp |
||||
|
public class FormElementsModel : PageModel |
||||
|
{ |
||||
|
public SampleModel MyModel { get; set; } |
||||
|
|
||||
|
public List<SelectListItem> CityList { get; set; } |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
MyModel = new SampleModel(); |
||||
|
|
||||
|
CityList = new List<SelectListItem> |
||||
|
{ |
||||
|
new SelectListItem { Value = "NY", Text = "New York"}, |
||||
|
new SelectListItem { Value = "LDN", Text = "London"}, |
||||
|
new SelectListItem { Value = "IST", Text = "Istanbul"}, |
||||
|
new SelectListItem { Value = "MOS", Text = "Moscow"} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public class SampleModel |
||||
|
{ |
||||
|
public string City { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CityList))] |
||||
|
public string AnotherCity { get; set; } |
||||
|
|
||||
|
public List<string> MultipleCities { get; set; } |
||||
|
|
||||
|
public CarType MyCarType { get; set; } |
||||
|
|
||||
|
public CarType? MyNullableCarType { get; set; } |
||||
|
} |
||||
|
|
||||
|
public enum CarType |
||||
|
{ |
||||
|
Sedan, |
||||
|
Hatchback, |
||||
|
StationWagon, |
||||
|
Coupe |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
### Attributes |
||||
|
|
||||
|
You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes. |
||||
|
|
||||
|
#### Property Attributes |
||||
|
|
||||
|
* `[SelectItems()]`: Sets the select data. Parameter should be the name of the data list. (see example above) |
||||
|
|
||||
|
- `[InputInfoText()]`: Sets a small info text for input. You can use a localization key directly. |
||||
|
- `[FormControlSize()]`: Sets size of form-control wrapper element. Available values are |
||||
|
- `AbpFormControlSize.Default` |
||||
|
- `AbpFormControlSize.Small` |
||||
|
- `AbpFormControlSize.Medium` |
||||
|
- `AbpFormControlSize.Large` |
||||
|
|
||||
|
#### Tag Attributes |
||||
|
|
||||
|
- `asp-items`: Sets the select data. This Should be a list of SelectListItem. |
||||
|
- `info`: Sets a small info text for input. You can use a localization key directly. |
||||
|
- `size`: Sets size of form-control wrapper element. Available values are |
||||
|
- `AbpFormControlSize.Default` |
||||
|
- `AbpFormControlSize.Small` |
||||
|
- `AbpFormControlSize.Medium` |
||||
|
- `AbpFormControlSize.Large` |
||||
|
- `label`: Sets the label for input. |
||||
|
- `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`. |
||||
|
|
||||
|
### Label & Localization |
||||
|
|
||||
|
You can set label of your input in different ways: |
||||
|
|
||||
|
- You can use `Label` attribute and directly set the label. But it doesn't auto localize your localization key. So use it as `label="@L["{LocalizationKey}"].Value".` |
||||
|
- You can set it using `[Display(name="{LocalizationKey}")]` attribute of Asp.Net Core. |
||||
|
- You can just let **abp** find the localization key for the property. It will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys. |
||||
|
|
||||
|
Localizations of combobox values are set by `abp-select` for **Enum** property. It searches for "{EnumTypeName}.{EnumPropertyName}" or "{EnumPropertyName}" localization keys. For instance, in the example above, it will use "CarType.StationWagon" or "StationWagon" keys for localization when it localizes combobox values. |
||||
|
|
||||
|
## abp-radio |
||||
|
|
||||
|
`abp-radio` tag creates a Bootstrap form radio group for a given c# property. Usage is very similar to `abp-select` tag. |
||||
|
|
||||
|
Usage: |
||||
|
|
||||
|
````xml |
||||
|
<abp-radio asp-for="@Model.MyModel.CityRadio" asp-items="@Model.CityList" inline="true"/> |
||||
|
|
||||
|
<abp-radio asp-for="@Model.MyModel.CityRadio2"/> |
||||
|
```` |
||||
|
|
||||
|
Model: |
||||
|
|
||||
|
````csharp |
||||
|
public class FormElementsModel : PageModel |
||||
|
{ |
||||
|
public SampleModel MyModel { get; set; } |
||||
|
|
||||
|
public List<SelectListItem> CityList { get; set; } = new List<SelectListItem> |
||||
|
{ |
||||
|
new SelectListItem { Value = "NY", Text = "New York"}, |
||||
|
new SelectListItem { Value = "LDN", Text = "London"}, |
||||
|
new SelectListItem { Value = "IST", Text = "Istanbul"}, |
||||
|
new SelectListItem { Value = "MOS", Text = "Moscow"} |
||||
|
}; |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
MyModel = new SampleModel(); |
||||
|
MyModel.CityRadio = "IST"; |
||||
|
MyModel.CityRadio2 = "MOS"; |
||||
|
} |
||||
|
|
||||
|
public class SampleModel |
||||
|
{ |
||||
|
public string CityRadio { get; set; } |
||||
|
|
||||
|
[SelectItems(nameof(CityList))] |
||||
|
public string CityRadio2 { get; set; } |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
### Attributes |
||||
|
|
||||
|
You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes. |
||||
|
|
||||
|
#### Property Attributes |
||||
|
|
||||
|
- `[SelectItems()]`: Sets the select data. Parameter should be the name of the data list. (see example above) |
||||
|
|
||||
|
#### Tag Attributes |
||||
|
|
||||
|
- `asp-items`: Sets the select data. This Should be a list of SelectListItem. |
||||
|
- `Inline`: If true, radio buttons will be in single line, next to each other. If false, they will be under each other. |
||||
@ -0,0 +1,42 @@ |
|||||
|
# Popovers |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
`abp-popover` is the abp tag for popover messages. |
||||
|
|
||||
|
Basic usage: |
||||
|
|
||||
|
````xml |
||||
|
<abp-button abp-popover="Hi, i'm popover content!"> |
||||
|
Popover Default |
||||
|
</abp-button> |
||||
|
```` |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Demo |
||||
|
|
||||
|
See the [popovers demo page](https://bootstrap-taghelpers.abp.io/Components/Popovers) to see it in action. |
||||
|
|
||||
|
## Attributes |
||||
|
|
||||
|
### disabled |
||||
|
|
||||
|
A value indicates if the element should be disabled for interaction. If this value is set to `true`, `dismissable` attribute will be ignored. Should be one of the following values: |
||||
|
|
||||
|
* `false` (default value) |
||||
|
* `true` |
||||
|
|
||||
|
### dismissable |
||||
|
|
||||
|
A value indicates to dismiss the popovers on the user's next click of a different element than the toggle element. Should be one of the following values: |
||||
|
|
||||
|
* `false` (default value) |
||||
|
* `true` |
||||
|
|
||||
|
### hoverable |
||||
|
|
||||
|
A value indicates if the popover content will be displayed on mouse hover. Should be one of the following values: |
||||
|
|
||||
|
* `false` (default value) |
||||
|
* `true` |
||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 45 KiB |