UNPKG

ngx-ntk-query-builder

Version:

Angular query builder

925 lines (761 loc) 23.7 kB
# NGX NTK Query Builder **ngx-ntk-query-builder** - Advanced Angular query builder component for creating complex database queries with visual interface and multi-language support ## 📋 Overview The NGX NTK Query Builder is a powerful Angular component for building complex database queries through an intuitive visual interface. It supports multiple data types, operators, nested conditions, and provides a flexible architecture for creating dynamic queries. Perfect for applications requiring advanced search and filtering capabilities. ## 🚀 Installation ```bash npm install ngx-ntk-query-builder ``` ### Dependencies This library requires the following peer dependencies: ```bash npm install @ngx-translate/core @ngx-translate/http-loader ``` ## 📦 Features ### Core Features - **Visual Query Builder** - Intuitive drag-and-drop interface - **Multiple Data Types** - String, Integer, Double, Date, Time, Boolean, Select - **Advanced Operators** - Equals, Contains, Greater/Less than, Null checks, etc. - **Nested Conditions** - Support for complex AND/OR logic - **Multi-language Support** - Internationalization ready - **Custom Field Types** - Extensible field type system - **Real-time Validation** - Built-in query validation ### Advanced Features - **Custom Operators** - Extensible operator system - **Field Settings** - Configurable field behavior - **Query Export** - Export queries in various formats - **Responsive Design** - Mobile-friendly interface - **Accessibility** - ARIA support and keyboard navigation - **Template Customization** - Custom templates for fields ## 🔧 Usage ### Basic Setup ```typescript import { NgModule } from "@angular/core"; import { NgxNtkQueryBuilderModule } from "ngx-ntk-query-builder"; @NgModule({ imports: [NgxNtkQueryBuilderModule], }) export class AppModule {} ``` ### Basic Implementation ```html <ngx-ntk-query-builder [fieldMap]="fieldMap" [(data)]="queryData" (queryChange)="onQueryChange($event)"> </ngx-ntk-query-builder> ``` ### Advanced Configuration ```typescript import { Component } from "@angular/core"; import { QueryBuilderFieldMap, QueryRuleSet } from "ngx-ntk-query-builder"; @Component({ selector: "app-query-builder", template: ` <ngx-ntk-query-builder [fieldMap]="fieldMap" [settings]="querySettings" [(data)]="queryData" [disabled]="false" (queryChange)="onQueryChange($event)" (ruleAdded)="onRuleAdded($event)" (ruleRemoved)="onRuleRemoved($event)"> </ngx-ntk-query-builder> `, }) export class QueryBuilderComponent { fieldMap: QueryBuilderFieldMap = { name: { name: "Name", type: "string", settings: { nullableOperators: true, emptyOperators: true, }, }, age: { name: "Age", type: "integer", }, email: { name: "Email", type: "string", }, status: { name: "Status", type: "select", options: [ { name: "Active", value: "active" }, { name: "Inactive", value: "inactive" }, { name: "Pending", value: "pending" }, ], }, birthDate: { name: "Birth Date", type: "date", }, isActive: { name: "Is Active", type: "boolean", }, }; querySettings = { allowRuleSets: true, }; queryData: QueryRuleSet = { condition: "and", rules: [], }; onQueryChange(query: QueryRuleSet): void { console.log("Query changed:", query); // Handle query changes } onRuleAdded(rule: any): void { console.log("Rule added:", rule); } onRuleRemoved(rule: any): void { console.log("Rule removed:", rule); } } ``` ## 📚 API Reference ### Input Properties | Property | Type | Default | Description | | ---------- | -------------------- | ------- | ---------------------------- | | `fieldMap` | QueryBuilderFieldMap | - | Field configuration map | | `settings` | QueryBuilderSettings | {} | Query builder settings | | `data` | QueryRuleSet | - | Query data (two-way binding) | | `disabled` | boolean | false | Disable query builder | | `language` | string | 'en' | Interface language | ### Output Events | Event | Type | Description | | ------------- | ------------ | ------------------ | | `queryChange` | EventEmitter | Query change event | | `ruleAdded` | EventEmitter | Rule added event | | `ruleRemoved` | EventEmitter | Rule removed event | ### Interfaces #### QueryBuilderFieldMap ```typescript export interface QueryBuilderFieldMap { [key: string]: QueryField; } ``` #### QueryField ```typescript export interface QueryField { name: string; // Display name type: string; // Field type value?: string; // Default value options?: SelectOption[]; // Options for select type settings?: QueryFieldSettings; // Field settings } ``` #### QueryFieldSettings ```typescript export interface QueryFieldSettings { nullableOperators?: boolean; // Allow null operators emptyOperators?: boolean; // Allow empty operators } ``` #### QueryRule ```typescript export interface QueryRule { field: string; // Field name type: string; // Field type operator: string; // Operator value?: any; // Field value } ``` #### QueryRuleSet ```typescript export interface QueryRuleSet { condition: string; // 'and' or 'or' rules: Array<QueryRuleSet | QueryRule>; // Nested rules } ``` #### SelectOption ```typescript export interface SelectOption { name: string; // Display name value: any; // Option value } ``` ## 🎨 Customization ### Custom Styling ```scss // Custom query builder styles .ngx-ntk-query-builder { .query-group-controls { display: flex; justify-content: space-between; align-items: center; padding: 12px; background-color: #f5f5f5; border-radius: 8px; margin-bottom: 16px; .condition-switcher { display: flex; gap: 16px; .switch-radio { display: none; } .switch-label { padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; transition: all 0.3s ease; &:hover { background-color: #e0e0e0; } &.active { background-color: #2196f3; color: white; border-color: #2196f3; } } } .actions { display: flex; gap: 8px; .query-btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: all 0.3s ease; &.add { background-color: #4caf50; color: white; &:hover { background-color: #45a049; } } &.remove { background-color: #f44336; color: white; &:hover { background-color: #da190b; } } } } } .query-conditions { .rule-wrap { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 12px; background-color: #fff; .rule { display: grid; grid-template-columns: 2fr 1fr 2fr auto; gap: 12px; align-items: center; .field, .operator, .value { .query-select, .query-input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; &:focus { outline: none; border-color: #2196f3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } } } .rule-controls { .query-btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; background-color: #f44336; color: white; font-size: 12px; &:hover { background-color: #da190b; } } } } } } } ``` ### Custom Operators ```typescript import { Injectable } from "@angular/core"; import { OperatorsService } from "ngx-ntk-query-builder"; @Injectable() export class CustomOperatorsService extends OperatorsService { // Add custom operators private readonly customOperators = { string: ["custom_operator_1", "custom_operator_2"], integer: ["custom_numeric_operator"], }; private readonly customOperatorsDisplayNames = { custom_operator_1: "Custom Operator 1", custom_operator_2: "Custom Operator 2", custom_numeric_operator: "Custom Numeric Operator", }; getOperators(rule: QueryRule, fieldMap: QueryBuilderFieldMap): string[] { const baseOperators = super.getOperators(rule, fieldMap); const customOperators = this.customOperators[rule.type] || []; return [...baseOperators, ...customOperators]; } getOperatorDisplayName(operator: string): string { return this.customOperatorsDisplayNames[operator] || super.getOperatorDisplayName(operator); } } ``` ## 🌐 Internationalization ### Translation Setup ```typescript // Translation keys for query builder const queryBuilderTranslations = { en: { querybuilder: { And: "AND", Or: "OR", AddRule: "Add Rule", AddGroup: "Add Group", Remove: "Remove", equal: "Equals", not_equal: "Does not equal", contains: "Contains", not_contains: "Does not contain", begins_with: "Begins with", not_begins_with: "Does not begin with", ends_with: "Ends with", not_ends_with: "Does not end with", greater: "Greater than", greater_or_equal: "Greater than or equal", less: "Less than", less_or_equal: "Less than or equal", is_null: "Is null", is_not_null: "Is not null", is_empty: "Is empty", is_not_empty: "Is not empty", in: "In", not_in: "Not in", Yes: "Yes", No: "No", }, }, fa: { querybuilder: { And: "و", Or: "یا", AddRule: "افزودن قانون", AddGroup: "افزودن گروه", Remove: "حذف", equal: "مساوی", not_equal: "مساوی نیست", contains: "شامل", not_contains: "شامل نیست", begins_with: "شروع می‌شود با", not_begins_with: "شروع نمی‌شود با", ends_with: "پایان می‌یابد با", not_ends_with: "پایان نمی‌یابد با", greater: "بزرگتر از", greater_or_equal: "بزرگتر یا مساوی", less: "کمتر از", less_or_equal: "کمتر یا مساوی", is_null: "خالی است", is_not_null: "خالی نیست", is_empty: "تهی است", is_not_empty: "تهی نیست", in: "در", not_in: "در نیست", Yes: "بله", No: "خیر", }, }, }; ``` ## 🔒 Validation & Error Handling ### Query Validation ```typescript // Custom query validation validateQuery(query: QueryRuleSet): boolean { if (!query.rules || query.rules.length === 0) { return false; } return query.rules.every(rule => { if (this.isQueryRuleSet(rule)) { return this.validateQuery(rule); } else { return this.validateRule(rule); } }); } private validateRule(rule: QueryRule): boolean { return rule.field && rule.type && rule.operator; } private isQueryRuleSet(rule: any): rule is QueryRuleSet { return 'condition' in rule && 'rules' in rule; } ``` ### Error Handling ```typescript @Component({ template: ` <ngx-ntk-query-builder [fieldMap]="fieldMap" [(data)]="queryData" (queryChange)="onQueryChange($event)" (validationError)="onValidationError($event)"> </ngx-ntk-query-builder> <div *ngIf="validationError" class="error-message"> {{ validationError }} </div> `, }) export class QueryBuilderWithValidation { validationError: string = ""; onQueryChange(query: QueryRuleSet): void { if (this.validateQuery(query)) { this.validationError = ""; // Process valid query } else { this.validationError = "Invalid query structure"; } } onValidationError(error: any): void { console.error("Query validation error:", error); this.validationError = "Query validation failed"; } } ``` ## 🧪 Testing ### Unit Tests ```typescript import { ComponentFixture, TestBed } from "@angular/core/testing"; import { NgxNtkQueryBuilderModule } from "ngx-ntk-query-builder"; describe("QueryBuilderComponent", () => { let component: QueryBuilderComponent; let fixture: ComponentFixture<QueryBuilderComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NgxNtkQueryBuilderModule], declarations: [QueryBuilderComponent], }).compileComponents(); fixture = TestBed.createComponent(QueryBuilderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should create", () => { expect(component).toBeTruthy(); }); it("should emit query change event", () => { const mockQuery: QueryRuleSet = { condition: "and", rules: [ { field: "name", type: "string", operator: "contains", value: "test", }, ], }; spyOn(component.queryChange, "emit"); component.onQueryChange(mockQuery); expect(component.queryChange.emit).toHaveBeenCalledWith(mockQuery); }); it("should validate query structure", () => { const validQuery: QueryRuleSet = { condition: "and", rules: [ { field: "name", type: "string", operator: "contains", value: "test", }, ], }; expect(component.validateQuery(validQuery)).toBe(true); }); }); ``` ### Integration Tests ```typescript import { ComponentFixture, TestBed } from "@angular/core/testing"; import { NgxNtkQueryBuilderModule } from "ngx-ntk-query-builder"; describe("QueryBuilder Integration", () => { let fixture: ComponentFixture<QueryBuilderComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NgxNtkQueryBuilderModule], declarations: [QueryBuilderComponent], }).compileComponents(); fixture = TestBed.createComponent(QueryBuilderComponent); }); it("should display query builder interface", () => { fixture.detectChanges(); const queryBuilder = fixture.nativeElement.querySelector(".ngx-ntk-query-builder"); expect(queryBuilder).toBeTruthy(); }); it("should add rule when add rule button is clicked", () => { fixture.detectChanges(); const addRuleButton = fixture.nativeElement.querySelector(".query-btn-add"); addRuleButton.click(); fixture.detectChanges(); const rules = fixture.nativeElement.querySelectorAll(".rule-wrap"); expect(rules.length).toBeGreaterThan(0); }); it("should change condition when condition switcher is clicked", () => { fixture.detectChanges(); const orCondition = fixture.nativeElement.querySelector('input[value="or"]'); orCondition.click(); fixture.detectChanges(); expect(fixture.componentInstance.queryData.condition).toBe("or"); }); }); ``` ## ⚡ Performance ### Optimization Tips ```typescript // Use OnPush change detection for better performance @Component({ selector: "app-query-builder", changeDetection: ChangeDetectionStrategy.OnPush, }) export class QueryBuilderComponent { // Implement trackBy function for ngFor trackByRule(index: number, rule: QueryRule | QueryRuleSet): string { return rule.field || `rule-${index}`; } // Debounce query changes private queryChangeSubject = new Subject<QueryRuleSet>(); ngOnInit(): void { this.queryChangeSubject .pipe( debounceTime(300), distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)) ) .subscribe((query) => { this.handleQueryChange(query); }); } onQueryChange(query: QueryRuleSet): void { this.queryChangeSubject.next(query); } } ``` ### Memory Management ```typescript // Proper cleanup ngOnDestroy(): void { this.queryChangeSubject.complete(); this.destroy$.next(); this.destroy$.complete(); } // Optimize field map updates updateFieldMap(newFieldMap: QueryBuilderFieldMap): void { if (JSON.stringify(this.fieldMap) !== JSON.stringify(newFieldMap)) { this.fieldMap = newFieldMap; this.cdr.markForCheck(); } } ``` ## 📝 Examples ### Basic Query Builder ```typescript @Component({ selector: "app-basic-query-builder", template: ` <ngx-ntk-query-builder [fieldMap]="fieldMap" [(data)]="queryData" (queryChange)="onQueryChange($event)"> </ngx-ntk-query-builder> `, }) export class BasicQueryBuilderComponent { fieldMap: QueryBuilderFieldMap = { name: { name: "Name", type: "string" }, age: { name: "Age", type: "integer" }, email: { name: "Email", type: "string" }, }; queryData: QueryRuleSet = { condition: "and", rules: [], }; onQueryChange(query: QueryRuleSet): void { console.log("Query:", query); } } ``` ### Advanced Query Builder with Custom Fields ```typescript @Component({ selector: "app-advanced-query-builder", template: ` <ngx-ntk-query-builder [fieldMap]="fieldMap" [settings]="settings" [(data)]="queryData" (queryChange)="onQueryChange($event)"> </ngx-ntk-query-builder> <div class="query-preview"> <h3>Generated Query:</h3> <pre>{{ queryPreview }}</pre> </div> `, styles: [ ` .query-preview { margin-top: 20px; padding: 16px; background-color: #f5f5f5; border-radius: 8px; pre { background-color: #fff; padding: 12px; border-radius: 4px; overflow-x: auto; } } `, ], }) export class AdvancedQueryBuilderComponent { fieldMap: QueryBuilderFieldMap = { name: { name: "Name", type: "string", settings: { nullableOperators: true, emptyOperators: true, }, }, age: { name: "Age", type: "integer", }, status: { name: "Status", type: "select", options: [ { name: "Active", value: "active" }, { name: "Inactive", value: "inactive" }, { name: "Pending", value: "pending" }, ], }, birthDate: { name: "Birth Date", type: "date", }, isActive: { name: "Is Active", type: "boolean", }, }; settings = { allowRuleSets: true, }; queryData: QueryRuleSet = { condition: "and", rules: [], }; get queryPreview(): string { return JSON.stringify(this.queryData, null, 2); } onQueryChange(query: QueryRuleSet): void { console.log("Advanced query:", query); // Generate SQL or other query format const sqlQuery = this.generateSQL(query); console.log("SQL Query:", sqlQuery); } private generateSQL(query: QueryRuleSet): string { // Implement SQL generation logic return "SELECT * FROM users WHERE " + this.buildWhereClause(query); } private buildWhereClause(query: QueryRuleSet): string { // Implement WHERE clause building logic return ""; } } ``` ### Query Builder with Database Integration ```typescript @Component({ selector: "app-database-query-builder", template: ` <ngx-ntk-query-builder [fieldMap]="fieldMap" [(data)]="queryData" (queryChange)="onQueryChange($event)"> </ngx-ntk-query-builder> <div class="query-actions"> <button mat-raised-button color="primary" (click)="executeQuery()">Execute Query</button> <button mat-button (click)="saveQuery()">Save Query</button> </div> <div *ngIf="queryResults" class="query-results"> <h3>Results ({{ queryResults.length }} records)</h3> <table mat-table [dataSource]="queryResults"> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef>Name</th> <td mat-cell *matCellDef="let element">{{ element.name }}</td> </ng-container> <ng-container matColumnDef="age"> <th mat-header-cell *matHeaderCellDef>Age</th> <td mat-cell *matCellDef="let element">{{ element.age }}</td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> </table> </div> `, }) export class DatabaseQueryBuilderComponent { fieldMap: QueryBuilderFieldMap = { name: { name: "Name", type: "string" }, age: { name: "Age", type: "integer" }, email: { name: "Email", type: "string" }, }; queryData: QueryRuleSet = { condition: "and", rules: [], }; queryResults: any[] = []; displayedColumns = ["name", "age"]; constructor(private dataService: DataService) {} onQueryChange(query: QueryRuleSet): void { console.log("Query changed:", query); } executeQuery(): void { const sqlQuery = this.generateSQL(this.queryData); this.dataService.executeQuery(sqlQuery).subscribe( (results) => { this.queryResults = results; }, (error) => { console.error("Query execution failed:", error); } ); } saveQuery(): void { this.dataService.saveQuery(this.queryData).subscribe( () => { console.log("Query saved successfully"); }, (error) => { console.error("Failed to save query:", error); } ); } private generateSQL(query: QueryRuleSet): string { // Implement SQL generation logic return "SELECT * FROM users WHERE 1=1"; } } ``` ## 🔄 Version History ### v20.25.5 - Initial release with core functionality - Visual query builder interface - Multiple data type support - Nested conditions support - Multi-language support - Custom operators system ### v20.25.4 - Bug fixes and performance improvements - Enhanced validation system - Improved accessibility features - Better mobile responsiveness ## 🤝 Contributing We welcome contributions! Please see our contributing guidelines: 1. Fork the repository 2. Create a feature branch 3. Make your changes 4. Add tests for new functionality 5. Submit a pull request ## 📄 License This project is licensed under the ISC License. ## 🆘 Support For support and questions: - Create an issue on GitHub - Contact: [ntk.ir](https://ntk.ir) ## 👨‍💻 Author **Alireza Karavi** - GitHub: [@akaravi](https://github.com/akaravi) - Website: [ntk.ir](https://ntk.ir) --- **Note**: This library is part of the NTK CMS Angular Libraries collection. For more information, see the main project README.