references/adaptor-decision-gate.md
# Adaptor Decision Gate — Human Approval Required
## ⚠️ MANDATORY HUMAN GATE: Adaptor Selection
**This gate is NON-NEGOTIABLE and MUST be triggered whenever a user provides a service URL without explicitly naming the adaptor type.**
If the agent cannot determine with 100% certainty which adaptor to use from the user's request, it **MUST STOP**, present this gate, and **wait for explicit user confirmation** before generating any `DataManager` code.
---
## Trigger Conditions
Trigger this gate when the user prompt matches ANY of the following:
| Condition | Example Prompt |
|-----------|---------------|
| Provides a URL with no adaptor keyword | `"Use this service URL: http://myapi.company.com/employees"` |
| Says "REST API" without specifying format | `"Connect to my REST API at http://myapi.company.com"` |
| Uses generic terms: "service", "endpoint", "backend" | `"Build an employee app using this service (http://myurl)"` |
| URL path is ambiguous (`/api/`, `/data/`, `/svc/`) | `"http://myserver/api/employees"` |
| User says "I have a service URL" | `"Can you create a simple employee DB using this service URL (http://customurl)"` |
| No mention of OData, GraphQL, Web API protocol | Any URL without protocol context |
| Ambiguous about data source type | `"Build a grid that handles filtering and sorting"` (without specifying HTTP endpoint or local data) |
| Mentions "full control over data operations" | `"I need to manage paging, sorting, and filtering in my component"` |
| Mentions `dataStateChange` or `dataSourceChanged` without context | `"Handle dataStateChange event for my grid"` |
| User provides a **local data file URL** (JSON file, not API endpoint) | `"Use this data: https://example.com/data.json"` (static file, not dynamic endpoint) |
---
## Gate Presentation Template
When this gate is triggered, respond with **exactly** this format before generating any code:
---
### 🛑 Adaptor Selection — Human Approval Required
I need your confirmation before I can configure `DataManager` for your service URL.
**Service URL provided:** `{USER_PROVIDED_URL}`
To generate the correct data binding code, I need to know which adaptor matches your backend service. Please select one:
| Option | Adaptor | When to Choose |
|--------|---------|----------------|
| **A** | `ODataV4Adaptor` | Your service is an **OData v4** endpoint (URL contains `/odata/`, uses `$filter`, `$top`, `$skip`, `[EnableQuery]` on controller, returns `@odata.count`) |
| **B** | `ODataAdaptor` | Your service is an **OData v3** endpoint (older OData protocol, URL may reference `/Northwind.svc/` style paths) |
| **C** | `WebApiAdaptor` | Your service is an **ASP.NET Web API** that returns `{ Items: [...], Count: N }` and accepts OData-style query strings (`$filter`, `$orderby`, `$top`, `$skip`) via GET |
| **D** | `UrlAdaptor` | Your service is a **custom REST API** (POST-based) that returns `{ result: [...], count: N }` lowercase format, with full control over filtering/sorting/paging server-side |
| **E** | `GraphQLAdaptor` | Your service is a **GraphQL** endpoint (uses queries/mutations, not REST) |
| **F** | `CustomAdaptor` | Your service is **non-HTTP** (in-memory, gRPC, SignalR, Entity Framework direct) or has unique logic that none of the above handle |
| **G** | `Custom Binding` | When you need the grid to fetch data on demand from any backend API, use the custom binding option — the dataStateChange event helps trigger and handle those requests |
> 💡 **Not sure?** Describe your backend: Is it an ASP.NET Core controller? Does it use `[enableQuery]`? What does the response JSON look like? I'll identify the right adaptor for you.
---
## Decision Outcome → Code Generation
After the user confirms, proceed with the selected adaptor. Use the reference files below for exact code patterns.
### Outcome Mapping
| User Choice | Adaptor Enum | Reference File | Response Format | Request Method |
|-------------|-------------|----------------|-----------------|----------------|
| A — OData v4 | `ODataV4Adaptor` | `odatav4-adaptor.md` | `@odata.count` + `value[]` | GET + OData query params |
| B — OData v3 | `ODataAdaptor` | `adaptors.md#odataadaptor` | `{ result, count }` | GET + OData query params |
| C — Web API | `WebApiAdaptor` | `web-api-adaptor.md` | `{ Items, Count }` | GET + QueryString |
| D — URL / Custom REST | `UrlAdaptor` | `url-adaptor.md` | `{ result, count }` | POST + DataManagerRequest body |
| E — GraphQL | `GraphQLAdaptor` | `graphql-adaptor.md` | GraphQL JSON response | POST + GraphQL query |
| F — Custom / In-Memory | `CustomAdaptor` | `custom-adaptor.md` | N/A (fully custom) | N/A |
| G — Custom Binding | **Custom Binding** | `custom-binding.md` | `{ result: [...], count: N }` | Event-driven (dataStateChange, dataSourceChanged) |
---
## Adaptor Quick-Reference Cheat Sheet
### ODataV4Adaptor
```typescript
import { Component } from '@angular/core';
import { DataManager, ODataV4Adaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-root',
template: `
<ejs-grid [dataSource]="data" [allowPaging]="true">
{/* columns */}
</ejs-grid>
`
})
export class AppComponent {
public data: DataManager = new DataManager({
url: 'url', // Replace with actual service URL
adaptor: new ODataV4Adaptor()
});
}
```
- **Server requirement:** `[enableQuery]` attribute on controller action
- **NuGet (server):** `Microsoft.AspNetCore.OData`
- **Auto-translates:** `$filter`, `$orderby`, `$skip`, `$top`, `$count=true`
- **CRUD HTTP verbs:** POST (insert), PATCH (update), DELETE (delete)
---
### ODataAdaptor (v3)
```typescript
import { Component } from '@angular/core';
import { DataManager, ODataAdaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-root',
template: `
<ejs-grid [dataSource]="data" [allowPaging]="true">
{/* columns */}
</ejs-grid>
`
})
export class AppComponent {
public data: DataManager = new DataManager({
url: 'url', // Replace with actual service URL
adaptor: new ODataAdaptor()
});
}
```
- **Server requirement:** OData v3 service (e.g., WCF Data Services or older ASP.NET OData)
- **NuGet (server):** `Microsoft.Data.OData`
---
### WebApiAdaptor
```typescript
import { Component } from '@angular/core';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-root',
template: `
<ejs-grid [dataSource]="data" [allowPaging]="true">
{/* columns */}
</ejs-grid>
`
})
export class AppComponent {
public data: DataManager = new DataManager({
url: 'url', // Replace with actual service URL
adaptor: new WebApiAdaptor()
});
}
```
- **Server requirement:** Returns `{ Items: [...], Count: N }` (case-sensitive keys)
- **Request method:** GET with `$skip`, `$top`, `$filter`, `$orderby` in QueryString
- **CRUD HTTP verbs:** POST (insert), PUT (update), DELETE (delete)
---
### UrlAdaptor
```typescript
import { Component } from '@angular/core';
import { DataManager, UrlAdaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-root',
template: `
<ejs-grid [dataSource]="data" [allowPaging]="true">
{/* columns */}
</ejs-grid>
`
})
export class AppComponent {
public data: DataManager = new DataManager({
url: 'url', // Replace with actual service URL
adaptor: new UrlAdaptor()
});
}
```
- **Server requirement:** POST endpoint accepting `DataManagerRequest` JSON body; returns `{ result: [...], count: N }` lowercase
- **Full manual control:** Apply `DataOperations.PerformFiltering/Sorting/Paging` server-side
---
### GraphQLAdaptor
```typescript
import { Component } from '@angular/core';
import { DataManager, GraphQLAdaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-root',
template: `
<ejs-grid [dataSource]="data" [allowPaging]="true">
{/* columns */}
</ejs-grid>
`
})
export class AppComponent {
public data: DataManager = new DataManager({
url: 'url',
adaptor: new GraphQLAdaptor({
response: {
result: 'getProducts.result',
count: 'getProducts.count'
},
query: `
query getProducts($datamanager: DataManagerInput) {
getProducts(datamanager: $datamanager) {
count
result {
productId
productName
}
}
}
`
})
});
}
```
- **Server requirement:** GraphQL schema with `DataManagerInput` type
- **Mutations required for CRUD:** `InsertMutation`, `UpdateMutation`, `DeleteMutation`
---
### CustomAdaptor
```typescript
import { Component } from '@angular/core';
import { DataManager, ODataV4Adaptor, Query } from '@syncfusion/ej2-data';
import { setValue } from '@syncfusion/ej2-base';
export class CustomAdaptor extends ODataV4Adaptor {
public override processResponse(): Object {
let i = 0;
const original: any = super.processResponse.apply(this, arguments as any);
// Adding serial number
if (original.result) {
original.result.forEach((item: any) => setValue('SNo', ++i, item));
}
return original;
}
public override processQuery(dm: DataManager, query: Query): Object {
dm.dataSource.url = 'url';
query.addParams('Syncfusion in Angular Grid', 'true');
return super.processQuery.apply(this, arguments as any);
}
public override beforeSend(dm: DataManager, request: any, settings?: any): void {
request.headers.set('Authorization', `Bearer ${(window as any).token}`);
super.beforeSend(dm, request, settings);
}
}
@Component({
selector: 'app-root',
template: `
<ejs-grid [dataSource]="data" [allowPaging]="true">
{/* columns */}
</ejs-grid>
`
})
export class AppComponent {
public data: DataManager = new DataManager({
url: 'url', // Replace with actual service URL
adaptor: new CustomAdaptor()
});
}
```
- **Use when:** Non-HTTP source, Entity Framework, SignalR, gRPC, or unique business rules
---
### Custom Binding
```typescript
// Custom Binding pattern for full control over data operations
import { Component, ViewChild } from '@angular/core';
import {
GridComponent,
DataStateChangeEventArgs,
DataSourceChangedEventArgs,
PageService,
SortService,
FilterService,
GroupService,
EditService,
ToolbarService
} from '@syncfusion/ej2-angular-grids';
import { DataManager } from '@syncfusion/ej2-data';
@Component({
selector: 'app-root',
template: `
<ejs-grid #grid [dataSource]="gridData" [allowPaging]="true" [allowSorting]="true" [allowFiltering]="true" [allowGrouping]="true" [pageSettings]="{ pageSize: 12 }" [editSettings]="editSettings" [toolbar]="toolbarOptions" (dataStateChange)="dataStateChange($event)" (dataSourceChanged)="dataSourceChanged($event)">
<e-columns>
<e-column field="OrderID" headerText="Order ID" width="120" [isPrimaryKey]="true"></e-column>
<e-column field="CustomerName" headerText="Customer Name" width="150"></e-column>
<e-column field="ShipCity" headerText="Ship City" width="150"></e-column>
</e-columns>
</ejs-grid>
`,
providers: [PageService, SortService, FilterService, GroupService, EditService, ToolbarService]
})
export class AppComponent {
@ViewChild('grid')
public grid: GridComponent;
public allData = [
{ OrderID: 10248, CustomerName: 'Maria', ShipCity: 'Berlin' },
{ OrderID: 10249, CustomerName: 'Thomas', ShipCity: 'London' }
];
public gridData = {
result: this.allData,
count: this.allData.length
};
public toolbarOptions = ['Add', 'Edit', 'Delete', 'Update', 'Cancel'];
public editSettings = {
allowAdding: true,
allowDeleting: true,
allowEditing: true
};
// Handle paging, sorting, filtering, grouping
public async dataStateChange(args: DataStateChangeEventArgs): Promise<void> {
const query = this.grid.getDataModule().generateQuery();
// Server-side example
const response = await fetch('/api/getData', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
skip: args.skip,
take: args.take,
sorted: args.sorted,
where: args.where,
group: args.group
})
});
const data = await response.json();
this.grid.dataSource = {
result: data.result,
count: data.count
};
// Local data example
const result = new DataManager([...this.allData]).executeLocal(query);
this.grid.dataSource = {
result,
count: this.allData.length
};
}
// Handle CRUD operations
public dataSourceChanged(args: DataSourceChangedEventArgs): void {
if (args.requestType === 'save') {
console.log('Save:', args.data);
}
if (args.requestType === 'delete') {
console.log('Delete:', args.data);
}
this.grid.endEdit();
}
}
```
**How It Works:**
1. **`dataStateChange` event** — Triggered on paging, sorting, filtering, grouping
- Generate query from grid state via `this.grid.getDataModule().generateQuery()`
- Fetch new data from server OR process local data with `Query.executeLocal()`
- Return `{ result: [...], count: N }` to grid
2. **`dataSourceChanged` event** — Triggered on CRUD actions
- Handle add, edit, delete operations
- Send to server or process locally
- Call `this.grid.endEdit()` after operation completes
3. **Response Format** — Always return `{ result: [...], count: N }`
- `result`: Array of records to display
- `count`: Total number of records in dataset
**Use when:**
- ✅ You need **full control over data operations** (filtering, sorting, paging, grouping, CRUD)
- ✅ Data source is **mixed** (local + remote, in-memory, external API, fetch)
- ✅ Custom **business logic** must be applied before binding data to Grid
- ✅ **Server-side operations** (filtering, sorting, paging) handled via fetch
- ✅ **Client-side operations** (in-memory data with Query.executeLocal())
- ✅ **Hybrid approach** combining server and client processing
**Key Features:**
- ✅ Full control over grid operations (paging, sorting, filtering, grouping)
- ✅ Complete CRUD operation handling via `dataSourceChanged` event
- ✅ Grid state management via `dataStateChange` event
- ✅ Works with fetch, local data
- ✅ Returns `{ result: [...], count: N }` format (required by Grid)
- ✅ Supports lazy loading for grouped data with `enableLazyLoading`
- ✅ Server-side or client-side processing (your choice)
**Limitations:**
- ❌ Manual implementation of all data operations
- ❌ Requires handling both `dataStateChange` and `dataSourceChanged` events
- ❌ For large datasets: consider server-side paging/filtering to reduce memory usage
**Common Data Source Options:**
**Option A: Server-side processing**
```typescript
public dataStateChange(args: any): void {
fetch('/api/grid-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ skip: args.skip, take: args.take })
})
.then(response => response.json())
.then(data => {
this.grid.dataSource = data; // { result, count }
});
}
```
**Option B: Client-side processing (local array)**
```typescript
public localData = [
{ OrderID: 10248, CustomerName: 'Maria', ShipCity: 'Berlin' },
{ OrderID: 10249, CustomerName: 'Thomas', ShipCity: 'London' }
];
public dataStateChange(args: any): void {
const query = this.grid.getDataModule().generateQuery();
const result = new DataManager(this.localData).executeLocal(query);
this.grid.dataSource = { result, count: this.localData.length };
}
```
---
## Security Rules (Mandatory — Apply After Gate Confirmation)
After the user confirms the adaptor, **these security rules apply to ALL URL-based adaptors**:
1. **Store URLs in `appsettings.json` or environment config** — never hardcode user-supplied URLs in markup
2. **Validate URL against a whitelist** in `OnInitialized()` before assigning to `DataManager`
3. **Use HTTPS only** — reject any `http://` endpoint in production
4. **Use string variables** for all URL properties on `DataManager` (not inline literals)
5. **Log all remote calls** with timestamps and endpoint metadata
```csharp
// ✅ Correct — whitelist + config-backed URL
private static readonly HashSet<string> AllowedEndpoints = new()
{
"https://myapi.company.com/api/"
};
private string ServiceUrl { get; set; } = string.Empty;
protected override void OnInitialized()
{
const string endpoint = "https://myapi.company.com/api/employees";
if (!AllowedEndpoints.Any(allowed => endpoint.StartsWith(allowed, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"Untrusted endpoint blocked: {endpoint}");
ServiceUrl = endpoint;
}
```
> ⚠️ Never assign a URL from user input, query parameters, or any untrusted source directly to `DataManager.Url`.
---
## Disambiguation Questions
If the user's answer is still ambiguous after the gate, ask these targeted follow-up questions:
| Situation | Follow-up Question |
|-----------|--------------------|
| User says "it's a REST API" | "Does your controller action have `[EnableQuery]` or return `{ Items, Count }` (Web API) vs `{ result, count }` (URL Adaptor)?" |
| User says "it's OData" | "Is it OData v3 or OData v4? Does the URL include `/V4/` or does the controller use `Microsoft.AspNetCore.OData`?" |
| User is unsure | "What does the server response JSON look like? Paste a sample response and I'll identify the adaptor." |
| User says "custom backend" | "Do you need to call the data from an HTTP endpoint, or is it in-memory / a local service? If HTTP, what format does it return?" |
| User says "in-memory data" with "filtering/sorting" | "Use **Custom Binding** if you need client-side filtering, sorting, and paging. Use `Query.executeLocal()` and `DataManager.executeLocal()` to process operations." |
---
## Integration with Stage 3 (Component Mapping)
During **Stage 3 — Layout & Component Mapping**, if `GridComponent`, `DropDownListComponent`, `ComboBoxComponent`, `ListViewComponent`, or any data-bound component is mapped **AND** the user provides a service URL **without specifying the adaptor**, the agent MUST:
1. **Flag the data binding decision** as "requires human gate approval" in the Component Mapping JSON
2. **Note in the Stage 3 output:** `⚠️ DataManager Adaptor: PENDING — Human gate approval required before Stage 5`
3. **Insert the gate presentation** (from the template above) at the end of Stage 3 output
4. **Block advancement to Stage 4** until the user selects an adaptor
```json
// Component Mapping JSON — example flag
{
"components": ["GridComponent", "TextBoxComponent", "ButtonComponent"],
"dataBinding": {
"status": "PENDING_HUMAN_GATE",
"serviceUrl": "http://customurl/api/employees",
"adaptorDecision": null,
"gateRequired": true,
"reason": "Service URL provided without adaptor type — cannot determine ODataV4, WebAPI, URL, GraphQL, or Custom without user confirmation"
}
}
```
---
## Integration with Stage 1 (Intent Analysis)
During **Stage 1 — Intent Analysis**, if the user's prompt contains a URL (detected via regex `https?://[^\s]+`), the agent MUST:
1. **Extract and record the URL** in the intent analysis output
2. **Mark data binding strategy as "ambiguous"** if no adaptor keyword is present
3. **Note:** `⚠️ Service URL detected: {URL} — Adaptor type unknown. Human gate will be triggered at Stage 3.`
**Adaptor keywords that resolve the gate automatically (no gate needed):**
| Keyword in prompt | Resolved Adaptor |
|-------------------|-----------------|
| "OData v4", "ODataV4", "odata/v4" | `ODataV4Adaptor` ✅ |
| "OData v3", "OData service", "ODataAdaptor" | `ODataAdaptor` ✅ |
| "Web API", "WebAPI", "ASP.NET API", "Items and Count" | `WebApiAdaptor` ✅ |
| "URL adaptor", "UrlAdaptor", "result and count" | `UrlAdaptor` ✅ |
| "GraphQL", "GraphQL endpoint", "mutations" | `GraphQLAdaptor` ✅ |
| "custom adaptor", "in-memory", "Entity Framework direct" | `CustomAdaptor` ✅ |
| "custom binding", "state management", "dataStateChange", "dataSourceChanged", "full control" | `Custom Binding` ✅ |
If **none** of these keywords are present → **trigger the gate**.
references/adaptors-guide.md
---
title: 10 Adaptors Guide - Syncfusion Angular DataManager
---
# Complete Guide to 10 Syncfusion Angular DataManager Adaptors
## Table of Contents
- [Overview](#overview)
- [Adaptor Decision Tree](#adaptor-decision-tree)
- [1. JsonAdaptor](#1-jsonadaptor)
- [2. ODataAdaptor](#2-odataadaptor)
- [3. ODataV4Adaptor](#3-odatav4adaptor)
- [4. UrlAdaptor](#4-urladaptor)
- [5. WebApiAdaptor](#5-webapiadaptor)
- [6. WebMethodAdaptor](#6-webmethodadaptor)
- [7. RemoteSaveAdaptor](#7-remotesaveadaptor)
- [8. GraphQLAdaptor](#8-graphqladaptor)
- [9. CustomDataAdaptor](#9-customdataadaptor)
- [10. CustomAdaptor](#10-customadaptor)
- [Comparison Matrix](#comparison-matrix)
---
## Overview
An adaptor handles communication between DataManager and your data source. Each adaptor format requests and interprets responses differently based on the backend's protocol.
### When to Use an Adaptor
- **JsonAdaptor** → In-memory local data
- **ODataAdaptor** → OData v3 endpoints
- **ODataV4Adaptor** → OData v4 endpoints
- **UrlAdaptor** → Generic REST APIs
- **WebApiAdaptor** → ASP.NET Web API
- **WebMethodAdaptor** → ASP.NET web methods
- **RemoteSaveAdaptor** → Client-side queries + server CRUD
- **GraphQLAdaptor** → GraphQL servers
- **CustomDataAdaptor** → Custom request/response handling
- **CustomAdaptor** → Extend built-in adaptors
---
## Adaptor Decision Tree
```
Start: Which data source?
│
├─ Local JSON array in memory?
│ └─ Use: JsonAdaptor
│
└─ Remote API endpoint?
│
├─ OData v3 standard protocol?
│ └─ Use: ODataAdaptor
│
├─ OData v4 standard protocol?
│ └─ Use: ODataV4Adaptor
│
├─ ASP.NET Web API?
│ └─ Use: WebApiAdaptor (recommended for .NET backends)
│
├─ ASP.NET Web Methods?
│ └─ Use: WebMethodAdaptor
│
├─ Generic RESTful API?
│ ├─ Need client-side queries + server-side CRUD?
│ │ └─ Use: RemoteSaveAdaptor
│ │
│ └─ Standard REST patterns?
│ └─ Use: UrlAdaptor
│
├─ GraphQL endpoint?
│ └─ Use: GraphQLAdaptor
│
└─ Need custom logic? Custom request/response format?
├─ FIRST CHOICE: CustomDataAdaptor ⭐ (MOST FLEXIBLE)
│ ├─ Use ANY fetching tool (fetch, axios, Socket.IO, etc.)
│ └─ Return standard: { result, count }
├─ Full control needed? → CustomAdaptor
└─ Standard REST format → UrlAdaptor
```
---
## 1. JsonAdaptor
### Purpose
Works with local JavaScript arrays. All operations happen in the browser (client-side).
### When to Use
- Data is already in memory
- Small to medium datasets
- Need instant filtering/sorting without server calls
- Working offline or without network
### Configuration
```typescript
import { DataManager, Query, JsonAdaptor } from '@syncfusion/ej2-data';
const data = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61 },
{ OrderID: 10250, CustomerID: 'HANAR', Freight: 65.83 }
];
const dataManager = new DataManager({
json: data,
adaptor: new JsonAdaptor()
});
```
### Usage Example
```typescript
// Client-side filtering and sorting
const result = dataManager.executeLocal(
new Query()
.where('Freight', 'greaterThan', 30)
.sortBy('CustomerID')
.take(10)
);
```
### Advantages
✓ No network calls
✓ Works offline
✓ Instant operations
✓ Simpler complexity
### Limitations
✗ Limited to loaded data
✗ Memory constraints
✗ Can't leverage server processing
---
## 2. ODataAdaptor
### Purpose
Communicates with OData v3 service endpoints using the OData protocol.
### When to Use
- Working with OData v3 compliant services
- Need standardized REST protocol
- Server supports $filter, $select, $top, $skip parameters
### Configuration
```typescript
import { DataManager, Query, ODataAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new ODataAdaptor()
});
```
### Usage Example
```typescript
dataManager.executeQuery(
new Query()
.select(['OrderID', 'CustomerID', 'Freight'])
.where('Freight', 'greaterThan', 30)
.sortBy('OrderID')
.take(10)
)
.then((result) => {
console.log(result.result);
console.log(result.count);
});
```
### Server Request Format
```
GET url
?$select=OrderID,CustomerID,Freight
&$filter=Freight gt 30
&$orderby=OrderID
&$top=10
```
---
## 3. ODataV4Adaptor
### Purpose
Supports OData v4 protocol endpoints (newer version of OData v3).
### When to Use
- OData v4 compliant services
- Need latest OData standards
- Server implements $expand, $group features
### Configuration
```typescript
import { DataManager, Query, ODataV4Adaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor()
});
```
### Usage Example
```typescript
dataManager.executeQuery(
new Query()
.select(['OrderID', 'CustomerID', 'Freight'])
.where('Freight', 'greaterThan', 30)
.expand('Customer') // Navigate to related data
.take(20)
)
.then((result) => {
const orders = result.result;
orders.forEach(order => {
console.log(order.OrderID, order.Customer.CustomerID);
});
});
```
---
## 4. UrlAdaptor
### Purpose
Generic RESTful API adaptor for standard HTTP endpoints. **Important:** UrlAdaptor sends **only POST requests** with query parameters in the request body - never GET requests.
### When to Use
- Custom REST APIs (POST-based)
- Non-OData compliant endpoints
- Any HTTP service endpoint accepting POST queries
- GraphQL or custom query formats
### Configuration
```typescript
import { DataManager, Query, UrlAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new UrlAdaptor()
});
```
### Usage Example
```typescript
dataManager.executeQuery(
new Query()
.select(['id', 'name', 'price'])
.where('price', 'greaterThan', 50)
.take(10)
)
.then((result) => {
this.orders = result.result;
});
```
### Expected Server Response
```json
{
"result": [
{ "OrderID": 10248, "CustomerID": "VINET", "Freight": 32.38 },
{ "OrderID": 10249, "CustomerID": "TOMSP", "Freight": 11.61 }
],
"count": 830
}
```
---
## 5. WebApiAdaptor
### Purpose
Specifically designed for ASP.NET Web API endpoints (recommended for .NET backends).
### When to Use
- ASP.NET Web API backends
- Standard REST resource endpoints
- Best practice for .NET integration
### Configuration
```typescript
import { DataManager, Query, WebApiAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
crossDomain: true
});
```
### Usage Example
```typescript
dataManager.executeQuery(
new Query()
.select(['OrderID', 'CustomerID', 'Freight'])
.where('Freight', 'greaterThan', 20)
.sortBy('CustomerID')
.take(25)
)
.then((result) => {
this.items = result.result;
this.pageCount = result.count;
})
.catch((error) => {
console.error('Error:', error);
});
```
### Backend Example (C#)
```csharp
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase {
[HttpGet]
public IActionResult Get(int $skip = 0, int $take = 10, string $filter = "", string $select = "") {
// Process OData-like parameters
var data = DBContext.Orders;
if (!string.IsNullOrEmpty($filter)) {
// Parse and apply filter
}
var result = data.Skip($skip).Take($take).ToList();
return Ok(new { Items, Count = data.Count() });
}
[HttpPost]
public IActionResult Post([FromBody] Order order) {
DBContext.Orders.Add(order);
DBContext.SaveChanges();
return Ok(order);
}
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] Order order) {
var existing = DBContext.Orders.Find(id);
if (existing == null) return NotFound();
existing.CustomerID = order.CustomerID;
existing.Freight = order.Freight;
DBContext.SaveChanges();
return Ok(existing);
}
[HttpDelete("{id}")]
public IActionResult Delete(int id) {
var order = DBContext.Orders.Find(id);
if (order == null) return NotFound();
DBContext.Orders.Remove(order);
DBContext.SaveChanges();
return Ok();
}
}
```
### Expected Response Format
```json
{
"Items": [
{ "OrderID": 10248, "CustomerID": "VINET", "Freight": 32.38 },
{ "OrderID": 10249, "CustomerID": "TOMSP", "Freight": 11.61 }
],
"Count": 830
}
```
---
## 6. WebMethodAdaptor
### Purpose
Integrates with ASP.NET Web Methods (traditional server-side methods exposed via HTTP).
### When to Use
- Legacy ASP.NET applications
- Web Method-based services
- Traditional ASMX web services
### Configuration
```typescript
import { DataManager, Query, WebMethodAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'urlervice.asmx/GetOrders',
adaptor: new WebMethodAdaptor()
});
```
### Usage Example
```typescript
dataManager.executeQuery(new Query().take(10))
.then((result) => {
this.orders = result.result;
});
```
---
## 7. RemoteSaveAdaptor
### Purpose
Unique adaptor for hybrid client/server processing: **Client-side queries (filtering, sorting, paging) + Server-side CRUD (insert, update, delete)**.
### When to Use
- Need to load all data once, filter locally
- Server-side CRUD for data persistence only
- Reduce CRUD operation server calls
- Large datasets with local filtering/sorting
- Dashboard-like applications
### Key Feature
Executes `executeQuery()` with client-side filtering/sorting, but sends CRUD operations (insert, update, delete) to server.
### Configuration
```typescript
import { DataManager, RemoteSaveAdaptor } from '@syncfusion/ej2-data';
this.http.get<Order[]>('url')
.subscribe((value: Order[]) => {
this.dataManager = new DataManager({
json: value, // Load data locally
adaptor: new RemoteSaveAdaptor(), // Handle persistence on server
url: 'url' // CRUD endpoint,
insertUrl: 'url/insert', // Executed Automaticaly
updateUrl: 'url/update', // Executed Automaticaly
removeUrl: 'url/delete', // Executed Automaticaly
});
});
```
### Usage Example
```typescript
// Filtering/sorting happens CLIENT-SIDE (executeQuery)
this.dataManager.executeQuery(
new Query()
.where('Freight', 'greaterThan', 30)
.sortBy('CustomerID')
.take(10)
)
.then((result) => {
this.filteredItems = result.result; // Client-side filtered
});
// CRUD operations go to SERVER
const newOrder = { CustomerID: 'NEW01', Freight: 25 };
this.dataManager.insert(newOrder, 'Orders') // Calls server: POST /api/orders
.then(() => console.log('Inserted'));
this.dataManager.update('OrderID', { OrderID: 10248, Freight: 50 }, 'Orders')
.then(() => console.log('Updated')); // Calls server: PUT /api/orders/10248
this.dataManager.remove('OrderID', 10249, 'Orders')
.then(() => console.log('Deleted')); // Calls server: DELETE /api/orders/10249
```
### Backend Response Format
```json
{
"result": [
{ "OrderID": 10248, "CustomerID": "VINET", "Freight": 32.38 },
{ "OrderID": 10249, "CustomerID": "TOMSP", "Freight": 11.61 }
],
"count": 830
}
```
### C# Backend Example
```csharp
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase {
[HttpGet]
public IActionResult Get() {
var orders = DBContext.Orders.ToList();
return Ok(new { result = orders, count = orders.Count() });
}
[HttpPost]
public IActionResult Post([FromBody] Order order) {
DBContext.Orders.Add(order);
DBContext.SaveChanges();
return Ok(order);
}
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] Order order) {
var existing = DBContext.Orders.Find(id);
if (existing != null) {
existing.CustomerID = order.CustomerID;
existing.Freight = order.Freight;
DBContext.SaveChanges();
}
return Ok(existing);
}
[HttpDelete("{id}")]
public IActionResult Delete(int id) {
var order = DBContext.Orders.Find(id);
if (order != null) {
DBContext.Orders.Remove(order);
DBContext.SaveChanges();
}
return Ok();
}
}
```
### Advantages
✓ Load once, work offline locally
✓ Instant filtering/sorting
✓ Server persists changes
✓ Reduce CRUD network overhead
### Limitations
✗ Memory limited to loaded data
✗ Real-time data not reflected
✗ Manual reload needed for server updates
---
## 8. GraphQLAdaptor
### Purpose
Integrates with GraphQL servers using GraphQL query language.
### When to Use
- GraphQL endpoints
- Complex, nested data queries
- Need flexible, client-driven queries
### Configuration
```typescript
import { DataManager, Query, GraphQLAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new GraphQLAdaptor()
});
```
### Usage Example
```typescript
dataManager.executeQuery(
new Query()
.select(['OrderID', 'CustomerID', 'Freight'])
.sortBy('OrderID')
)
.then((result) => {
this.orders = result.result;
});
```
---
## 9. CustomDataAdaptor
### Purpose
Complete control over request/response with custom fetch logic. Extends UrlAdaptor with ability to override `getData()` method.
### When to Use
- APIs with non-standard request/response formats
- Custom authentication mechanisms
- Non-standard query parameters
- Complex transformation logic
- Complete control over HTTP communication
### Key Feature
Override `getData()` method to implement custom fetch logic with complete control over request construction and response handling.
### Configuration
```typescript
import { DataManager, Query, CustomDataAdaptor } from '@syncfusion/ej2-data';
this.dataManager = new DataManager({
adaptor: new CustomDataAdaptor({
getData: (option: any) => {
// Custom request construction
const request = {
skip: option.skip,
take: option.take,
filter: option.where,
sort: option.sorted
};
fetch("url", {
method: 'POST'
})
.then(response => response.json())
.then(data => {
// Custom response handling
option.onSuccess({
result: data.items,
count: data.total
});
})
.catch(error => {
option.onFailure({}, error);
});
}
})
});
```
### Complete Example
```typescript
this.dataManager = new DataManager({
adaptor: new CustomDataAdaptor({
getData: (option: any) => {
// Build custom request payload
const queryOptions = {
page: Math.floor(option.skip / option.take) + 1,
pageSize: option.take,
search: option.search,
filters: option.where,
sortBy: option.sorted
};
fetch("url", {
method: 'POST'
})
.then(response => {
if (!response.ok) {
throw new Error('API Error');
}
return response.json();
})
.then(data => {
// Transform response to expected format
const result = {
result: data.data, // Extract items
count: data.pagination.total // Extract count
};
option.onSuccess(result, {});
})
.catch(error => {
option.onFailure({}, error);
});
}
})
});
// Use with queries
this.dataManager.executeQuery(
new Query().take(10).skip(0)
)
.then((result) => {
this.items = result.result;
});
```
### Expected Server Response
```json
{
"result": [
{ "OrderID": 10248, "CustomerID": "VINET" }
],
"count": 830
}
```
### Advantages
✓ Complete control over requests
✓ Flexible response handling
✓ Custom authentication
✓ Transform data as needed
### Limitations
✗ More complex setup
✗ Manual error handling
✗ Requires deeper API knowledge
---
## 10. CustomAdaptor
### Purpose
Extend built-in adaptors (like UrlAdaptor) and override specific methods for fine-grained customization.
### When to Use
- Need to modify behavior of existing adaptor
- Custom query translation
- Request/response interception
- Extend rather than completely replace adaptor
### Configuration with Method Overrides
```typescript
import { DataManager, Query, UrlAdaptor } from '@syncfusion/ej2-data';
class MyCustomAdaptor extends UrlAdaptor {
// Override processQuery to customize URL building
processQuery(dm: DataManager, query: Query, params?: any): DMRequest | null {
const req = super.processQuery(dm, query, params);
// Modify request
console.log('Query:', req);
return req;
}
// Override beforeSend to add headers
beforeSend(dm: DataManager, request: XMLHttpRequest | Request, settings?: any): void {
super.beforeSend(dm, request, settings);
if (request instanceof XMLHttpRequest) {
request.setRequestHeader('Authorization', 'send_token');
request.setRequestHeader('X-API-Key', 'apikey');
}
}
// Override processResponse to transform data
processResponse(response: any, dm?: DataManager, query?: Query, xhr?: XMLHttpRequest, request?: Request, params?: any): any {
const result = super.processResponse(response, dm, query, xhr, request, params);
// Transform result
if (result && result.result) {
result.result = result.result.map((item: any) => ({
...item,
processed: true
}));
}
return result;
}
insert(dm: DataManager, value, tableName) {
// Complete custom insert logic
return fetch(`${dm.dataSource.url}/${tableName}`, {
method: 'POST',
}).then(r => r.json());
}
update(dm: DataManager, value, tableName, key, keyField) {
// Complete custom update logic
return fetch(`${dm.dataSource.url}/${tableName}/${key}`, {
method: 'PUT',
body: JSON.stringify(value)
}).then(r => r.json());
}
remove(dm: DataManager, keyField, value, tableName) {
// Complete custom delete logic
return fetch(`${dm.dataSource.url}/${tableName}/${value}`, {
method: 'DELETE'
}).then(r => r.json());
}
}
const dataManager = new DataManager({
url: 'url',
adaptor: new MyCustomAdaptor()
});
```
### Available Methods to Override
- `processQuery()` — Transform Query to request
- `beforeSend()` — Modify headers, authentication
- `processResponse()` — Transform server response
- `insert()`, `update()`, `remove()` — Custom CRUD
---
## Comparison Matrix
| Feature | Json | OData | ODataV4 | Url | WebApi | WebMethod | RemoteSave | GraphQL | CustomData | Custom |
|---------|------|-------|---------|-----|--------|-----------|------------|---------|-----------|--------|
| **Local Data** | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ |
| **Remote API** | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| **Server Filtering** | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| **Offline Mode** | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ |
| **GraphQL Support** | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓* | ✓* |
| **Custom Headers** | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| **Ease of Use** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| **Customization** | None | Low | Low | Medium | Medium | Medium | Low | Low | High | High |
---
## Performance Recommendations
| Scenario | Recommended Adaptor | Reason |
|----------|-------------------|--------|
| Small local dataset | JsonAdaptor | Fastest, no network |
| Large remote dataset | WebApiAdaptor | Server-side processing |
| Real-time dashboard | RemoteSaveAdaptor | Load once, filter locally |
| Non-standard API | CustomDataAdaptor | Complete flexibility |
| .NET Backend | WebApiAdaptor | Best practice integration |
| OData Service | ODataV4Adaptor | Use latest OData version |
---
references/advanced-features.md
---
title: Advanced Features in Syncfusion Angular DataManager
---
# Advanced Features in Syncfusion Angular DataManager
## Table of Contents
- [Overview](#overview)
- [Load on Demand](#load-on-demand)
- [Lazy Loading with Expand](#lazy-loading-with-expand)
- [Pagination Strategies](#pagination-strategies)
- [Deferred Operations](#deferred-operations)
- [Async/Await Patterns](#asyncawait-patterns)
- [Promise.all() for Parallel Operations](#promiseall-for-parallel-operations)
- [Error Handling with Status Codes](#error-handling-with-status-codes)
- [Memory Management](#memory-management)
- [State Persistence](#state-persistence)
- [Type Safety with TypeScript](#type-safety-with-typescript)
---
## Overview
Advanced features enable:
- Efficient handling of large datasets
- Complex async workflows
- Better error management
- Performance optimization
- Type-safe development
---
## Load on Demand
### Virtual Scrolling Pattern
```typescript
@Component({
selector: 'app-virtual-scroll',
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="list">
<div *cdkVirtualFor="let item of items">
{{ item.OrderID }}: {{ item.CustomerID }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`.list { height: 500px; }`]
})
export class VirtualScrollComponent implements OnInit {
public items: any[] = [];
private dataManager: DataManager;
ngOnInit(): void {
this.dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
this.loadInitialData();
}
async loadInitialData(): Promise<void> {
const result = await this.dataManager.executeQuery(
new Query().take(50)
);
this.items = result.result;
}
}
```
### Intersection Observer Pattern
```typescript
@Component({
selector: 'app-load-on-scroll',
template: `
<div #endOfList></div>
<div *ngFor="let item of items">{{ item.OrderID }}</div>
`
})
export class LoadOnScrollComponent implements AfterViewInit, OnDestroy {
@ViewChild('endOfList') endOfList: ElementRef;
public items: any[] = [];
private observer: IntersectionObserver;
private pageIndex = 0;
ngAfterViewInit(): void {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.loadMore();
}
});
this.observer.observe(this.endOfList.nativeElement);
}
async loadMore(): Promise<void> {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dataManager.executeQuery(
new Query()
.skip(this.pageIndex * 50)
.take(50)
);
this.items.push(...result.result);
this.pageIndex++;
}
ngOnDestroy(): void {
if (this.observer) {
this.observer.disconnect();
}
}
}
```
---
## Lazy Loading with Expand
### Navigation Properties
```typescript
import { DataManager, Query, ODataV4Adaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new ODataV4Adaptor()
});
// Load related Customer data
dataManager.executeQuery(
new Query()
.select(['OrderID', 'CustomerID', 'Freight'])
.expand('Customer') // Load related Customer entity
.take(10)
)
.then((result) => {
result.result.forEach((order: any) => {
console.log(order.OrderID);
console.log(order.Customer.CustomerName); // Related data available
});
});
```
### Multi-level Expand
```typescript
// Load Order -> OrderDetails -> Product data
dataManager.executeQuery(
new Query()
.expand('OrderDetails')
.expand('OrderDetails.Product') // Nested expand
.take(5)
)
.then((result) => {
result.result.forEach((order: any) => {
order.OrderDetails.forEach((detail: any) => {
console.log(detail.Product.ProductName);
});
});
});
```
---
## Pagination Strategies
### Client-side Pagination
```typescript
@Component({
selector: 'app-pagination'
})
export class PaginationComponent {
public allData: any[] = [];
public pageSize = 10;
public currentPage = 1;
get pagedData(): any[] {
const start = (this.currentPage - 1) * this.pageSize;
return this.allData.slice(start, start + this.pageSize);
}
async ngOnInit(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query());
this.allData = result.result;
}
getTotal(): number {
return Math.ceil(this.allData.length / this.pageSize);
}
}
```
### Server-side Pagination
```typescript
async goToPage(pageNumber: number): Promise<void> {
const pageSize = 10;
const skip = (pageNumber - 1) * pageSize;
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dataManager.executeQuery(
new Query()
.skip(skip)
.take(pageSize)
);
console.log('Total records:', result.count);
console.log('Current page:', result.result);
}
```
---
## Deferred Operations
The `Deferred` object provides methods for managing asynchronous operations with `.resolve()`, `.reject()`, `.promise`, `.then()`, and `.catch()`:
### Basic Deferred Usage
```typescript
import { DataManager, Deferred } from '@syncfusion/ej2-data';
// Create a deferred object for async operation management
const deferred = new Deferred();
// Simulate async work (e.g., fetching from API)
setTimeout(() => {
const result = {
result: [
{ id: 1, name: 'Order 1' },
{ id: 2, name: 'Order 2' }
],
count: 2
};
deferred.resolve(result); // Mark operation as successful
}, 1000);
// Handle the deferred operation result
deferred.promise
.then((result: any) => {
console.log('Operation succeeded:', result.result);
})
.catch((error: any) => {
console.error('Operation failed:', error.message);
});
```
### Deferred with Rejection (Error Handling)
```typescript
import { DataManager, Deferred } from '@syncfusion/ej2-data';
function fetchDataWithError(shouldFail: boolean = false): Deferred {
const deferred = new Deferred();
setTimeout(() => {
if (shouldFail) {
deferred.reject(new Error('API returned 500 Server Error'));
} else {
deferred.resolve({ result: [{ id: 1, value: 'Success' }] });
}
}, 1000);
return deferred;
}
// Handling rejected deferred
fetchDataWithError(true)
.promise
.then((result: any) => {
console.log('Success:', result.result);
})
.catch((error: any) => {
console.error('Error caught:', error.message); // "API returned 500 Server Error"
});
```
### Deferred with Query Execution
```typescript
import { DataManager, Query, WebApiAdaptor, Deferred } from '@syncfusion/ej2-data';
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// executeQuery() returns a Deferred internally
const deferred: any = dm.executeQuery(
new Query().where('Status', 'equal', 'Completed').take(10)
);
deferred
.then((result: any) => {
console.log('Query succeeded:', result.result);
return result.result;
})
.catch((error: any) => {
console.error('Query failed:', error);
});
```
### Multiple Deferred Operations (Parallel Execution)
```typescript
import { Deferred } from '@syncfusion/ej2-data';
const deferred1 = new Deferred();
const deferred2 = new Deferred();
const deferred3 = new Deferred();
// Simulate multiple async operations
setTimeout(() => deferred1.resolve({ data: 'Result 1' }), 500);
setTimeout(() => deferred2.resolve({ data: 'Result 2' }), 700);
setTimeout(() => deferred3.resolve({ data: 'Result 3' }), 600);
// Wait for all deferred operations to complete
Promise.all([deferred1.promise, deferred2.promise, deferred3.promise])
.then((results: any[]) => {
console.log('All operations complete:', results);
})
.catch((error: any) => {
console.error('One or more operations failed:', error);
});
```
---
## Async/Await Patterns
### Sequential Operations
```typescript
async loadDataSequentially(): Promise<void> {
try {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Load orders
const ordersResult = await dm.executeQuery(new Query());
console.log('Orders loaded:', ordersResult.result.length);
// Then load customers
const customersResult = await dm.executeQuery(
new Query().select(['CustomerID', 'CustomerName'])
);
console.log('Customers loaded:', customersResult.result.length);
} catch (error) {
console.error('Sequential load failed:', error);
}
}
```
### Parallel Operations
```typescript
async loadDataParallel(): Promise<void> {
try {
const ordersDm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const customersDm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Load both simultaneously
const [ordersResult, customersResult] = await Promise.all([
ordersDm.executeQuery(new Query()),
customersDm.executeQuery(new Query())
]);
console.log('Orders:', ordersResult.result);
console.log('Customers:', customersResult.result);
} catch (error) {
console.error('Parallel load failed:', error);
}
}
```
---
## Promise.all() for Parallel Operations
### Multiple Data Sources
```typescript
async loadMultipleSources(): Promise<void> {
const sources = [
new DataManager({ url: 'api/orders', adaptor: new WebApiAdaptor() }),
new DataManager({ url: 'api/customers', adaptor: new WebApiAdaptor() }),
new DataManager({ url: 'api/products', adaptor: new WebApiAdaptor() }),
new DataManager({ url: 'api/employees', adaptor: new WebApiAdaptor() })
];
try {
const results = await Promise.all(
sources.map(dm => dm.executeQuery(new Query().take(100)))
);
this.orders = results[0].result;
this.customers = results[1].result;
this.products = results[2].result;
this.employees = results[3].result;
} catch (error) {
console.error('Multi-source load failed:', error);
}
}
```
### Race Condition Handling
```typescript
// First data source to respond wins
async loadFatest(): Promise<any> {
const sources = [
new DataManager({ url: 'api1.example.com/orders', adaptor: new WebApiAdaptor() }),
new DataManager({ url: 'api2.example.com/orders', adaptor: new WebApiAdaptor() })
];
try {
const result = await Promise.race(
sources.map(dm => dm.executeQuery(new Query()))
);
return result.result;
} catch (error) {
console.error('All sources failed:', error);
}
}
```
---
## Error Handling with Status Codes
### HTTP Status Code Handling
```typescript
async loadWithErrorHandling(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
try {
const result = await dm.executeQuery(new Query());
this.orders = result.result;
} catch (error: any) {
const status = error.status || error.statusCode || 0;
if (status === 0) {
console.error('Network error - check connectivity');
this.errorMessage = 'Network connection failed';
} else if (status === 400) {
console.error('Bad request - invalid parameters');
this.errorMessage = 'Invalid request data';
} else if (status === 401) {
console.error('Unauthorized - authentication required');
this.errorMessage = 'Please login again';
this.redirectToLogin();
} else if (status === 403) {
console.error('Forbidden - access denied');
this.errorMessage = 'You don\'t have permission to access this';
} else if (status === 404) {
console.error('Not found - resource doesn\'t exist');
this.errorMessage = 'Resource not found';
} else if (status === 409) {
console.error('Conflict - data conflict');
this.errorMessage = 'Data conflict - please refresh and try again';
} else if (status === 500) {
console.error('Server error - try again later');
this.errorMessage = 'Server error - please try again later';
} else if (status >= 500) {
console.error('Server error:', status);
this.errorMessage = 'Server error - please try again later';
} else {
console.error('Unknown error:', status);
this.errorMessage = 'An unexpected error occurred';
}
}
}
```
### Retry Logic with Exponential Backoff
```typescript
async loadWithRetry(maxRetries = 3): Promise<any> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await dm.executeQuery(new Query());
} catch (error: any) {
const isRetryable = error.status === 0 || error.status >= 500;
const isLastAttempt = attempt === maxRetries;
if (isRetryable && !isLastAttempt) {
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
```
---
## Memory Management
### Garbage Collection for Large Datasets
```typescript
@Component({
selector: 'app-large-dataset'
})
export class LargeDatasetComponent implements OnDestroy {
private dataManager: DataManager;
private subscription: any;
ngOnInit(): void {
this.dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
}
loadLargeDataset(): void {
// Load only what's visible to user (pagination)
const pageSize = 50;
this.dataManager.executeQuery(
new Query().take(pageSize)
)
.then((result) => {
// Keep limited data in memory
this.data = result.result.slice(0, pageSize);
});
}
ngOnDestroy(): void {
// Clean up
if (this.subscription) {
this.subscription.unsubscribe();
}
// Help garbage collection
if (this.dataManager) {
this.dataManager.clearCache();
}
}
}
```
### Memory-efficient Mapping
```typescript
// ❌ MEMORY INEFFICIENT - Creates large intermediate arrays
const results = data
.map(item => ({ id: item.OrderID, name: item.CustomerID }))
.filter(item => item.id > 10248)
.map(item => item.name)
.slice(0, 100);
// ✅ MEMORY EFFICIENT - Single iteration, minimal allocation
const results = [];
for (const item of data) {
if (item.OrderID > 10248) {
results.push(item.CustomerID);
if (results.length >= 100) break;
}
}
```
---
## State Persistence
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
Syncfusion DataManager provides built-in state persistence using browser localStorage. Query states are automatically saved and restored across page reloads.
### Basic State Persistence
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
@Component({
selector: 'app-state-persistence',
template: `
<div>
<h3>Orders (sorted by CustomerID desc)</h3>
<ul>
<li *ngFor="let order of orders">
{{ order.CustomerID }} - ${{ order.Freight }}
</li>
</ul>
<p>Note: Sorting state persisted. Refresh page to see state restored.</p>
</div>
`
})
export class StatePersistenceComponent implements OnInit {
orders: any[] = [];
ngOnInit(): void {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enablePersistence: true, // Enable state persistence
id: 'angularOrderManager' // Unique ID for localStorage key
});
// Execute query with sorting
dataManager.executeQuery(
new Query().sortBy('CustomerID', 'descending').take(10)
).then((result) => {
this.orders = result.result;
// Query state (sorting) automatically saved to localStorage
}).catch((error) => {
console.error('Error:', error);
});
}
}
```
### Excluding Queries from Persistence
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, UrlAdaptor, Query } from '@syncfusion/ej2-data';
@Component({
selector: 'app-selective-persistence'
})
export class SelectivePersistenceComponent implements OnInit {
products: any[] = [];
ngOnInit(): void {
const dataManager = new DataManager({
url: 'url',
adaptor: new UrlAdaptor(),
enablePersistence: true,
id: 'angularProductManager',
ignoreOnPersist: ['onSortBy', 'onSearch'] // Don't persist sorting and search
});
// These will be persisted: filtering, grouping
// These will NOT be persisted: sorting, search
dataManager.executeQuery(
new Query()
.where('Status', 'equal', 'Active')
.sortBy('Price', 'descending') // Won't persist
).then((result) => {
this.products = result.result;
}).catch((error) => {
console.error('Error:', error);
});
}
}
```
### Retrieve Persisted State
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
import { Component } from '@angular/core';
import { DataManager } from '@syncfusion/ej2-data';
@Component({
selector: 'app-get-persisted-data',
template: `<button (click)="getPersistedState()">Show Persisted State</button>`
})
export class GetPersistedDataComponent {
getPersistedState(): void {
// Retrieve the persisted query state from localStorage
const persisted = DataManager.getPersistedData('angularOrderManager');
console.log('Persisted state:', persisted);
// Output: { onSortBy: [{field: 'CustomerID', direction: 'descending'}], ... }
}
}
```
### Update Persisted State
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
import { Component } from '@angular/core';
import { DataManager, Query } from '@syncfusion/ej2-data';
@Component({
selector: 'app-set-persisted-data',
template: `<button (click)="updatePersistedState()">Update Persisted State</button>`
})
export class SetPersistedDataComponent {
updatePersistedState(): void {
// Create a new query to be persisted
const newQuery = new Query()
.where('Freight', 'greaterThan', 100)
.sortBy('EmployeeID', 'descending');
// Update the persisted state in localStorage
DataManager.setPersistData(
null, // event: null when not in event context
'angularOrderManager', // DataManager id
newQuery // New query to persist
);
console.log('Persisted state updated');
}
}
```
### Clear Persisted State
```typescript
import { Component } from '@angular/core';
import { DataManager } from '@syncfusion/ej2-data';
@Component({
selector: 'app-clear-persistence',
template: `
<button (click)="clearThis()">Clear This DataManager</button>
<button (click)="clearAll()">Clear All (Logout)</button>
`
})
export class ClearPersistenceComponent {
clearThis(): void {
// Remove all persisted data for this DataManager
DataManager.clearPersistence('angularOrderManager');
console.log('Persisted state cleared - DataManager reset to initial state');
}
clearAll(): void {
// Clear persistence for multiple DataManagers on logout
DataManager.clearPersistence('angularOrderManager');
DataManager.clearPersistence('angularProductManager');
console.log('All persisted states cleared');
}
}
```
### Persistence Properties Reference
| Property | Type | Purpose |
|----------|------|---------|
| `enablePersistence` | boolean | Enable/disable state persistence |
| `id` | string | Unique identifier for localStorage key |
| `ignoreOnPersist` | string[] | Query types to exclude: `onSortBy`, `onSearch`, `onWhere`, `Grouping` |
---
## Type Safety with TypeScript
### Strongly Typed Operations
```typescript
interface Order {
OrderID: number;
CustomerID: string;
EmployeeID: number;
Freight: number;
}
async loadTypedData(): Promise<Order[]> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query());
const orders: Order[] = result.result as Order[];
// TypeScript knows Order properties
orders.forEach(order => {
console.log(order.OrderID); // ✓ Valid
console.log(order.Freight); // ✓ Valid
console.log(order.InvalidField); // ✗ Compile error
});
return orders;
}
```
### Generic Type Definitions
```typescript
async genericLoad<T>(url: string): Promise<T[]> {
const dm = new DataManager({
url: url,
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query());
return result.result as T[];
}
// Usage
const orders = await this.genericLoad<Order>('api/orders');
const customers = await this.genericLoad<Customer>('api/customers');
```
---
## Complete Advanced Example
```typescript
@Component({
selector: 'app-advanced-data'
})
export class AdvancedDataComponent implements OnInit, OnDestroy {
public items: any[] = [];
public loading = false;
public error: string | null = null;
constructor(private stateService: DataManagerStateService) {}
async ngOnInit(): Promise<void> {
// Try to restore state
const saved = this.stateService.restoreState();
if (saved) {
this.items = saved.results;
return;
}
// Load fresh data
await this.loadData();
}
private async loadData(): Promise<void> {
this.loading = true;
this.error = null;
try {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(
new Query()
.where('Status', 'equal', 'Active')
.sortBy('OrderID')
.take(100)
);
this.items = result.result;
this.stateService.saveState(new Query(), this.items);
} catch (error: any) {
this.error = this.getErrorMessage(error.status);
} finally {
this.loading = false;
}
}
private getErrorMessage(status: number): string {
const messages: { [key: number]: string } = {
0: 'Network error',
401: 'Please login',
403: 'Access denied',
404: 'Not found',
500: 'Server error'
};
return messages[status] || 'Unknown error';
}
ngOnDestroy(): void {
this.stateService.clearState();
}
}
```
---
references/caching-offline-mode.md
---
title: Caching and Offline Mode in Syncfusion Angular DataManager
---
# Caching and Offline Mode in Syncfusion Angular DataManager
## Table of Contents
- [Overview](#overview)
- [Offline Mode Configuration](#offline-mode-configuration)
- [Data Persistence with localStorage](#data-persistence-with-localstorage)
- [Cache Auto-clearing Rules](#cache-auto-clearing-rules)
- [Manual Cache Management](#manual-cache-clearing-with-clearcache)
- [Online/Offline Workflow](#onlineoffline-workflow)
- [Sync Strategies](#sync-strategies)
- [Performance Optimization](#performance-optimization)
---
## Overview
DataManager provides two key optimization features:
- **Offline Mode** — Load all data at initialization, work without server connection
- **Caching** — Reduce redundant server requests by storing responses
Both features are essential for:
- Mobile applications with intermittent connectivity
- Reducing server load
- Improving application performance
- Better user experience during network issues
---
##Offline Mode Configuration
### Enable Offline Mode
```typescript
import { DataManager, Query, WebApiAdaptor, ReturnOption } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true // ENABLE OFFLINE MODE
});
// Data will be loaded during initialization with offline: true
// All queries will then execute locally without server calls
console.log('DataManager initialized with offline mode enabled');
```
### How Offline Mode Works
```typescript
// STEP 1: Load all data at initialization
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true
});
// STEP 2: Data is loaded during initialization
// Execute queries locally on the cached data
const query = new Query()
.where('Freight', 'greaterThan', 50)
.sortBy('CustomerID')
.take(20);
// STEP 3: Queries run CLIENT-SIDE (no server calls)
dataManager.executeQuery(
new Query()
.where('Freight', 'greaterThan', 50)
.sortBy('CustomerID')
.take(20)
)
.then((result) => {
// Uses local data, instant result
console.log('Filtered records:', result.result);
});
```
### Manual Data Loading for Offline
```typescript
// For manual control, load via HTTP then use locally
import { HttpClient } from '@angular/common/http';
constructor(private http: HttpClient) {}
loadDataForOffline(): void {
this.http.get<any[]>('url')
.subscribe((data) => {
const dataManager = new DataManager({
json: data,
adaptor: new JsonAdaptor()
});
// All operations are client-side
const result = dataManager.executeLocal(
new Query().take(10)
);
});
}
```
---
## Data Persistence with localStorage
### Save Data to localStorage
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
async saveDataOffline(): Promise<void> {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
try {
const result = await dataManager.executeQuery(new Query());
// Save to browser storage
localStorage.setItem(
'cached_orders',
JSON.stringify(result.result)
);
// Save metadata
localStorage.setItem('cached_orders_timestamp', new Date().toISOString());
console.log('Data saved for offline use');
} catch (error) {
console.error('Failed to cache data:', error);
}
}
```
### Load Data from localStorage
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
loadDataFromCache(): any[] {
const cached = localStorage.getItem('cached_orders');
const timestamp = localStorage.getItem('cached_orders_timestamp');
if (cached && timestamp) {
const age = Date.now() - new Date(timestamp).getTime();
const oneHour = 60 * 60 * 1000;
if (age < oneHour) {
console.log('Using cached data (fresh)');
return JSON.parse(cached);
} else {
console.log('Cache expired, fetching fresh data');
localStorage.removeItem('cached_orders');
localStorage.removeItem('cached_orders_timestamp');
return [];
}
}
return [];
}
```
### Cache Strategy with Expiration
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
@Injectable({ providedIn: 'root' })
export class CacheService {
private cacheKey = (name: string) => `cache_${name}`;
private timestampKey = (name: string) => `cache_timestamp_${name}`;
private cacheExpiry = 1 * 60 * 60 * 1000; // 1 hour
set(name: string, data: any): void {
localStorage.setItem(this.cacheKey(name), JSON.stringify(data));
localStorage.setItem(this.timestampKey(name), Date.now().toString());
}
get(name: string): any {
const data = localStorage.getItem(this.cacheKey(name));
const timestamp = localStorage.getItem(this.timestampKey(name));
if (!data || !timestamp) return null;
const age = Date.now() - parseInt(timestamp);
if (age > this.cacheExpiry) {
this.remove(name);
return null;
}
return JSON.parse(data);
}
remove(name: string): void {
localStorage.removeItem(this.cacheKey(name));
localStorage.removeItem(this.timestampKey(name));
}
clear(): void {
localStorage.clear();
}
}
```
---
## Cache Auto-clearing Rules
### When DataManager Clears Cache
Cache automatically clears when CRUD operations occur:
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true
});
// Data is automatically cached when offline: true is set
// No need to wait explicitly
// INSERT → Cache clears (new record added to server)
dataManager.insert({ CustomerID: 'NEW', Freight: 25 }, 'Orders')
.then(() => {
// Cache invalidated automatically
console.log('Record inserted, cache cleared');
});
// UPDATE → Cache clears
dataManager.update('OrderID', { OrderID: 10248, Freight: 50 }, 'Orders')
.then(() => {
console.log('Record updated, cache cleared');
});
// DELETE → Cache clears
dataManager.remove('OrderID', 10248, 'Orders')
.then(() => {
console.log('Record deleted, cache cleared');
});
```
### After CRUD, Re-fetch Fresh Data
```typescript
async performCrudAndRefresh(newRecord: any): Promise<void> {
const dataManager = new dataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true
});
try {
// 1. Insert new record
await dataManager.insert(newRecord, 'Orders');
console.log('Inserted successfully');
// 2. Cache cleared automatically
// 3. Manually re-fetch to get fresh data including new record
const freshResult = await dataManager.executeQuery(new Query());
this.orders = freshResult.result;
} catch (error) {
console.error('CRUD failed:', error);
}
}
```
---
## Manual Cache Clearing with clearCache()
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
Clear cached data explicitly using the `clearCache()` method:
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
enableCache: true,
offline: true
});
// Data is automatically loaded and cached with offline: true
// Option 1: Clear all cache manually
dataManager.clearCache();
console.log('Cache cleared');
// Next query hits server
dataManager.executeQuery(new Query()).then((result) => {
console.log('Fresh data from server');
});
```
**Method signature:** `dm.clearCache()`
**Purpose:** Clears cached data from offline mode or browser storage
**When to use:**
- After modifying offline data
- During user logout or session end
- Resetting application state
- Manually invalidating entire cache
- After critical system updates
**Example in Angular service:**
```typescript
@Injectable({ providedIn: 'root' })
export class OrderService {
private dm: DataManager;
constructor() {
this.dm = new DataManager({
url: '/api/orders',
adaptor: new WebApiAdaptor(),
enableCache: true
});
}
clearAllCache(): void {
// Clear all cached orders
this.dm.clearCache();
console.log('All order cache cleared');
}
async logout(): Promise<void> {
// Security: Clear sensitive cached data
this.dm.clearCache();
// Clear other caches
localStorage.clear();
sessionStorage.clear();
}
async performCriticalUpdate(data: any): Promise<void> {
// Make update to critical data
await this.dm.update('OrderID', data, 'Orders');
// Force all components to refresh
this.dm.clearCache();
console.log('Critical update complete - cache cleared');
}
}
```
**Usage in component:**
```typescript
@Component({
selector: 'app-orders',
templateUrl: './orders.component.html'
})
export class OrdersComponent implements OnInit {
orders: any[] = [];
constructor(private orderService: OrderService) {}
ngOnInit() {
this.loadOrders();
}
loadOrders() {
this.orderService.getOrders().subscribe(data => {
this.orders = data;
});
}
onLogout() {
this.orderService.logout();
// Redirect to login
}
onCriticalUpdate() {
// After important system change
this.orderService.clearAllCache();
this.loadOrders(); // Reload fresh data
}
}
```
**Difference: Automatic vs Manual Clearing:**
- **Automatic:** CRUD operations automatically clear cache
```typescript
await dm.insert(newOrder, 'Orders'); // Auto-clears cache
await dm.update('OrderID', updated, 'Orders'); // Auto-clears cache
```
- **Manual:** Explicit control with clearCache()
```typescript
dm.clearCache(); // Explicit clear for all cache
```
---
## Online/Offline Workflow
### Detect Network Status
```typescript
@Injectable({ providedIn: 'root' })
export class NetworkService {
private onlineSubject = new BehaviorSubject<boolean>(navigator.onLine);
public online$ = this.onlineSubject.asObservable();
constructor() {
window.addEventListener('online', () => this.onlineSubject.next(true));
window.addEventListener('offline', () => this.onlineSubject.next(false));
}
isOnline(): boolean {
return this.onlineSubject.value;
}
}
```
### Adaptive Data Loading
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
@Component({
selector: 'app-adaptive-data',
templateUrl: './adaptive-data.component.html'
})
export class AdaptiveDataComponent implements OnInit {
public orders: any[] = [];
public isOnline = true;
constructor(
private networkService: NetworkService,
private dataService: DataService
) {}
ngOnInit(): void {
this.networkService.online$.subscribe((online) => {
this.isOnline = online;
this.loadData();
});
}
loadData(): void {
if (this.isOnline) {
// Online: Fetch fresh data and save for offline
this.dataService.getOrders()
.then((result) => {
this.orders = result;
localStorage.setItem('cached_orders', JSON.stringify(result));
})
.catch((error) => {
console.error('Load failed:', error);
this.loadFromCache();
});
} else {
// Offline: Use cached data
this.loadFromCache();
}
}
loadFromCache(): void {
const cached = localStorage.getItem('cached_orders');
this.orders = cached ? JSON.parse(cached) : [];
}
}
```
### Queue Operations While Offline
```typescript
@Injectable({ providedIn: 'root' })
export class OfflineQueueService {
private queue: any[] = [];
constructor(private networkService: NetworkService) {
this.networkService.online$.subscribe((online) => {
if (online) {
this.processPendingOperations();
}
});
}
queueOperation(operation: any): void {
this.queue.push({
...operation,
timestamp: new Date()
});
console.log('Operation queued:', operation);
}
async processPendingOperations(): Promise<void> {
while (this.queue.length > 0) {
const operation = this.queue.shift();
try {
await this.executeOperation(operation);
console.log('Operation completed:', operation);
} catch (error) {
console.error('Operation failed, re-queueing:', operation);
this.queue.unshift(operation);
break;
}
}
}
private async executeOperation(operation: any): Promise<any> {
// Execute the operation (update, insert, delete)
// Implementation depends on operation type
}
}
```
---
## Sync Strategies
### Strategy 1: Last-Write-Wins
```typescript
class LastWriteWinsStrategy {
async sync(localData: any[], remoteData: any[]): Promise<any[]> {
const merged = [];
for (const local of localData) {
const remote = remoteData.find(r => r.id === local.id);
if (!remote) {
// Record exists only locally (new)
merged.push(local);
} else if (!local.modifiedDate || !remote.modifiedDate) {
// Fallback to local
merged.push(local);
} else {
// Use whichever was modified last
const localModified = new Date(local.modifiedDate).getTime();
const remoteModified = new Date(remote.modifiedDate).getTime();
merged.push(localModified > remoteModified ? local : remote);
}
}
// Add remote records not in local copy
for (const remote of remoteData) {
if (!merged.find(m => m.id === remote.id)) {
merged.push(remote);
}
}
return merged;
}
}
```
### Strategy 2: Conflict Resolution
```typescript
class ConflictResolvingSync {
async sync(localChanges: any[], dataManager: DataManager): Promise<void> {
const conflicts: any[] = [];
for (const change of localChanges) {
try {
// Try to apply change
if (change.operation === 'insert') {
await dataManager.insert(change.data, 'Orders');
} else if (change.operation === 'update') {
await dataManager.update('OrderID', change.data, 'Orders');
} else if (change.operation === 'delete') {
await dataManager.remove('OrderID', change.id, 'Orders');
}
} catch (error: any) {
if (error.status === 409) {
// Conflict detected
conflicts.push({ change, error });
}
}
}
if (conflicts.length > 0) {
console.warn('Sync conflicts:', conflicts);
// Notify user to resolve conflicts
}
}
}
```
---
## Performance Optimization
### Lazy Loading with Pagination
```typescript
@Component({
selector: 'app-lazy-load',
template: `
<div>
<div *ngFor="let order of orders">{{ order.OrderID }}</div>
<button (click)="loadMore()">Load More</button>
</div>
`
})
export class LazyLoadComponent {
public orders: any[] = [];
private pageSize = 50;
private pageIndex = 0;
constructor(private dataManager: DataManager) {}
ngOnInit(): void {
this.loadMore();
}
async loadMore(): Promise<void> {
const result = await this.dataManager.executeQuery(
new Query()
.skip(this.pageIndex * this.pageSize)
.take(this.pageSize)
);
this.orders.push(...result.result);
this.pageIndex++;
}
}
```
### Memory Management
```typescript
// Clean up large datasets
if (this.orders.length > 10000) {
// Implement pagination or virtual scrolling
this.orders = this.orders.slice(0, 5000);
}
// Use trackBy to help Angular with change detection
trackByOrderId(index: number, order: any): number {
return order.OrderID;
}
```
---
## Complete Example
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
@Component({
selector: 'app-offline-orders',
template: `
<div>
<p>Status: {{ isOnline ? '🟢 Online' : '🔴 Offline' }}</p>
<button (click)="syncData()">Sync Data</button>
<table>
<tr *ngFor="let order of orders; trackBy: trackByOrderId">
<td>{{ order.OrderID }}</td>
<td>{{ order.CustomerID }}</td>
</tr>
</table>
</div>
`
})
export class OfflineOrdersComponent implements OnInit {
public orders: any[] = [];
public isOnline = navigator.onLine;
constructor(
private http: HttpClient,
private networkService: NetworkService
) {}
ngOnInit(): void {
window.addEventListener('online', () => this.onGoOnline());
window.addEventListener('offline', () => this.onGoOffline());
this.initializeData();
}
private async initializeData(): Promise<void> {
if (this.isOnline) {
await this.loadFromServer();
} else {
this.loadFromCache();
}
}
private async loadFromServer(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true
});
try {
const result = await dm.ready;
this.orders = result.result || [];
this.cacheData(this.orders);
} catch (error) {
console.error('Load failed:', error);
this.loadFromCache();
}
}
private loadFromCache(): void {
const cached = localStorage.getItem('orders_cache');
this.orders = cached ? JSON.parse(cached) : [];
}
private cacheData(data: any[]): void {
localStorage.setItem('orders_cache', JSON.stringify(data));
localStorage.setItem('orders_cache_time', Date.now().toString());
}
async syncData(): Promise<void> {
if (this.isOnline) {
await this.loadFromServer();
}
}
private onGoOnline(): void {
this.isOnline = true;
console.log('Connected to network');
this.loadFromServer();
}
private onGoOffline(): void {
this.isOnline = false;
console.log('Disconnected from network');
this.loadFromCache();
}
trackByOrderId(index: number, order: any): number {
return order.OrderID;
}
}
```
---
references/crud-operations.md
---
title: CRUD Operations in Syncfusion Angular DataManager
---
# CRUD Operations in Syncfusion Angular DataManager
## Table of Contents
- [Overview](#overview)
- [Insert - Adding Records](#insert---adding-records)
- [Update - Modifying Records](#update---modifying-records)
- [Delete - Removing Records](#delete---removing-records)
- [Batch Operations](#batch-operations)
- [Response Handling](#response-handling)
- [Error Handling](#error-handling)
---
## Overview
CRUD (Create, Read, Update, Delete) operations manage data persistence. DataManager provides methods for each operation:
- **insert()** — Add new records
- **update()** — Modify existing records
- **remove()** — Delete records
- **saveChanges()** — Batch persist all changes
All CRUD methods return Promises for async handling.
---
## Insert - Adding Records
### Basic Insert
```typescript
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const newOrder = {
OrderID: 10251,
CustomerID: 'NEW01',
EmployeeID: 1,
Freight: 25.50
};
dataManager.insert(newOrder, 'Orders')
.then((result) => {
console.log('Record inserted successfully');
})
.catch((error) => {
console.error('Insert failed:', error);
});
```
### Insert Multiple Records
```typescript
const newOrders = [
{ CustomerID: 'CUST1', EmployeeID: 1, Freight: 20 },
{ CustomerID: 'CUST2', EmployeeID: 2, Freight: 30 },
{ CustomerID: 'CUST3', EmployeeID: 3, Freight: 40 }
];
// Insert one by one
for (const order of newOrders) {
dataManager.insert(order, 'Orders');
}
// Or collect and insert via saveChanges
const batch = [];
for (const order of newOrders) {
batch.push(dataManager.insert(order, 'Orders'));
}
await Promise.all(batch);
```
### Insert with Local Data
```typescript
const localData = [
{ OrderID: 10248, CustomerID: 'VINET' },
{ OrderID: 10249, CustomerID: 'TOMSP' }
];
const dataManager = new DataManager({
json: localData,
adaptor: new JsonAdaptor()
});
// For local data, add to array and reassign
const newRecord = { OrderID: 10250, CustomerID: 'HANAR' };
localData.push(newRecord);
// Or use DataSource property
dataManager.insert(newRecord, null);
```
---
## Update - Modifying Records
### Basic Update
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const updatedRecord = {
OrderID: 10248,
CustomerID: 'VINET_UPDATED',
Freight: 50.00
};
dataManager.update('Orders', updatedRecord)
.then(() => {
console.log('Record updated successfully');
})
.catch((error) => {
console.error('Update failed:', error);
});
```
### Update Specific Fields
```typescript
// Update only selected fields
const partialUpdate = {
OrderID: 10248,
Freight: 45.00 // Only update Freight
};
dataManager.update('OrderID', partialUpdate, 'Orders');
```
### Update Multiple Records
```typescript
const updates = [
{ OrderID: 10248, Freight: 40.00 },
{ OrderID: 10249, Freight: 25.00 },
{ OrderID: 10250, Freight: 60.00 }
];
const promises = updates.map(update =>
dataManager.update('OrderID', update, 'Orders')
);
Promise.all(promises)
.then(() => console.log('All updates complete'))
.catch((error) => console.error('Update failed:', error));
```
---
## Delete - Removing Records
### Basic Delete
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const recordToDelete = { OrderID: 10248 };
dataManager.remove('OrderID', recordToDelete.OrderID, 'Orders')
.then(() => {
console.log('Record deleted successfully');
})
.catch((error) => {
console.error('Delete failed:', error);
});
```
### Delete by Key
```typescript
// Pass just the key value
dataManager.remove('OrderID', 10248, 'Orders'); // Deletes record with OrderID = 10248
// For string keys
dataManager.remove('CustomerID', 'VINET', 'Orders'); // Deletes record with CustomerID = 'VINET'
```
### Delete Multiple Records
```typescript
const keysToDelete = [10248, 10249, 10250];
const promises = keysToDelete.map(id =>
dataManager.remove('OrderID', id, 'Orders')
);
Promise.all(promises)
.then(() => console.log('All deletions complete'))
.catch((error) => console.error('Delete failed:', error));
```
---
## Batch Operations
### Using saveChanges()
saveChanges() persists multiple operations (inserts, updates, deletes) in a single batch.
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Collect changes
const recordsToInsert = [
{ CustomerID: 'NEW1', Freight: 25 },
{ CustomerID: 'NEW2', Freight: 30 }
];
const recordsToUpdate = [
{ OrderID: 10248, Freight: 40 }
];
const recordsToDelete = [10249, 10250];
// Execute batch
dataManager.saveChanges({
addedRecords: recordsToInsert,
changedRecords: recordsToUpdate,
deletedRecords: recordsToDelete
})
.then((result) => {
console.log('Batch operation completed');
console.log('Inserted:', result.addedRecords);
console.log('Updated:', result.changedRecords);
console.log('Deleted:', result.deletedRecords);
})
.catch((error) => {
console.error('Batch operation failed:', error);
});
```
### Batch Example with Component
```typescript
import { Component } from '@angular/core';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-batch-crud',
template: `
<div>
<h3>Manage Orders</h3>
<button (click)="addOrder()">Add Order</button>
<button (click)="updateOrder()">Update Order</button>
<button (click)="deleteOrder()">Delete Order</button>
<button (click)="saveBatch()" style="font-weight:bold;">Save All Changes</button>
</div>
`
})
export class BatchCrudComponent {
private dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
private toAdd = [];
private toUpdate = [];
private toDelete = [];
addOrder(): void {
const newOrder = {
CustomerID: 'NEWCUST',
EmployeeID: 1,
Freight: 30
};
this.toAdd.push(newOrder);
console.log('Order added to batch');
}
updateOrder(): void {
const updatedOrder = {
OrderID: 10248,
Freight: 50
};
this.toUpdate.push(updatedOrder);
console.log('Order updated in batch');
}
deleteOrder(): void {
this.toDelete.push(10249);
console.log('Order deleted from batch');
}
async saveBatch(): Promise<void> {
try {
const result = await this.dataManager.saveChanges({
addedRecords: this.toAdd,
changedRecords: this.toUpdate,
deletedRecords: this.toDelete
});
console.log('Batch saved successfully:', result);
// Clear batches
this.toAdd = [];
this.toUpdate = [];
this.toDelete = [];
} catch (error) {
console.error('Batch save failed:', error);
}
}
}
```
---
## Response Handling
### Success Response
```typescript
dataManager.insert('Orders', newOrder)
.then((result) => {
// result = { AddedRecords: [...] } or similar
console.log('Insertion successful');
console.log('Response:', result);
});
```
### Response Structure
```typescript
interface InsertResponse {
AddedRecords?: any[];
ChangedRecords?: any[];
DeletedRecords?: any[];
}
dataManager.saveChanges({...})
.then((response: InsertResponse) => {
console.log('Added:', response.AddedRecords);
console.log('Changed:', response.ChangedRecords);
console.log('Deleted:', response.DeletedRecords);
});
```
---
## Error Handling
### HTTP Error Codes
```typescript
dataManager.update('Orders', updatedRecord)
.catch((error: any) => {
if (error.status === 400) {
console.error('Bad request - invalid data');
} else if (error.status === 401) {
console.error('Unauthorized - check authentication');
} else if (error.status === 404) {
console.error('Record not found');
} else if (error.status === 409) {
console.error('Conflict - record modified by another user');
} else if (error.status === 500) {
console.error('Server error - retry later');
}
});
```
### Validation Before CRUD
```typescript
function validateOrder(order: any): boolean {
if (!order.OrderID) {
console.error('OrderID is required');
return false;
}
if (!order.CustomerID) {
console.error('CustomerID is required');
return false;
}
if (order.Freight < 0) {
console.error('Freight cannot be negative');
return false;
}
return true;
}
if (validateOrder(newOrder)) {
dataManager.insert(newOrder, 'Orders');
}
```
### Optimistic Error Recovery
```typescript
const originalData = { ...recordToUpdate };
dataManager.update('OrderID', recordToUpdate, 'Orders')
.catch((error) => {
if (error.status === 409) {
console.warn('Update conflict - reverting to original data');
// Restore from originalData
} else {
console.error('Update failed:', error);
}
});
```
---
## Complete CRUD Example
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
interface Order {
OrderID?: number;
CustomerID: string;
EmployeeID: number;
Freight: number;
}
@Component({
selector: 'app-order-crud',
template: `
<div>
<form (ngSubmit)="onSubmit()">
<label>Customer ID: <input [(ngModel)]="form.CustomerID" name="customer"></label>
<label>Employee ID: <input type="number" [(ngModel)]="form.EmployeeID" name="employee"></label>
<label>Freight: <input type="number" [(ngModel)]="form.Freight" name="freight"></label>
<button type="submit">{{ isEdit ? 'Update' : 'Add' }}</button>
<button type="button" (click)="cancel()" *ngIf="isEdit">Cancel</button>
</form>
<table>
<thead>
<tr><th>ID</th><th>Customer</th><th>Employee</th><th>Freight</th><th>Actions</th></tr>
</thead>
<tbody>
<tr *ngFor="let order of orders">
<td>{{ order.OrderID }}</td>
<td>{{ order.CustomerID }}</td>
<td>{{ order.EmployeeID }}</td>
<td>{{ order.Freight }}</td>
<td>
<button (click)="onEdit(order)">Edit</button>
<button (click)="onDelete(order.OrderID)">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
`
})
export class OrderCrudComponent implements OnInit {
public orders: Order[] = [];
public form: Order = { CustomerID: '', EmployeeID: 0, Freight: 0 };
public isEdit = false;
public editingId: number | null = null;
private dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
ngOnInit(): void {
this.loadOrders();
}
async loadOrders(): Promise<void> {
try {
const result = await this.dataManager.executeQuery(new Query());
this.orders = result.result as Order[];
} catch (error) {
console.error('Load failed:', error);
}
}
async onSubmit(): Promise<void> {
try {
if (this.isEdit && this.editingId) {
await this.dataManager.update('OrderID', { OrderID: this.editingId, ...this.form }, 'Orders');
} else {
await this.dataManager.insert(this.form, 'Orders');
}
await this.loadOrders();
this.resetForm();
} catch (error) {
console.error('Save failed:', error);
}
}
onEdit(order: Order): void {
this.form = { ...order };
this.editingId = order.OrderID;
this.isEdit = true;
}
async onDelete(id: number): Promise<void> {
try {
await this.dataManager.remove('Orders', id);
await this.loadOrders();
} catch (error) {
console.error('Delete failed:', error);
}
}
cancel(): void {
this.resetForm();
}
private resetForm(): void {
this.form = { CustomerID: '', EmployeeID: 0, Freight: 0 };
this.isEdit = false;
this.editingId = null;
}
}
```
---
references/data-binding.md
---
title: Data Binding in Syncfusion Angular DataManager
---
# Data Binding in Syncfusion Angular DataManager
## Table of Contents
- [Overview](#overview)
- [Local Binding (JSON)](#local-binding-json)
- [Remote Binding (URL)](#remote-binding-url)
- [Client-side vs Server-side Processing](#client-side-vs-server-side-processing)
- [Promise-based Async Operations](#promise-based-async-operations)
- [Error Handling](#error-handling)
- [Property Reference](#property-reference)
---
## Overview
Data binding in DataManager determines where your data comes from and how it's processed:
- **Local Binding:** Data exists in memory (JSON array)
- **Remote Binding:** Data fetches from a server URL
Each binding type uses different DataManager properties and methods.
---
## Local Binding (JSON)
### When to Use Local Binding
- Working with in-memory data already fetched
- Small datasets that fit in browser memory
- Quick prototyping or testing
- Offline-capable application features
- Client-side filtering/sorting for better UX
### Configuration
```typescript
import { DataManager, JsonAdaptor, Query } from '@syncfusion/ej2-data';
const orders = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61 },
{ OrderID: 10250, CustomerID: 'HANAR', Freight: 65.83 }
];
const dataManager = new DataManager({
json: orders, // Local data array
adaptor: new JsonAdaptor() // Handles local operations
});
```
### Basic Operations
```typescript
// Fetch all records
const all = dataManager.executeLocal(new Query());
// Filter records
const filtered = dataManager.executeLocal(
new Query().where('Freight', 'greaterThan', 30)
);
// Sort and paginate
const sorted = dataManager.executeLocal(
new Query()
.sortBy('Freight')
.take(5)
.skip(0)
);
// Combine operations
const result = dataManager.executeLocal(
new Query()
.where('CustomerID', 'equal', 'VINET')
.select(['OrderID', 'Freight'])
.sortBy('OrderID')
);
```
### Key Properties
| Property | Type | Purpose |
|----------|------|---------|
| `json` | `array` | Your local data array |
| `adaptor` | `JsonAdaptor` | Handles json property |
### Advantages
- ✓ No network latency
- ✓ Works offline
- ✓ Instant filtering/sorting
- ✓ Simpler error handling
### Limitations
- ✗ Limited to data already loaded
- ✗ Memory constraints for large datasets
- ✗ Must load all data at once
---
## Remote Binding (URL)
### When to Use Remote Binding
- Data stored on a server (REST API, OData, GraphQL)
- Large datasets too big for memory
- Real-time data that changes frequently
- Need server-side processing (filtering, sorting)
- Security-sensitive operations
### Configuration
```typescript
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url', // API endpoint
adaptor: new WebApiAdaptor(), // Handles HTTP requests
crossDomain: true
});
```
### Basic Operations
```typescript
// Fetch with promise
dataManager.executeQuery(new Query())
.then((result) => {
console.log(result.result); // Data array
console.log(result.count); // Total records
})
.catch((error) => {
console.error('Error:', error);
});
// With filtering
dataManager.executeQuery(
new Query().where('Freight', 'greaterThan', 50)
)
.then((result) => {
this.items = result.result;
});
// With paging
dataManager.executeQuery(
new Query().take(10).skip(0)
)
.then((result) => {
this.items = result.result;
this.totalCount = result.count;
});
```
### Key Properties
| Property | Type | Purpose |
|----------|------|---------|
| `url` | `string` | API endpoint URL |
| `adaptor` | `Adaptor` | Type of data service (WebApiAdaptor, ODataAdaptor, etc.) |
| `headers` | `object[]` | Custom HTTP headers |
| `crossDomain` | `boolean` | Enable CORS requests |
| `offline` | `boolean` | Enable offline mode |
### Supported Adaptors for Remote Binding
- **WebApiAdaptor** — ASP.NET Web API
- **ODataAdaptor** — OData v3 services
- **ODataV4Adaptor** — OData v4 endpoints
- **UrlAdaptor** — Generic REST endpoints
- **WebMethodAdaptor** — ASP.NET web methods
- **GraphQLAdaptor** — GraphQL servers
- **CustomDataAdaptor** — Custom fetch logic
- **RemoteSaveAdaptor** — Client-side queries + server CRUD
### Advantages
- ✓ Handle unlimited data volume
- ✓ Real-time data
- ✓ Server-side processing (faster, efficient)
- ✓ Secure operations on server
### Limitations
- ✗ Network delays
- ✗ Requires server connection
- ✗ More complex error handling
---
## Client-side vs Server-side Processing
### Client-side Processing (executeLocal)
**Use: Local binding with executeLocal()**
```typescript
const dataManager = new DataManager({
json: largeArray,
adaptor: new JsonAdaptor()
});
// Processing happens in browser
const result = dataManager.executeLocal(
new Query()
.where('Status', 'equal', 'Active')
.sortBy('Date')
.take(10)
);
```
**Characteristics:**
- All data loaded first
- Filtering/sorting in browser
- Instant results
- Limited to memory size
---
### Server-side Processing (executeQuery)
**Use: Remote binding with executeQuery()**
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Request sent to server with query parameters
// Server filters, sorts, and returns only needed data
const result = await dataManager.executeQuery(
new Query()
.where('Status', 'equal', 'Active')
.sortBy('Date')
.take(10)
);
```
**Characteristics:**
- Only needed data downloaded
- Server handles heavy operations
- Better for large datasets
- More efficient bandwidth
- Scales better with size
---
## Promise-based Async Operations
### Using .then().catch()
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dataManager.executeQuery(new Query())
.then((result) => {
// Success: result contains data
this.items = result.result;
this.totalCount = result.count;
})
.catch((error) => {
// Failure: handle error
console.error('Data fetch failed:', error);
});
```
### Using Async/Await
```typescript
async loadData(): Promise<void> {
try {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dataManager.executeQuery(new Query());
this.items = result.result;
} catch (error) {
console.error('Error:', error);
}
}
```
### Multiple Sequential Operations
```typescript
// Pattern: Load data, then load related data
async loadOrderAndItems(orderId: number): Promise<void> {
try {
const orderDm = new DataManager({
url: `url/${orderId}`,
adaptor: new WebApiAdaptor()
});
const order = await orderDm.executeQuery(new Query());
this.order = order.result;
// Then load related items
const itemsDm = new DataManager({
url: `url/${orderId}/items`,
adaptor: new WebApiAdaptor()
});
const items = await itemsDm.executeQuery(new Query());
this.items = items.result;
} catch (error) {
console.error('Error:', error);
}
}
```
### Parallel Operations with Promise.all
```typescript
async loadAllData(): Promise<void> {
try {
const ordersDm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const customersDm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Load both simultaneously
const [ordersResult, customersResult] = await Promise.all([
ordersDm.executeQuery(new Query()),
customersDm.executeQuery(new Query())
]);
this.orders = ordersResult.result;
this.customers = customersResult.result;
} catch (error) {
console.error('Error:', error);
}
}
```
---
## Error Handling
### Basic Error Handling
```typescript
dataManager.executeQuery(query)
.then((result) => {
this.items = result.result;
})
.catch((error) => {
console.error('Data fetch failed:', error);
});
```
### HTTP Status Code Handling
```typescript
dataManager.executeQuery(query)
.catch((error) => {
if (error.status === 401) {
// Handle unauthorized
this.redirectToLogin();
} else if (error.status === 403) {
// Handle forbidden
console.error('Access denied');
} else if (error.status === 404) {
// Handle not found
console.error('Endpoint not found');
} else if (error.status === 500) {
// Handle server error
console.error('Server error');
} else if (error.status === 0) {
// Handle network error
console.error('Network connectivity issue');
} else {
console.error('Unknown error:', error);
}
});
```
### Async/Await Error Handling
```typescript
async loadData(): Promise<void> {
try {
const result = await dataManager.executeQuery(new Query());
this.items = result.result;
} catch (error: any) {
if (error.status === 401) {
// Redirect to login
} else if (error.status >= 500) {
// Show server error message
this.errorMessage = 'Server error - please try again later';
} else {
// Generic error
this.errorMessage = 'Failed to load data';
}
}
}
```
---
## Complete Example: Remote Data with Error Handling
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, Query, WebApiAdaptor, ReturnOption } from '@syncfusion/ej2-data';
interface Order {
OrderID: number;
CustomerID: string;
Freight: number;
}
@Component({
selector: 'app-remote-orders',
template: `
<div>
<h3>Orders</h3>
<div *ngIf="errorMessage" class="error">{{ errorMessage }}</div>
<div *ngIf="loading" class="loader">Loading...</div>
<table *ngIf="!loading && orders.length">
<thead>
<tr>
<th>Order ID</th>
<th>Customer</th>
<th>Freight</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let order of orders">
<td>{{ order.OrderID }}</td>
<td>{{ order.CustomerID }}</td>
<td>{{ order.Freight | currency }}</td>
</tr>
</tbody>
</table>
</div>
`,
styles: [`
.error { color: red; padding: 10px; background: #ffebee; }
.loader { text-align: center; padding: 20px; }
`]
})
export class RemoteOrdersComponent implements OnInit {
public orders: Order[] = [];
public loading = false;
public errorMessage = '';
ngOnInit(): void {
this.loadOrders();
}
loadOrders(): void {
this.loading = true;
this.errorMessage = '';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dataManager.executeQuery(
new Query().select(['OrderID', 'CustomerID', 'Freight']).take(20)
)
.then((result: ReturnOption) => {
this.orders = result.result as Order[];
this.loading = false;
})
.catch((error) => {
this.loading = false;
if (error.status === 0) {
this.errorMessage = 'Network error - check your connection';
} else if (error.status === 404) {
this.errorMessage = 'Service not found';
} else {
this.errorMessage = 'Failed to load orders';
}
});
}
}
```
---
## Property Reference
| Property | Type | Local | Remote | Purpose |
|----------|------|-------|--------|---------|
| `json` | array | ✓ | ✗ | Local data array |
| `url` | string | ✗ | ✓ | Remote endpoint |
| `adaptor` | class | ✓ | ✓ | Adaptor type |
| `headers` | object[] | ✗ | ✓ | HTTP headers |
| `offline` | boolean | ✗ | ✓ | Offline support |
| `enableCache` | boolean | ✗ | ✓ | Response caching |
| `crossDomain` | boolean | ✗ | ✓ | CORS requests |
---
references/getting-started.md
---
title: Getting Started with Syncfusion Angular DataManager
---
# Getting Started with Syncfusion Angular DataManager
## Table of Contents
- [Installation](#installation)
- [Package Dependencies](#package-dependencies)
- [TypeScript Imports](#typescript-imports)
- [Creating Your First DataManager](#creating-your-first-datamanager)
- [executeLocal vs executeQuery](#executelocal-vs-executequery)
- [Handling Results](#handling-results)
---
## Installation
Install the Syncfusion data module using npm:
```bash
npm install @syncfusion/ej2-data
```
### Verify Installation
Check your package.json to confirm the package is listed:
```json
{
"dependencies": {
"@syncfusion/ej2-data": "^20.0.0 or higher"
}
}
```
---
## Package Dependencies
The `@syncfusion/ej2-data` package includes:
- **DataManager** — Core data management class
- **Query** — Query builder for filtering, sorting, paging
- **Adaptors** — Multiple adaptor types (Json, OData, Rest, etc.)
- **ReturnOption** — TypeScript interface for results
### Optional Dependencies
For HTTP requests, install Angular's HTTP client:
```bash
npm install @angular/common
```
---
## TypeScript Imports
### Basic Imports for Local Data
```typescript
import { DataManager, Query, JsonAdaptor } from '@syncfusion/ej2-data';
// For results type safety
import { ReturnOption } from '@syncfusion/ej2-data';
```
### Imports for Remote Data
```typescript
import {
DataManager,
Query,
WebApiAdaptor, // or ODataAdaptor, UrlAdaptor, etc.
ReturnOption
} from '@syncfusion/ej2-data';
```
### Complete Component Imports
```typescript
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
DataManager,
Query,
WebApiAdaptor,
ReturnOption
} from '@syncfusion/ej2-data';
```
---
## Creating Your First DataManager
### Local Data (JSON Array)
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, Query, JsonAdaptor, ReturnOption } from '@syncfusion/ej2-data';
@Component({
selector: 'app-local-data',
templateUrl: './local-data.component.html'
})
export class LocalDataComponent implements OnInit {
public items: object[];
ngOnInit(): void {
// Step 1: Define local data
const data = [
{ OrderID: 10248, CustomerID: 'VINET', EmployeeID: 5 },
{ OrderID: 10249, CustomerID: 'TOMSP', EmployeeID: 6 },
{ OrderID: 10250, CustomerID: 'HANAR', EmployeeID: 4 }
];
// Step 2: Create DataManager
const dataManager = new DataManager({
json: data,
adaptor: new JsonAdaptor()
});
// Step 3: Execute query
const result = dataManager.executeLocal(new Query().take(2));
this.items = result;
}
}
```
### Remote Data (API Endpoint)
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, Query, WebApiAdaptor, ReturnOption } from '@syncfusion/ej2-data';
@Component({
selector: 'app-remote-data',
templateUrl: './remote-data.component.html'
})
export class RemoteDataComponent implements OnInit {
public items: object[];
ngOnInit(): void {
// Step 1: Create DataManager with remote URL
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Step 2: Execute query
dataManager.executeQuery(new Query().take(5))
.then((e: ReturnOption) => {
this.items = e.result;
})
.catch((error: any) => {
console.error('Error:', error);
});
}
}
```
---
## executeLocal vs executeQuery
### executeLocal — Client-side Processing
**Use when:**
- Data is already in memory (JSON array)
- You want instant operations without server calls
- Working offline
```typescript
const data = [
{ id: 1, name: 'Product A' },
{ id: 2, name: 'Product B' },
{ id: 3, name: 'Product C' }
];
const dm = new DataManager({ json: data, adaptor: new JsonAdaptor() });
// Returns immediately (synchronous)
const result = dm.executeLocal(
new Query().where('id', 'greaterThan', 1)
);
// result = [{ id: 2, name: 'Product B' }, { id: 3, name: 'Product C' }]
```
### executeQuery — Server-side Processing
**Use when:**
- Data is on remote server
- You need server-side filtering/sorting
- Large datasets (pagination on server)
```typescript
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Returns Promise (asynchronous)
dm.executeQuery(new Query().take(10))
.then((result: ReturnOption) => {
console.log(result.result); // Array of records
console.log(result.count); // Total record count
})
.catch((error) => console.error(error));
```
### Key Differences Table
| Aspect | executeLocal | executeQuery |
|--------|-------------|-------------|
| **Data Source** | In-memory JSON | Remote server |
| **Processing** | Client-side | Server-side |
| **Return Type** | Direct array | Promise |
| **Speed** | Instant | Network delay |
| **Best For** | Small datasets | Large datasets |
| **Offline** | Works offline | Requires connection |
---
## Handling Results
### Processing Local Results
```typescript
// Direct array result
const data = [
{ id: 1, value: 'A' },
{ id: 2, value: 'B' }
];
const dm = new DataManager({ json: data, adaptor: new JsonAdaptor() });
const result = dm.executeLocal(new Query().take(1));
console.log(result); // [{ id: 1, value: 'A' }]
console.log(result[0].id); // 1
```
### Processing Remote Results
```typescript
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dm.executeQuery(new Query().take(10))
.then((result: ReturnOption) => {
// result object structure:
// {
// result: [ /* array of records */ ],
// count: 100, // total records on server
// aggregates: { /* aggregate values */ }
// }
this.items = result.result as any[];
this.totalCount = result.count;
})
.catch((error) => {
console.error('Failed to fetch data:', error);
});
```
### Typed Results with TypeScript
```typescript
interface Product {
id: number;
name: string;
price: number;
}
async getProducts(): Promise<void> {
try {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query());
const products = result.result as Product[];
products.forEach(p => {
console.log(`${p.name}: $${p.price}`);
});
} catch (error) {
console.error('Error:', error);
}
}
```
---
## Error Handling
### Basic Error Handling
```typescript
dm.executeQuery(query)
.then((result: ReturnOption) => {
this.items = result.result;
})
.catch((error) => {
console.error('Data fetch failed:', error);
});
```
### Detailed Error Handling
```typescript
dm.executeQuery(query)
.catch((error: any) => {
if (error.statusCode === 404) {
console.error('Endpoint not found');
} else if (error.statusCode === 401) {
console.error('Unauthorized - check authentication');
} else if (error.statusCode === 500) {
console.error('Server error');
} else {
console.error('Unknown error:', error);
}
});
```
---
## Complete Example: Orders Component
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, Query, WebApiAdaptor, ReturnOption } from '@syncfusion/ej2-data';
interface Order {
OrderID: number;
CustomerID: string;
EmployeeID: number;
Freight: number;
}
@Component({
selector: 'app-orders',
template: `
<div>
<h3>Orders</h3>
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Customer</th>
<th>Employee</th>
<th>Freight</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let order of orders">
<td>{{ order.OrderID }}</td>
<td>{{ order.CustomerID }}</td>
<td>{{ order.EmployeeID }}</td>
<td>{{ order.Freight | currency }}</td>
</tr>
</tbody>
</table>
</div>
`
})
export class OrdersComponent implements OnInit {
public orders: Order[] = [];
ngOnInit(): void {
this.loadOrders();
}
loadOrders(): void {
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
});
dataManager.executeQuery(
new Query().select(['OrderID', 'CustomerID', 'EmployeeID', 'Freight']).take(10)
)
.then((result: ReturnOption) => {
this.orders = result.result as Order[];
})
.catch((error) => {
console.error('Failed to load orders:', error);
});
}
}
```
---
references/how-to-recipes.md
---
title: How-To Recipes for Syncfusion Angular DataManager
---
# How-To Recipes: Common Patterns and Solutions
## Table of Contents
- [Work in Offline Mode](#work-in-offline-mode)
- [Send Additional Parameters](#send-additional-parameters)
- [Add Custom Headers](#add-custom-headers)
- [Implement Authentication](#implement-authentication)
- [Handle Errors Gracefully](#handle-errors-gracefully)
- [Implement Search](#implement-search)
- [Sort by Multiple Fields](#sort-by-multiple-fields)
- [Group and Aggregate](#group-and-aggregate)
- [Real-time Data Updates](#real-time-data-updates)
---
## Work in Offline Mode
### Recipe: Load Data Once, Work Without Server
```typescript
// Complete implementation
@Component({
selector: 'app-offline-recipe',
template: `
<button (click)="toggleOnline()">{{ isOnline ? 'Go Offline' : 'Go Online' }}</button>
<input [(ngModel)]="filterText" placeholder="Search (works offline too)">
<table>
<tr *ngFor="let item of filteredItems">
<td>{{ item.OrderID }}</td>
<td>{{ item.CustomerID }}</td>
</tr>
</table>
`
})
export class OfflineRecipeComponent implements OnInit {
public isOnline = true;
public filterText = '';
public items: any[] = [];
get filteredItems(): any[] {
if (!this.filterText) return this.items;
const dm = new DataManager({ json: this.items, adaptor: new JsonAdaptor() });
return dm.executeLocal(
new Query().where('CustomerID', 'contains', this.filterText)
);
}
ngOnInit(): void {
this.loadData();
}
private async loadData(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true // Load all data, enable offline
});
try {
// Data is loaded with offline: true
// Execute a query to get all cached records
const result = dm.executeLocal(new Query());
this.items = result || [];
console.log(`Loaded ${this.items.length} records for offline use`);
} catch (error) {
console.error('Failed to load data:', error);
}
}
toggleOnline(): void {
this.isOnline = !this.isOnline;
console.log(this.isOnline ? 'Online mode' : 'Offline mode');
}
}
```
---
## Send Additional Parameters
### Recipe: Pass Custom Query Parameters to API
```typescript
// The user.created the API and wants to send custom parameters
// API expects: /api/orders?department=sales®ion=north
@Component({
selector: 'app-params-recipe'
})
export class ParamsRecipeComponent implements OnInit {
ngOnInit(): void {
this.loadWithParameters();
}
private loadWithParameters(): void {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const query = new Query()
.addParams('department', 'sales')
.addParams('region', 'north')
.take(10);
dm.executeQuery(query)
.then((result) => {
console.log('Results:', result.result);
})
.catch((error) => {
console.error('Error:', error);
});
}
}
// Result URL: /api/orders?department=sales®ion=north&$take=10
```
---
## Add Custom Headers
### Recipe: Send Authorization and Custom Headers
```typescript
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-headers-recipe'
})
export class HeadersRecipeComponent implements OnInit {
constructor(private authService: AuthService) {}
ngOnInit(): void {
this.loadWithHeaders();
}
private loadWithHeaders(): void {
const token = this.authService.getToken();
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
dm.executeQuery(new Query().take(10))
.then((result) => {
console.log('Data with headers:', result.result);
});
}
}
// HTTP Request headers:
// Authorization: send_token
// X-API-Version: 2.0
// X-Custom-Header: custom-value
// Accept-Language: en-US
```
---
## Implement Authentication
### Recipe: JWT Token-based Authentication
```typescript
@Injectable({ providedIn: 'root' })
export class AuthenticatedDataService {
private token: string;
constructor(private http: HttpClient, private auth: AuthService) {
this.token = this.auth.getToken();
this.auth.tokenRefreshed$.subscribe(newToken => {
this.token = newToken;
});
}
getDataManager(url: string): DataManager {
return new DataManager({
url: url,
adaptor: new WebApiAdaptor()
});
}
async loadSecuredData(endpoint: string): Promise<any> {
const dm = this.getDataManager(endpoint);
try {
const result = await dm.executeQuery(new Query().take(10));
return result.result;
} catch (error: any) {
if (error.status === 401) {
// Token expired, refresh and retry
await this.auth.refreshToken();
return this.loadSecuredData(endpoint);
}
throw error;
}
}
}
// Usage in Component
@Component({
selector: 'app-auth-recipe'
})
export class AuthRecipeComponent implements OnInit {
public data: any[] = [];
constructor(private dataService: AuthenticatedDataService) {}
async ngOnInit(): Promise<void> {
try {
this.data = await this.dataService.loadSecuredData('url');
} catch (error) {
console.error('Failed to load secured data:', error);
}
}
}
```
---
## Handle Errors Gracefully
### Recipe: Comprehensive Error Handling
```typescript
@Component({
selector: 'app-error-recipe',
template: `
<div *ngIf="error" class="error-box">
<h3>{{ error.title }}</h3>
<p>{{ error.message }}</p>
<button (click)="retry()" *ngIf="error.retryable">Retry</button>
</div>
<div *ngIf="!error && loading" class="loader">Loading...</div>
<div *ngIf="!error && !loading && items.length">
<table>
<tr *ngFor="let item of items">
<td>{{ item.OrderID }}</td>
</tr>
</table>
</div>
`
})
export class ErrorRecipeComponent {
public items: any[] = [];
public loading = false;
public error: any = null;
async loadData(): Promise<void> {
this.loading = true;
this.error = null;
try {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query().take(10));
this.items = result.result;
} catch (err: any) {
this.error = this.parseError(err);
} finally {
this.loading = false;
}
}
private parseError(error: any): any {
const status = error.status || 0;
const errorMap: { [key: number]: any } = {
0: {
title: 'Network Error',
message: 'Please check your internet connection',
retryable: true
},
400: {
title: 'Bad Request',
message: 'Invalid query parameters',
retryable: false
},
401: {
title: 'Unauthorized',
message: 'Please login again',
retryable: false
},
404: {
title: 'Not Found',
message: 'The requested resource does not exist',
retryable: false
},
500: {
title: 'Server Error',
message: 'The server encountered an error. Please try again later',
retryable: true
}
};
return errorMap[status] || {
title: 'Unknown Error',
message: `Error code: ${status}`,
retryable: true
};
}
async retry(): Promise<void> {
await this.loadData();
}
}
```
---
## Implement Search
### Recipe: Real-time Search Across Multiple Fields
```typescript
@Component({
selector: 'app-search-recipe',
template: `
<input
[(ngModel)]="searchText"
(ngModelChange)="onSearchChange()"
placeholder="Search orders...">
<div *ngIf="searching" class="loader">Searching...</div>
<table>
<tr *ngFor="let item of searchResults">
<td>{{ item.OrderID }}</td>
<td>{{ item.CustomerID }}</td>
<td>{{ item.ShipCity }}</td>
</tr>
</table>
<p *ngIf="!searching && searchResults.length === 0">No results found</p>
`
})
export class SearchRecipeComponent {
public searchText = '';
public searchResults: any[] = [];
public searching = false;
private allData: any[] = [];
private searchTimeout: any;
async ngOnInit(): Promise<void> {
// Load all data once
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor(),
offline: true
});
const result = await dm.ready;
this.allData = result.result || [];
}
onSearchChange(): void {
// Debounce search
clearTimeout(this.searchTimeout);
if (!this.searchText.trim()) {
this.searchResults = [];
return;
}
this.searching = true;
this.searchTimeout = setTimeout(() => {
const dm = new DataManager({
json: this.allData,
adaptor: new JsonAdaptor()
});
// Search across multiple fields
this.searchResults = dm.executeLocal(
new Query().search(this.searchText, [
'CustomerID',
'ShipCity',
'ShipCountry'
])
);
this.searching = false;
}, 300); // Debounce 300ms
}
}
```
---
## Sort by Multiple Fields
### Recipe: Multi-field Sorting with User Interface
```typescript
@Component({
selector: 'app-sort-recipe',
template: `
<div class="sort-controls">
<button (click)="toggleSort('CustomerID')">
Customer {{ getSortIndicator('CustomerID') }}
</button>
<button (click)="toggleSort('Freight')">
Freight {{ getSortIndicator('Freight') }}
</button>
<button (click)="resetSort()">Reset</button>
</div>
<table>
<tr *ngFor="let item of sortedItems">
<td>{{ item.OrderID }}</td>
<td>{{ item.CustomerID }}</td>
<td>{{ item.Freight }}</td>
</tr>
</table>
`
})
export class SortRecipeComponent {
public items: any[] = [];
private sortFields: Map<string, 'asc' | 'desc'> = new Map();
async ngOnInit(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query());
this.items = result.result;
}
toggleSort(field: string): void {
const current = this.sortFields.get(field);
if (!current) {
this.sortFields.set(field, 'asc');
} else if (current === 'asc') {
this.sortFields.set(field, 'desc');
} else {
this.sortFields.delete(field);
}
this.applySort();
}
private applySort(): void {
const dm = new DataManager({
json: this.items,
adaptor: new JsonAdaptor()
});
let query = new Query();
this.sortFields.forEach((direction, field) => {
if (direction === 'asc') {
query = query.sortBy(field);
} else {
query = query.sortByDescending(field);
}
});
this.items = dm.executeLocal(query);
}
getSortIndicator(field: string): string {
const direction = this.sortFields.get(field);
return direction === 'asc' ? '↑' : direction === 'desc' ? '↓' : '';
}
resetSort(): void {
this.sortFields.clear();
this.loadItems();
}
private async loadItems(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query());
this.items = result.result;
}
}
```
---
## Group and Aggregate
### Recipe: Grouping with Aggregate Calculations
```typescript
@Component({
selector: 'app-group-recipe'
})
export class GroupRecipeComponent {
public groupedData: any[] = [];
async ngOnInit(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
const result = await dm.executeQuery(new Query().group('CustomerID'));
// Transform grouped result
this.groupedData = result.result.map((group: any) => ({
customerID: group.key,
count: group.items.length,
totalFreight: group.items.reduce((sum: number, item: any) => sum + item.Freight, 0),
avgFreight: group.items.reduce((sum: number, item: any) => sum + item.Freight, 0) / group.items.length,
items: group.items
}));
}
// Template to display grouped data
template = `
<div *ngFor="let group of groupedData">
<h4>{{ group.customerID }}</h4>
<p>Orders: {{ group.count }}, Total: {{ group.totalFreight | currency }}, Avg: {{ group.avgFreight | currency }}</p>
<ul>
<li *ngFor="let item of group.items">
Order {{ item.OrderID }}: {{ item.Freight | currency }}
</li>
</ul>
</div>
`;
}
```
---
## Real-time Data Updates
### Recipe: Polling for New Data
```typescript
@Component({
selector: 'app-realtime-recipe'
})
export class RealtimeRecipeComponent implements OnInit, OnDestroy {
public items: any[] = [];
private pollInterval: any;
private pollFrequency = 30000; // 30 seconds
ngOnInit(): void {
this.startPolling();
}
private startPolling(): void {
this.pollInterval = setInterval(() => {
this.refreshData();
}, this.pollFrequency);
// Initial load
this.refreshData();
}
private async refreshData(): Promise<void> {
const dm = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
try {
const result = await dm.executeQuery(
new Query().orderByDescending('OrderDate').take(10)
);
this.items = result.result;
console.log('Data refreshed at', new Date().toLocaleTimeString());
} catch (error) {
console.error('Refresh failed:', error);
}
}
ngOnDestroy(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
}
}
```
### Recipe: WebSocket Real-time Updates
```typescript
@Component({
selector: 'app-websocket-recipe'
})
export class WebsocketRecipeComponent implements OnInit, OnDestroy {
public items: any[] = [];
private ws: WebSocket;
ngOnInit(): void {
this.connectWebSocket();
}
private connectWebSocket(): void {
this.ws = new WebSocket('url');
this.ws.onopen = () => {
console.log('WebSocket connected');
};
this.ws.onmessage = (event) => {
const update = JSON.parse(event.data);
this.handleUpdate(update);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
private handleUpdate(update: any): void {
const dm = new DataManager({
json: this.items,
adaptor: new JsonAdaptor()
});
if (update.operation === 'insert') {
this.items.push(update.data);
} else if (update.operation === 'update') {
const index = this.items.findIndex(item => item.OrderID === update.data.OrderID);
if (index >= 0) {
this.items[index] = update.data;
}
} else if (update.operation === 'delete') {
this.items = this.items.filter(item => item.OrderID !== update.id);
}
console.log('Data updated via WebSocket');
}
ngOnDestroy(): void {
if (this.ws) {
this.ws.close();
}
}
}
```
---
references/middleware-customization.md
---
title: Middleware Customization in Syncfusion Angular DataManager
---
# Middleware Customization in Syncfusion Angular DataManager
## Table of Contents
- [Overview](#overview)
- [Pre-Request Middleware](#pre-request-middleware)
- [Post-Request Middleware](#post-request-middleware)
- [Custom Headers](#custom-headers)
- [Authentication Patterns](#authentication-patterns)
- [Request/Response Transformation](#requestresponse-transformation)
- [Error Handling](#error-handling)
- [Complete Example](#complete-example)
---
## Overview
Middleware in DataManager allows you to intercept and modify:
- **Pre-request:** Executed before sending request to server
- **Post-request:** Executed after receiving response
This enables:
- Adding authentication headers
- Transforming request/response data
- Logging and monitoring
- Custom business logic
- Error handling and retry logic
---
## Pre-Request Middleware
### applyPreRequestMiddlewares()
Modify the request BEFORE it's sent to the server. The `applyPreRequestMiddlewares` method runs before a request is sent to the backend. It allows you to modify request headers, query parameters, or payloads.
### Basic Usage - Add Authorization Token
```typescript
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Apply pre-request middleware to add authentication token
dataManager.applyPreRequestMiddlewares([
async (context) => {
context.request.headers['Authorization'] = 'send_token';
}
]);
```
### Advanced - Dynamic Header from Server
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
}).executeQuery(new Query().take(8));
// Fetch token from authentication server before each request
dataManager.applyPreRequestMiddlewares = async (request: string | Object): Promise<object> => {
const response = await fetch('url', {
method: 'POST'
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const tokenData = await response.json();
return { token: tokenData.access_token };
};
```
### Example - Add Timestamp Header
```typescript
dataManager.applyPreRequestMiddlewares([
async (context) => {
context.request.headers['X-Timestamp'] = new Date().toISOString();
context.request.headers['X-Request-ID'] = generateRequestId();
}
]);
function generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
```
### Add Authorization Header
```typescript
class AuthenticatedAdaptor extends WebApiAdaptor {
constructor(private token: string) {
super();
}
beforeSend(dm: DataManager, request: any): void {
super.beforeSend(dm, request);
if (request instanceof XMLHttpRequest) {
request.setRequestHeader('Authorization', `send_token`);
}
}
}
const dataManager = new DataManager({
url: 'url',
adaptor: new AuthenticatedAdaptor('your-jwt-token')
});
```
### Transform Request Data
```typescript
class TransformAdaptor extends WebApiAdaptor {
processQuery(dm: DataManager, query: Query, params?: any): any {
const request = super.processQuery(dm, query, params);
// Transform parameters for custom API format
const transformed = {
...request,
// Convert skip/take to page/size for some APIs
page: Math.floor(request.skip / request.take) + 1,
pageSize: request.take
};
return transformed;
}
}
```
---
## Post-Request Middleware
### applyPostRequestMiddlewares()
Modify the response AFTER it's received from the server. The `applyPostRequestMiddlewares` method runs after a response is received from the server but before binding the data to a component. It allows you to modify, filter, or restructure response data as needed.
### Basic Usage - Transform Response Data
```typescript
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Apply post-request middleware to format response data
dataManager.applyPostRequestMiddlewares([
async (context) => {
// Transform each item in the response
if (context.response.result) {
context.response.result = context.response.result.map(item => ({
id: item.Id,
name: item.Name.toUpperCase(),
date: new Date(item.Timestamp).toLocaleDateString()
}));
}
}
]);
```
### Parse Nested Response Structure
```typescript
// Server returns { data: { items: [...], total: N } }
dataManager.applyPostRequestMiddlewares([
async (context) => {
const originalResponse = context.response;
// Normalize your custom response format to DataManager format
context.response = {
result: originalResponse.data.items,
count: originalResponse.data.total
};
}
]);
```
### Parse Nested Response
```typescript
class NestedResponseAdaptor extends WebApiAdaptor {
processResponse(response: any, dm?: DataManager): any {
// Server returns { data: { items: [...], total: N } }
const normalizedResponse = {
result: response.data.items,
count: response.data.total
};
return super.processResponse(normalizedResponse, dm);
}
}
```
### Format Data Types
```typescript
class TypeFormatterAdaptor extends WebApiAdaptor {
processResponse(response: any, dm?: DataManager): any {
const result = super.processResponse(response, dm);
if (result.result) {
result.result = result.result.map((item: any) => ({
...item,
orderDate: new Date(item.orderDate), // String to Date
freight: parseFloat(item.freight), // String to Number
isActive: item.isActive === 'true' // String to Boolean
}));
}
return result;
}
}
```
---
## Custom Headers
### Add Static Headers
```typescript
import { DataManager, WebApiAdaptor } from '@syncfusion/ej2-data';
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
```
### Add Dynamic Headers
```typescript
class DynamicHeaderAdaptor extends WebApiAdaptor {
constructor(private userId: string) {
super();
}
beforeSend(dm: DataManager, request: any): void {
super.beforeSend(dm, request);
if (request instanceof XMLHttpRequest) {
request.setRequestHeader('X-User-ID', this.userId);
request.setRequestHeader('X-Request-Time', new Date().toISOString());
request.setRequestHeader('X-Trace-ID', this.generateTraceId());
}
}
generateTraceId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
```
### Content Negotiation
```typescript
class ContentNegotiationAdaptor extends WebApiAdaptor {
beforeSend(dm: DataManager, request: any): void {
super.beforeSend(dm, request);
if (request instanceof XMLHttpRequest) {
request.setRequestHeader('Accept', 'application/json;charset=utf-8');
request.setRequestHeader('Content-Type', 'application/json;charset=utf-8');
}
}
}
```
---
## Authentication Patterns
### JWT Token Authentication
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class DataService {
private token: string;
constructor(private http: HttpClient) {}
getAuthenticatedDataManager(): DataManager {
// Get token from storage or auth service
this.token = localStorage.getItem('auth_token') || '';
return new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
}
}
```
### Refresh Token Logic
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
class RefreshTokenAdaptor extends WebApiAdaptor {
constructor(private authService: AuthService) {
super();
}
processResponse(response: any, dm?: DataManager): any {
// Handle 401 Unauthorized
if (response.status === 401) {
// Refresh token and retry
this.authService.refreshToken()
.then(newToken => {
localStorage.setItem('auth_token', newToken);
// Retry request with new token
});
}
return super.processResponse(response, dm);
}
}
```
### Session Validation
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
class SessionAdaptor extends WebApiAdaptor {
beforeSend(dm: DataManager, request: any): void {
super.beforeSend(dm, request);
// Check session validity
const sessionId = sessionStorage.getItem('session_id');
if (!sessionId) {
throw new Error('Session expired - please login again');
}
if (request instanceof XMLHttpRequest) {
request.setRequestHeader('X-Session-ID', sessionId);
}
}
}
```
---
## Request/Response Transformation
### Transform Before Sending
```typescript
class RequestTransformAdaptor extends WebApiAdaptor {
processQuery(dm: DataManager, query: Query, params?: any): any {
const request = super.processQuery(dm, query, params);
// Your API uses different format
return {
page: request.skip / request.take,
size: request.take,
filter: request.filter ? JSON.stringify(request.filter) : null,
sort: request.sorted
};
}
}
```
### Transform After Receiving
```typescript
class ResponseTransformAdaptor extends WebApiAdaptor {
processResponse(response: any, dm?: DataManager): any {
// Server returns different structure
const transformed = {
result: response.payload || [],
count: response.pagination?.total || 0
};
return super.processResponse(transformed, dm);
}
}
```
### Complete Transformation Example
```typescript
class CompletTransformAdaptor extends WebApiAdaptor {
processQuery(dm: DataManager, query: Query): any {
// Request transformation
const request = super.processQuery(dm, query);
return {
offset: request.skip,
limit: request.take,
where: request.where,
order: request.sorted?.map((s: any) => ({
field: s.name,
direction: s.direction
}))
};
}
processResponse(response: any, dm?: DataManager): any {
// Response transformation
const result = {
result: response.items || [],
count: response.totalCount || 0
};
return super.processResponse(result, dm);
}
}
```
---
## Error Handling
### Catch Request Errors
```typescript
class ErrorHandlingAdaptor extends WebApiAdaptor {
processResponse(response: any, dm?: DataManager): any {
try {
return super.processResponse(response, dm);
} catch (error: any) {
console.error('Response processing error:', error);
if (error.status === 404) {
console.error('Resource not found');
} else if (error.status === 500) {
console.error('Server error - retrying...');
// Implement retry logic
}
throw error;
}
}
}
```
### Retry Failed Requests
```typescript
class RetryAdaptor extends WebApiAdaptor {
private retryCount = 0;
private maxRetries = 3;
processResponse(response: any, dm?: DataManager): any {
if (response.status === 500 && this.retryCount < this.maxRetries) {
this.retryCount++;
console.log(`Retry attempt ${this.retryCount}/${this.maxRetries}`);
// Implement exponential backoff
setTimeout(() => {
// Retry logic
}, Math.pow(2, this.retryCount) * 1000);
} else {
this.retryCount = 0;
}
return super.processResponse(response, dm);
}
}
```
### Graceful Error Messages
```typescript
class UserFriendlyErrorAdaptor extends WebApiAdaptor {
processResponse(response: any, dm?: DataManager): any {
if (response.error) {
const errorMessage = this.getErrorMessage(response.error.code);
console.error(errorMessage);
throw new Error(errorMessage);
}
return super.processResponse(response, dm);
}
private getErrorMessage(code: string): string {
const messages: { [key: string]: string } = {
'ERR_001': 'Failed to load data. Please try again.',
'ERR_002': 'Unauthorized access. Please login again.',
'ERR_003': 'Invalid data format.',
'NETWORK_ERROR': 'Network connection failed.'
};
return messages[code] || 'An unknown error occurred.';
}
}
```
---
## Complete Example
### Full Custom Middleware with All Features
> 🔒 **Security Warning:** `localStorage` and `sessionStorage` store data **unencrypted** in the browser. **Never store sensitive data** (passwords, tokens, PII, payment info, user secrets, authentication credentials) in persisted TreeGrid state. State persistence is safe for **UI state only** (expand/collapse state, page number, sort order, column visibility, filter selections). For sensitive configuration or user data, use secure server-side session storage instead.
```typescript
import { DataManager, WebApiAdaptor, Query } from '@syncfusion/ej2-data';
class CompleteAdaptor extends WebApiAdaptor {
private apiVersion = 'v2.0';
private requestCount = 0;
constructor(private authToken: string) {
super();
}
// Pre-request: Add headers, transform data
beforeSend(dm: DataManager, request: any): void {
super.beforeSend(dm, request);
this.requestCount++;
if (request instanceof XMLHttpRequest) {
// Add authentication
request.setRequestHeader('Authorization', `send_token`);
// Add API version
request.setRequestHeader('X-API-Version', this.apiVersion);
// Add request metadata
request.setRequestHeader('X-Request-ID', `${Date.now()}-${this.requestCount}`);
request.setRequestHeader('X-Timestamp', new Date().toISOString());
// Add custom headers
request.setRequestHeader('Accept-Language', navigator.language);
request.setRequestHeader('X-Client', 'angular-datamanager');
}
}
// Transform request parameters
processQuery(dm: DataManager, query: Query, params?: any): any {
const request = super.processQuery(dm, query, params);
// Convert to your API format
return {
page: Math.floor((request.skip || 0) / (request.take || 10)) + 1,
pageSize: request.take || 10,
filter: request.where || null,
sort: request.sorted || []
};
}
// Transform and validate response
processResponse(response: any, dm?: DataManager, query?: Query): any {
// Check for errors
if (response.error) {
throw new Error(response.error.message);
}
// Transform response structure
const transformed = {
result: response.data?.items || response.data || [],
count: response.data?.total || response.total || 0
};
// Validate required fields
if (!transformed.result) {
throw new Error('Invalid response format');
}
// Apply post-processing
if (Array.isArray(transformed.result)) {
transformed.result = transformed.result.map((item: any) => ({
...item,
_processed: true,
_timestamp: new Date()
}));
}
return super.processResponse(transformed, dm, query);
}
}
// Usage in Component
@Component({
selector: 'app-orders',
templateUrl: './orders.component.html'
})
export class OrdersComponent {
public orders: any[] = [];
constructor() {
const token = localStorage.getItem('auth_token') || 'default-token';
const dataManager = new DataManager({
url: 'url',
adaptor: new CompleteAdaptor(token)
});
// Use the configured data manager
dataManager.executeQuery(new Query().take(10))
.then((result) => {
this.orders = result.result;
})
.catch((error) => {
console.error('Failed to load orders:', error);
});
}
}
```
---
## Best Practices
1. **Keep middleware focused** — Do one thing well
2. **Handle errors gracefully** — Always have fallbacks
3. **Log appropriately** — Debug development, minimal production
4. **Secure sensitive data** — Never log tokens or passwords
5. **Document transformations** — Explain non-obvious changes
6. **Test thoroughly** — Test both success and failure paths
7. **Use TypeScript** — TypeScript guards help prevent errors
8. **Monitor performance** — Middleware adds overhead
---
references/querying-and-filtering.md
---
title: Querying and Filtering in Syncfusion Angular DataManager
---
# Querying and Filtering in Syncfusion Angular DataManager
## Table of Contents
- [Overview](#overview)
- [Query Class Fundamentals](#query-class-fundamentals)
- [Filtering with where()](#filtering-with-where)
- [Sorting Operations](#sorting-operations)
- [Pagination Methods](#pagination-methods)
- [Selection and Grouping](#selection-and-grouping)
- [Search Functionality](#search-functionality)
- [Combining Operations](#combining-operations)
- [Predicate-based Filtering](#predicate-based-filtering)
---
## Overview
The **Query** class is the core tool for manipulating data. It provides methods to:
- **Filter** data by conditions
- **Sort** by one or multiple columns
- **Paginate** results
- **Group** by fields
- **Select** specific columns
- **Search** across data
All methods are chainable, allowing combination of multiple operations.
---
## Query Class Fundamentals
### Creating a Query
```typescript
import { Query } from '@syncfusion/ej2-data';
// Empty query (returns all)
const query = new Query();
// With from() - define data source
const query2 = new Query().from('table_name');
// With select() - choose columns
const query3 = new Query().select(['OrderID', 'CustomerID']);
```
### Using from() Method
```typescript
// Specifies the data source table/entity
const query = new Query()
.from('Orders') // Data source
.select(['OrderID', 'CustomerID']);
// Used with remote data sources supporting multiple tables
```
---
## Filtering with where()
### Simple Equality
```typescript
import { DataManager, Query, JsonAdaptor } from '@syncfusion/ej2-data';
const data = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61 },
{ OrderID: 10250, CustomerID: 'VINET', Freight: 65.83 }
];
const dm = new DataManager({ json: data, adaptor: new JsonAdaptor() });
// Filter: where CustomerID equals 'VINET'
const result = dm.executeLocal(
new Query().where('CustomerID', 'equal', 'VINET')
);
// Result: 2 records with VINET
```
### Comparison Operators
```typescript
// Greater than
new Query().where('Freight', 'greaterThan', 50)
// Less than
new Query().where('OrderID', 'lessThan', 10250)
// Greater than or equal
new Query().where('Freight', 'greaterThanOrEqual', 30)
// Less than or equal
new Query().where('OrderID', 'lessThanOrEqual', 10249)
// Not equal
new Query().where('CustomerID', 'notEqual', 'VINET')
```
### String Operators
```typescript
// Starts with
new Query().where('CustomerID', 'startswith', 'V')
// Ends with
new Query().where('CustomerID', 'endswith', 'NET')
// Contains
new Query().where('CustomerID', 'contains', 'IN')
// Not contains
new Query().where('CustomerID', 'notcontains', 'ABC')
```
### Multiple Filters (AND)
```typescript
const result = dm.executeLocal(
new Query()
.where('CustomerID', 'equal', 'VINET')
.where('Freight', 'greaterThan', 30)
);
// Both conditions must be true
// Equivalent: CustomerID = 'VINET' AND Freight > 30
```
### Multiple Filters (OR)
```typescript
const result = dm.executeLocal(
new Query().where(
new Predicate('CustomerID', 'equal', 'VINET')
.or('CustomerID', 'equal', 'TOMSP')
)
);
// Either condition can be true
// Equivalent: CustomerID = 'VINET' OR CustomerID = 'TOMSP'
```
---
## Sorting Operations
### Basic Sort (Ascending)
```typescript
// Sort by Freight ascending (smallest to largest)
const result = dm.executeLocal(
new Query().sortBy('Freight')
);
```
### Descending Sort
```typescript
// Sort by OrderID descending (largest to smallest)
const result = dm.executeLocal(
new Query().sortByDesc('OrderID')
);
```
### Multiple Sort Fields
```typescript
// Sort by CustomerID (ascending), then by Freight (descending)
const result = dm.executeLocal(
new Query()
.sortBy('CustomerID')
.sortByDesc('Freight')
);
```
### Practical Example
```typescript
const query = new Query()
.where('Freight', 'greaterThan', 20)
.sortBy('CustomerID')
.sortByDesc('Freight');
const result = dm.executeLocal(query);
```
---
## Pagination Methods
### take() - Limit Results
```typescript
// Get first 10 records
const result = dm.executeLocal(
new Query().take(10)
);
```
### skip() - Skip Records
```typescript
// Skip first 20, get next 10
const result = dm.executeLocal(
new Query().skip(20).take(10)
);
```
### Complete Pagination Pattern
```typescript
// Page 1: records 0-9
const page1 = dm.executeLocal(
new Query().take(10)
);
// Page 2: records 10-19
const page2 = dm.executeLocal(
new Query().skip(10).take(10)
);
// Page 3: records 20-29
const page3 = dm.executeLocal(
new Query().skip(20).take(10)
);
```
---
## Selection and Grouping
### select() - Choose Columns
```typescript
// Select specific columns only
const result = dm.executeLocal(
new Query().select(['OrderID', 'CustomerID', 'Freight'])
);
// Result objects contain only these 3 properties
```
### group() - Group by Field
```typescript
// Group records by CustomerID
const result = dm.executeLocal(
new Query().group('CustomerID')
);
// Result: Objects grouped by CustomerID value
// [
// { key: 'VINET', items: [...] },
// { key: 'TOMSP', items: [...] },
// ...
// ]
```
### aggregate() - Calculate Values
```typescript
import { AggregateType } from '@syncfusion/ej2-data';
// Calculate sum, average, count
const result = dm.executeLocal(
new Query()
.aggregate('sum', 'Freight')
.aggregate('average', 'Freight')
);
```
---
## Search Functionality
### search() Method
```typescript
// Simple search across all fields
const result = dm.executeLocal(
new Query().search('VINET', ['CustomerID', 'ShipCity'])
);
// Searches for 'VINET' in CustomerID and ShipCity fields
// Equivalent: CustomerID contains 'VINET' OR ShipCity contains 'VINET'
```
### Case-Insensitive Search
```typescript
// Search is case-insensitive by default
const result = dm.executeLocal(
new Query().search('vinet', ['CustomerID'])
); // Matches 'VINET', 'Vinet', 'vinet'
```
---
## Combining Operations
### Complete Example: Filter, Select, Sort, Paginate
```typescript
const query = new Query()
// Filter conditions
.where('Freight', 'greaterThan', 20)
.where('CustomerID', 'startswith', 'V')
// Select specific columns
.select(['OrderID', 'CustomerID', 'Freight'])
// Sort
.sortBy('Freight')
// Pagination
.skip(0)
.take(10);
const result = dm.executeLocal(query);
```
### Progressive Building
```typescript
const filters = new Query()
.where('Freight', 'greaterThan', 20)
.where('CustomerID', 'startswith', 'V');
const sorted = filters
.sortBy('OrderID');
const paginated = sorted
.skip(0)
.take(10);
const result = dm.executeLocal(paginated);
```
### Complex Multi-Field Filtering
```typescript
const result = dm.executeLocal(
new Query()
.where('Status', 'equal', 'Active')
.where('Priority', 'greaterThan', 2)
.where('AssignedTo', 'notEqual', 'Unassigned')
.select(['ID', 'Title', 'Status', 'Priority'])
.sortByDesc('Priority')
.take(20)
);
```
---
## Predicate-based Filtering
### Using Predicate Class
```typescript
import { Predicate } from '@syncfusion/ej2-data';
// Single predicate
const predicate = new Predicate('CustomerID', 'equal', 'VINET');
const result = dm.executeLocal(new Query().where(predicate));
// AND combination (both must be true)
const predicate2 = new Predicate('CustomerID', 'equal', 'VINET')
.and('Freight', 'greaterThan', 30);
// OR combination (either can be true)
const predicate3 = new Predicate('CustomerID', 'equal', 'VINET')
.or('CustomerID', 'equal', 'TOMSP');
// Complex logic
const predicate4 = new Predicate('Status', 'equal', 'Active')
.and('Priority', 'greaterThan', 2)
.or('IsUrgent', 'equal', true);
const result = dm.executeLocal(new Query().where(predicate4));
```
### Nested Predicates
```typescript
const p1 = new Predicate('CustomerID', 'equal', 'VINET')
.or('CustomerID', 'equal', 'TOMSP');
const p2 = new Predicate('Freight', 'greaterThan', 20)
.or('Freight', 'lessThan', 10);
const combined = p1.and(p2);
const result = dm.executeLocal(new Query().where(combined));
```
---
## Remote Query Example
### Server-side Filtering and Sorting
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Server processes these operations
dataManager.executeQuery(
new Query()
.select(['OrderID', 'CustomerID', 'Freight'])
.where('Freight', 'greaterThan', 30)
.sortBy('OrderID')
.take(10)
)
.then((result) => {
console.log('Filtered results:', result.result);
console.log('Total count:', result.count);
});
```
---
## Practical Example: Orders Grid
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, Query, JsonAdaptor } from '@syncfusion/ej2-data';
@Component({
selector: 'app-orders-grid',
template: `
<div>
<div>
<label>Filter by Customer:
<select (change)="filterCustomer($event)">
<option value="">All</option>
<option value="VINET">VINET</option>
<option value="TOMSP">TOMSP</option>
</select>
</label>
<label>Min Freight:
<input type="number" [(ngModel)]="minFreight"
(change)="applyFilters()">
</label>
<button (click)="applyFilters()">Apply</button>
</div>
<table>
<thead>
<tr>
<th (click)="sort('OrderID')">Order ID</th>
<th (click)="sort('CustomerID')">Customer</th>
<th (click)="sort('Freight')">Freight</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let order of filteredOrders">
<td>{{ order.OrderID }}</td>
<td>{{ order.CustomerID }}</td>
<td>{{ order.Freight | currency }}</td>
</tr>
</tbody>
</table>
</div>
`
})
export class OrdersGridComponent implements OnInit {
public allOrders = [
{ OrderID: 10248, CustomerID: 'VINET', Freight: 32.38 },
{ OrderID: 10249, CustomerID: 'TOMSP', Freight: 11.61 },
{ OrderID: 10250, CustomerID: 'VINET', Freight: 65.83 }
];
public filteredOrders = [];
public selectedCustomer = '';
public minFreight = 0;
public sortField = 'OrderID';
dm = new DataManager({ json: this.allOrders, adaptor: new JsonAdaptor() });
ngOnInit(): void {
this.applyFilters();
}
filterCustomer(event: any): void {
this.selectedCustomer = event.target.value;
this.applyFilters();
}
applyFilters(): void {
let query = new Query();
if (this.selectedCustomer) {
query = query.where('CustomerID', 'equal', this.selectedCustomer);
}
if (this.minFreight > 0) {
query = query.where('Freight', 'greaterThan', this.minFreight);
}
query = query.sortBy(this.sortField);
this.filteredOrders = this.dm.executeLocal(query);
}
sort(field: string): void {
this.sortField = field;
this.applyFilters();
}
}
```
---
## Query Methods Reference
| Method | Purpose | Example |
|--------|---------|---------|
| `where()` | Add filter condition | `.where('id', 'equal', 5)` |
| `select()` | Choose columns | `.select(['id', 'name'])` |
| `sortBy()` | Sort ascending | `.sortBy('name')` |
| `sortByDesc()` | Sort descending | `.sortByDesc('date')` |
| `take()` | Limit results | `.take(10)` |
| `skip()` | Skip records | `.skip(20)` |
| `search()` | Search text | `.search('query', ['field1'])` |
| `group()` | Group by field | `.group('category')` |
| `aggregate()` | Calculate values | `.aggregate('sum', 'amount')` |
---
SKILL.md
---
name: syncfusion-angular-data-manager
description: Implements Syncfusion Angular DataManager for local/remote binding, CRUD, querying, caching, and middleware. Supports JsonAdaptor, ODataAdaptor, ODataV4Adaptor, UrlAdaptor, WebApiAdaptor, WebMethodAdaptor, RemoteSaveAdaptor, GraphQLAdaptor, CustomDataAdaptor, and CustomAdaptor. Covers Query class, filtering, sorting, paging, grouping, persistence, offline mode, caching, and error handling.
metadata:
author: "Syncfusion Inc"
version: "34.1.29"
category: "Data Management"
---
# Syncfusion Angular DataManager
## When to Use This Skill
**Use this skill when you need to:**
- Bind local JSON arrays to Angular components
- Connect Angular apps to remote REST, OData, or GraphQL services
- Implement querying, filtering, sorting, and paging on data
- Perform CRUD operations (Create, Read, Update, Delete) with data persistence
- Work offline and sync data when reconnected
- Transform and filter data using Query class
- Customize data communication with middleware
- Cache data to reduce server requests
- Handle different data source protocols automatically
**This is your primary reference for:**
- Any data operation in Syncfusion Angular components
- Universal data binding across all components
- Data source configuration and selection
- Async operations and error handling
- Performance optimization with caching
---
## Component Overview
The **DataManager** is Syncfusion's universal data management module that acts as a bridge between your Angular application and any data source. It provides:
- **10 Built-in Adaptors** — Support for JSON, OData v3/v4, REST, Web API, Web Methods, GraphQL, and custom sources
- **Query Class** — Powerful filtering, sorting, grouping, and paging API
- **CRUD Operations** — Insert, update, delete with automatic handling
- **Promise-based Async** — Native JavaScript Promise support
- **Middleware Customization** — Pre/post request hooks for authentication and transformation
- **Offline Support** — Work without server connection, sync when online
- **Caching** — Reduce server load and improve performance
- **Type Safety** — Full TypeScript definitions
---
---
## 🛑 MANDATORY HUMAN GATE: Adaptor Decision (Read This First)
> **This is the most critical rule in this skill. It cannot be skipped or bypassed.**
**When the user provides a service URL without explicitly naming the adaptor type, the agent MUST:**
1. **STOP code generation immediately**
2. **READ:** `references/adaptor-decision-gate.md` (full file)
3. **PRESENT** the gate to the user using the template in that file
4. **WAIT** for explicit user confirmation of the adaptor choice
5. **Only then** proceed to generate `DataManager` code with the confirmed adaptor
### Gate Trigger Conditions — Examples
| User Prompt | Gate Required? |
|-------------|---------------|
| `"Build an employee app using this service URL (http://myapi.com/employees)"` | ✅ YES — no adaptor specified |
| `"Connect GridComponent to http://myserver/api/data"` | ✅ YES — "API" is ambiguous |
| `"Use OData v4 endpoint at https://myserver/odata/employees"` | ❌ NO — OData v4 explicitly stated |
| `"My backend returns { Items, Count } at https://myserver/api/orders"` | ❌ NO — WebApiAdaptor pattern confirmed |
| `"I have a REST service at http://custom-url"` | ✅ YES — REST is ambiguous (UrlAdaptor vs WebApiAdaptor vs ODataV4) |
| `"Bind the grid to local list data"` | ❌ NO — local binding, no adaptor needed |
📄 **Full gate template, adaptor cheat sheet, disambiguation questions, and security rules:**
→ `references/adaptor-decision-gate.md`
---
## ⚠️ Security & Trust Boundary
- This skill generates code only, the agent does not execute data operations or fetch remote endpoints — all DataManager interactions occur solely within the user's application at runtime.
- Generated code must treat all third-party API responses as untrusted input, never bind to unvalidated or user-provided URLs, and ensure authentication is enforced on all remote endpoints.
---
## ⚠️ Critical Security Requirements
**When binding to remote services, you MUST implement these protections:**
1. **Use HTTPS only** — never use unencrypted HTTP connections; encrypt all data in transit
3. **Use string variables for URLs** — assign endpoints to private string properties rather than hardcoding in markup; this enables centralized validation and easier maintenance
5. **Validate and sanitize responses** — implement server-side schema validation; reject unexpected response formats; map responses to strongly-typed models
6. **Monitor remote calls** — log all external API requests with timestamps, endpoints, and outcomes for security audit trails
7. **Implement CORS securely** — use specific trusted origins only; never use wildcard `*` in production; require HTTPS
8. **Prevent indirect prompt injection attacks** — verify endpoint ownership; implement request signing; validate response content types and schemas before consuming data
**Third-party API responses can introduce security risks.** Always verify endpoint legitimacy, authenticate requests, and validate data before binding to UI components.
## Key Concepts & Trigger Keywords
| Concept | When to Use | Key Methods |
|---------|------------|------------|
| **Local Data Binding** | Working with in-memory JSON arrays | JsonAdaptor, executeLocal |
| **Remote Data Binding** | Calling API endpoints for data | UrlAdaptor, WebApiAdaptor, executeQuery |
| **Query Construction** | Filtering, sorting, grouping data | from(), where(), select(), sortBy(), take() |
| **CRUD Operations** | Managing records (add, edit, delete) | insert(), update(), remove(), saveChanges() |
| **Adaptor Selection** | Choosing the right data source type | 10 adaptors with decision tree |
| **Error Handling** | Managing failure scenarios | .catch(), try/catch, error status checking |
| **Offline Mode** | Working without server connection | offline property, localStorage |
| **Caching** | Improving performance | enableCache, cache clearing strategies |
| **Middleware** | Customizing requests/responses | applyPreRequestMiddlewares, applyPostMiddlewares |
| **Async Patterns** | Sequential and parallel operations | async/await, Promise.all(), then/catch |
---
## Documentation and Navigation Guide
### 🛑 Adaptor Decision Gate (Human Approval — MANDATORY FIRST STEP for Remote URLs)
📄 **Read:** [references/adaptor-decision-gate.md](references/adaptor-decision-gate.md)
- **When to trigger:** User provides any service URL without explicitly naming the adaptor
- Gate presentation template (copy-paste ready)
- Full adaptor comparison table (ODataV4 / OData / WebAPI / URL / GraphQL / Custom)
- Adaptor quick-reference cheat sheets with code examples
- Disambiguation follow-up questions
- Security rules applied after gate confirmation
- Stage 1 & Stage 3 integration instructions (flags ambiguous URLs in component mapping JSON)
- Keywords that auto-resolve the gate (no user confirmation needed)
### Getting Started
📄 **Read:** [references/getting-started.md](references/getting-started.md)
**When to read:** Starting new DataManager implementation, understanding package setup, learning TypeScript imports, configuring your first DataManager instance
**Topics covered:**
- Package installation and dependencies
- Module imports and TypeScript types
- Creating DataManager instances
- Difference between executeLocal and executeQuery
- Basic query execution patterns
---
### Data Binding - Local and Remote
📄 **Read:** [references/data-binding.md](references/data-binding.md)
**When to read:** Deciding between client-side and server-side processing, implementing local JSON binding, connecting to remote APIs, understanding executeLocal vs executeQuery patterns
**Topics covered:**
- Local binding with json property
- Remote binding with url + adaptor
- Client-side vs server-side processing
- Promise-based async patterns
- Basic error handling
---
### Querying and Filtering
📄 **Read:** [references/querying-and-filtering.md](references/querying-and-filtering.md)
**When to read:** Building queries, filtering by multiple conditions, sorting results, searching data, pagination, grouping records, advanced predicates
**Topics covered:**
- Query class fundamentals
- from() and select() methods
- where() with operators and predicates
- sortBy(), orderBy(), orderByDescending()
- take(), skip() for pagination
- search() method
- Grouping and aggregation
---
### CRUD Operations
📄 **Read:** [references/crud-operations.md](references/crud-operations.md)
**When to read:** Adding new records, updating existing data, deleting records, batch operations, CRUD method parameters, persisting changes to server
**Topics covered:**
- Insert (add new records)
- Update (modify existing records)
- Remove (delete records)
- keyField in CRUD method parameters
- saveChanges() for batch operations
- Error handling and validation
---
### 10 Adaptors Guide - Complete Reference
📄 **Read:** [references/adaptors-guide.md](references/adaptors-guide.md)
**When to read:** Selecting appropriate data source, understanding adaptor differences, implementing specific adaptor patterns, working with different API types
**Topics covered:**
- **JsonAdaptor** — Local JavaScript arrays, no server calls
- **ODataAdaptor** — OData v3 service endpoints
- **ODataV4Adaptor** — OData v4 protocol support
- **UrlAdaptor** — Generic RESTful API endpoints
- **WebApiAdaptor** — ASP.NET Web API integration
- **WebMethodAdaptor** — ASP.NET Web Methods
- **RemoteSaveAdaptor** — Client-side queries + server-side CRUD
- **GraphQLAdaptor** — GraphQL query language support
- **CustomDataAdaptor** — Custom request/response handling
- **CustomAdaptor** — Extending built-in adaptors
- Adaptor decision tree
- Comparison matrix for all 10
- Code examples for each adaptor
---
### Middleware Customization
📄 **Read:** [references/middleware-customization.md](references/middleware-customization.md)
**When to read:** Adding authentication headers, transforming requests/responses, logging API calls, implementing custom business logic
**Topics covered:**
- Pre-request middleware (applyPreRequestMiddlewares)
- Post-request middleware (applyPostMiddlewares)
- Custom headers (Authorization, etc.)
- Authentication patterns (Bearer tokens)
- Request/response transformation
- Error handling in middleware
---
### Caching and Offline Mode
📄 **Read:** [references/caching-offline-mode.md](references/caching-offline-mode.md)
**When to read:** Improving performance with caching, working offline when server unavailable, implementing sync strategies, managing data persistence
**Topics covered:**
- Offline property configuration
- localStorage for data persistence
- Cache auto-clearing on CRUD
- Manual cache management
- Online/offline workflow
- Sync strategies
---
### Advanced Features
📄 **Read:** [references/advanced-features.md](references/advanced-features.md)
**When to read:** Load on demand for large datasets, lazy loading relationships, pagination strategies, deferred operations, async/await patterns, memory management, state persistence
**Topics covered:**
- Load on demand pattern
- Lazy loading with expand()
- Pagination (client & server-side)
- Deferred operations with Promises
- Async/await patterns
- Promise.all() for parallel operations
- Error handling with status codes
- Memory management strategies
- Type safety with TypeScript
- State persistence patterns
---
### How-To Recipes
📄 **Read:** [references/how-to-recipes.md](references/how-to-recipes.md)
**When to read:** Quick solutions for common scenarios, best practices, troubleshooting, pattern examples
**Topics covered:**
- Work in offline mode
- Send additional parameters
- Add custom headers
- Implement authentication
- Handle errors gracefully
- Common patterns
- Performance optimization tips
- Troubleshooting guides
---
## Quick Start Example
### Setup and Basic Data Binding
**Step 1: Install Package**
```bash
npm install @syncfusion/ej2-data
```
**Step 2: Create Component**
```typescript
import { Component, OnInit } from '@angular/core';
import { DataManager, Query, JsonAdaptor, ReturnOption } from '@syncfusion/ej2-data';
@Component({
selector: 'app-data-demo',
templateUrl: './data-demo.component.html',
styleUrls: ['./data-demo.component.css']
})
export class DataDemoComponent implements OnInit {
public orders: object[];
ngOnInit(): void {
// Local data
const data = [
{ OrderID: 10248, CustomerID: 'VINET', EmployeeID: 5, ShipCity: 'Reims' },
{ OrderID: 10249, CustomerID: 'TOMSP', EmployeeID: 6, ShipCity: 'Münster' },
{ OrderID: 10250, CustomerID: 'HANAR', EmployeeID: 4, ShipCity: 'Rio de Janeiro' }
];
// Create DataManager with local data
const dataManager = new DataManager({
json: data,
adaptor: new JsonAdaptor()
});
// Execute query
this.orders = dataManager.executeLocal(
new Query().where('EmployeeID', 'equal', 5)
);
}
}
```
**Step 3: Display in Template**
```html
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Customer ID</th>
<th>Employee ID</th>
<th>Ship City</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let order of orders">
<td>{{ order.OrderID }}</td>
<td>{{ order.CustomerID }}</td>
<td>{{ order.EmployeeID }}</td>
<td>{{ order.ShipCity }}</td>
</tr>
</tbody>
</table>
```
---
## Common Patterns
### Remote Server with Promise
```typescript
const dataManager = new DataManager({
url: 'url',
adaptor: new WebApiAdaptor()
});
// Using Promise
dataManager.executeQuery(new Query().take(10))
.then((result: ReturnOption) => {
this.orders = result.result;
})
.catch((error) => {
console.error('Failed to fetch orders:', error);
});
```
### Async/Await Pattern
```typescript
async fetchOrders(): Promise<void> {
try {
const result = await this.dataManager.executeQuery(
new Query().take(10)
);
this.orders = result.result;
} catch (error) {
console.error('Error:', error);
}
}
```
### Filtering with Predicates
```typescript
const query = new Query()
.where('EmployeeID', 'equal', 5)
.where('ShipCity', 'startswith', 'Rio');
const result = dataManager.executeLocal(query);
```
### Server-side Operations
```typescript
const query = new Query()
.select(['OrderID', 'CustomerID', 'EmployeeID'])
.where('EmployeeID', 'greaterThan', 3)
.sortBy('OrderID')
.take(10)
.skip(0);
dataManager.executeQuery(query).then((result: ReturnOption) => {
this.orders = result.result;
this.totalRecords = result.count;
});
```
---
## Key Properties Reference
| Property | Type | Purpose |
|----------|------|---------|
| `json` | `array` | Local data array |
| `url` | `string` | Remote service endpoint |
| `adaptor` | `Adaptor` | Data source type handler |
| `headers` | `object[]` | Custom HTTP headers |
| `offline` | `boolean` | Enable offline mode |
| `enableCache` | `boolean` | Enable response caching |
| `crossDomain` | `boolean` | Enable cross-domain requests |
| `cachingPageSize` | `number` | Paging record count |
| `timeTillExpiration` | `number` | Cache TTL in milliseconds (default: infinite) |
| `ignoreOnPersist` | `string[]` | Properties to exclude from persistence |
| `timeZoneHandling` | `boolean` | Enable timezone offset handling |
---
## Error Handling Best Practices
```typescript
dataManager.executeQuery(query)
.then((result: ReturnOption) => {
if (result.result) {
this.items = result.result;
}
})
.catch((error: any) => {
if (error.status === 401) {
// Redirect to login
console.error('Unauthorized');
} else if (error.status === 403) {
// Access denied
console.error('Forbidden');
} else if (error.status === 500) {
// Server error
console.error('Server error');
} else if (error.status === 0) {
// Network error
console.error('Network error - check connectivity');
}
});
```
---
## Common Use Cases
### Use Case 1: Display Data Table
Start with [getting-started.md](references/getting-started.md) → [data-binding.md](references/data-binding.md) → [querying-and-filtering.md](references/querying-and-filtering.md)
### Use Case 2: Implement CRUD Form
Start with [adaptors-guide.md](references/adaptors-guide.md) → [crud-operations.md](references/crud-operations.md) → [advanced-features.md](references/advanced-features.md)
### Use Case 3: Work with Multiple Data Sources
Start with [adaptors-guide.md](references/adaptors-guide.md) (decision tree) → specific adaptor pattern → [middleware-customization.md](references/middleware-customization.md)
### Use Case 4: Optimize Large Datasets
Start with [advanced-features.md](references/advanced-features.md) → [caching-offline-mode.md](references/caching-offline-mode.md) → [how-to-recipes.md](references/how-to-recipes.md)
---
## TypeScript Type Safety
```typescript
// Define your data model
interface Order {
OrderID: number;
CustomerID: string;
EmployeeID: number;
ShipCity: string;
}
// Use generics for type safety
const dataManager: DataManager<Order> = new DataManager({
json: orders,
adaptor: new JsonAdaptor()
});
// Typed results
const result = await dataManager.executeQuery(new Query());
const typedOrders: Order[] = result.result as Order[];
```
---
## Performance Optimization Tips
1. **Use proper adaptor** — Select adaptor matching your backend
2. **Filter early** — Push filtering to server when possible
3. **Enable caching** — Reduce redundant server calls
4. **Lazy load** — Load data on demand for large datasets
5. **Pagination** — Limit records per request
6. **Offline mode** — Load once, work offline
7. **Select columns** — Fetch only needed fields
---