├── projects ├── expression-builder-demo │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── app.component.scss │ │ │ ├── app.module.ts │ │ │ ├── app.component.ts │ │ │ ├── app.component.html │ │ │ ├── shared │ │ │ │ └── material.module.ts │ │ │ ├── app.component.spec.ts │ │ │ ├── sample.remote.service.ts │ │ │ └── models │ │ │ │ └── sample-data.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── styles.scss │ │ ├── main.ts │ │ ├── index.html │ │ ├── test.ts │ │ └── polyfills.ts │ ├── tsconfig.app.json │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── browserslist │ └── karma.conf.js ├── emerbrito │ └── expression-builder │ │ ├── src │ │ ├── lib │ │ │ ├── components │ │ │ │ ├── field-select │ │ │ │ │ ├── field-select.component.css │ │ │ │ │ ├── field-select.component.html │ │ │ │ │ ├── field-select.component.spec.ts │ │ │ │ │ └── field-select.component.ts │ │ │ │ ├── logical-operator │ │ │ │ │ ├── logical-operator.component.css │ │ │ │ │ ├── logical-operator.component.spec.ts │ │ │ │ │ ├── logical-operator.component.html │ │ │ │ │ └── logical-operator.component.ts │ │ │ │ ├── condition │ │ │ │ │ ├── condition.component.css │ │ │ │ │ ├── condition.component.spec.ts │ │ │ │ │ ├── condition.component.html │ │ │ │ │ └── condition.component.ts │ │ │ │ └── expression-builder │ │ │ │ │ ├── expression-builder.component.spec.ts │ │ │ │ │ ├── expression-builder.component.scss │ │ │ │ │ ├── expression-builder.component.html │ │ │ │ │ └── expression-builder.component.ts │ │ │ ├── models │ │ │ │ ├── labels.ts │ │ │ │ └── models.ts │ │ │ ├── services │ │ │ │ ├── expression.service.spec.ts │ │ │ │ └── expression.service.ts │ │ │ ├── expression-builder.module.ts │ │ │ └── shared │ │ │ │ └── material.module.ts │ │ ├── public-api.ts │ │ └── test.ts │ │ ├── ng-package.json │ │ ├── tslint.json │ │ ├── tsconfig.spec.json │ │ ├── package.json │ │ ├── tsconfig.lib.json │ │ ├── karma.conf.js │ │ └── README.md └── expression-builder-demo-e2e │ ├── tsconfig.e2e.json │ ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts │ └── protractor.conf.js ├── .editorconfig ├── .vscode └── launch.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── tslint.json ├── README.md └── angular.json /projects/expression-builder-demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/field-select/field-select.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/logical-operator/logical-operator.component.css: -------------------------------------------------------------------------------- 1 | .action { 2 | margin-left: 5px; 3 | } 4 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emerbrito/ng-expression-builder/HEAD/projects/expression-builder-demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 15px; 4 | font-family: Roboto, "Helvetica Neue", sans-serif; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../../dist/emerbrito/expression-builder", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/expression-builder-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of query-builder 3 | */ 4 | 5 | export * from './lib/models/models'; 6 | export * from './lib/components/expression-builder/expression-builder.component'; 7 | export * from './lib/expression-builder.module'; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/expression-builder-demo-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /projects/expression-builder-demo-e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('eb-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/models/labels.ts: -------------------------------------------------------------------------------- 1 | export const ConditionLabels = { 2 | eq: 'Equals', 3 | ne: 'Not Equals', 4 | gt: 'Greater Than', 5 | ge: 'Greater Or Equal', 6 | lt: 'Less Than', 7 | le: 'Less Or Equal', 8 | contains: 'Contains', 9 | null: 'Does Not Contain Data', 10 | notnull: 'Contains Data' 11 | } -------------------------------------------------------------------------------- /projects/expression-builder-demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "eb", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "eb", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "eb", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "eb", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/condition/condition.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .field-control { 9 | min-width: 206px; 10 | } 11 | 12 | .space-left { 13 | margin-left: 15px; 14 | } 15 | 16 | .remove-button { 17 | margin-top: auto; 18 | margin-bottom: auto; 19 | } -------------------------------------------------------------------------------- /projects/expression-builder-demo/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/services/expression.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ExpressionService } from './expression.service'; 4 | 5 | describe('ExpressionService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ExpressionService = TestBed.get(ExpressionService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceFolder}/expression-builder" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ExpressionBuilderDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/field-select/field-select.component.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | {{item.label}} 14 | 15 | 16 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/expression-builder-demo-e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to expression-builder-demo!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/condition/condition.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ConditionComponent } from './condition.component'; 4 | 5 | describe('ConditionComponent', () => { 6 | let component: ConditionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ConditionComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ConditionComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/field-select/field-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FieldSelectComponent } from './field-select.component'; 4 | 5 | describe('FieldSelectComponent', () => { 6 | let component: FieldSelectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FieldSelectComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FieldSelectComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emerbrito/expression-builder", 3 | "version": "0.0.1", 4 | "description": "An expression builder built with Angular Material components.", 5 | "author": { 6 | "name": "Emerson Brito" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/emerbrito/ng-expression-builder.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/emerbrito/ng-expression-builder/issues" 14 | }, 15 | "homepage": "https://github.com/emerbrito/ng-expression-builder", 16 | "keywords": [ 17 | "angular", 18 | "material", 19 | "expression", 20 | "query", 21 | "builder" 22 | ], 23 | "license": "MIT", 24 | "peerDependencies": { 25 | "@angular/common": "^7.2.0", 26 | "@angular/core": "^7.2.0", 27 | "@angular/material": "^7.3.7" 28 | } 29 | } -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/logical-operator/logical-operator.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LogicalOperatorComponent } from './logical-operator.component'; 4 | 5 | describe('LogicalOperatorComponent', () => { 6 | let component: LogicalOperatorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LogicalOperatorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LogicalOperatorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/expression-builder-demo-e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/expression-builder/expression-builder.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExpressionBuilderComponent } from './expression-builder.component'; 4 | 5 | describe('ExpressionBuilderComponent', () => { 6 | let component: ExpressionBuilderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ExpressionBuilderComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ExpressionBuilderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/lib", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2018" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true 27 | }, 28 | "exclude": [ 29 | "src/test.ts", 30 | "**/*.spec.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | 6 | //import { ExpressionBuilderModule } from '@emerbrito/expression-builder'; 7 | import { ExpressionBuilderModule } from '../../../emerbrito/expression-builder/src/lib/expression-builder.module'; 8 | 9 | import { MaterialModule } from './shared/material.module'; 10 | import { AppComponent } from './app.component'; 11 | 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent 16 | ], 17 | imports: [ 18 | BrowserModule, 19 | BrowserAnimationsModule, 20 | HttpClientModule, 21 | MaterialModule, 22 | ExpressionBuilderModule 23 | ], 24 | providers: [], 25 | bootstrap: [AppComponent] 26 | }) 27 | export class AppModule { } 28 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | //import { Field, QueryExpression, ExpressionChangeEvent } from '@emerbrito/expression-builder'; 3 | import { Field, QueryExpression, ExpressionChangeEvent } from '../../../emerbrito/expression-builder/src/lib/models/models'; 4 | import { sampleFields, sampleData } from './models/sample-data'; 5 | 6 | @Component({ 7 | selector: 'eb-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class AppComponent { 13 | 14 | fields: Field[] = sampleFields; 15 | data: QueryExpression; 16 | 17 | valid: boolean; 18 | expression: QueryExpression; 19 | 20 | ngOnInit(): void { 21 | } 22 | 23 | feed(): void { 24 | this.data = sampleData as QueryExpression; 25 | } 26 | 27 | change(e: ExpressionChangeEvent) { 28 | this.valid = e.valid; 29 | this.expression = e.expression 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/expression-builder.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { MaterialModule } from './shared/material.module'; 6 | import { ExpressionBuilderComponent } from './components/expression-builder/expression-builder.component'; 7 | import { LogicalOperatorComponent } from './components/logical-operator/logical-operator.component'; 8 | import { ConditionComponent } from './components/condition/condition.component'; 9 | import { FieldSelectComponent } from './components/field-select/field-select.component'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | ExpressionBuilderComponent, 14 | LogicalOperatorComponent, 15 | ConditionComponent, 16 | FieldSelectComponent 17 | ], 18 | imports: [ 19 | CommonModule, 20 | MaterialModule, 21 | ReactiveFormsModule 22 | ], 23 | exports: [ 24 | ExpressionBuilderComponent 25 | ] 26 | }) 27 | export class ExpressionBuilderModule { } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "@emerbrito/expresion-builder": [ 23 | "dist/emerbrito/expresion-builder" 24 | ], 25 | "@emerbrito/expresion-builder/*": [ 26 | "dist/emerbrito/expresion-builder/*" 27 | ], 28 | "@emerbrito/expression-builder": [ 29 | "dist/emerbrito/expression-builder", 30 | "dist/emerbrito/expression-builder" 31 | ], 32 | "@emerbrito/expression-builder/*": [ 33 | "dist/emerbrito/expression-builder/*", 34 | "dist/emerbrito/expression-builder/*" 35 | ] 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Query Builder 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Query Builder Output 22 | 23 | 24 | Expression is valid 25 | Invalid Expression 26 | 27 | 28 | 29 |
{{expression | json}}
30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Emerson Brito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/shared/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { 5 | MatAutocompleteModule, 6 | MatButtonModule, 7 | MatButtonToggleModule, 8 | MatCardModule, 9 | MatDividerModule, 10 | MatIconModule, 11 | MatInputModule, 12 | MatFormFieldModule, 13 | MatMenuModule, 14 | MatSelectModule 15 | } from '@angular/material'; 16 | 17 | @NgModule({ 18 | declarations: [], 19 | imports: [ 20 | CommonModule, 21 | MatAutocompleteModule, 22 | MatButtonModule, 23 | MatButtonToggleModule, 24 | MatCardModule, 25 | MatDividerModule, 26 | MatIconModule, 27 | MatInputModule, 28 | MatFormFieldModule, 29 | MatMenuModule, 30 | MatSelectModule 31 | 32 | ], 33 | exports: [ 34 | MatAutocompleteModule, 35 | MatButtonModule, 36 | MatButtonToggleModule, 37 | MatCardModule, 38 | MatDividerModule, 39 | MatIconModule, 40 | MatInputModule, 41 | MatFormFieldModule, 42 | MatMenuModule, 43 | MatSelectModule 44 | ] 45 | }) 46 | export class MaterialModule { } 47 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../../coverage/emerbrito/expression-builder'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/expression-builder-demo'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'expression-builder-demo'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('expression-builder-demo'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to expression-builder-demo!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/logical-operator/logical-operator.component.html: -------------------------------------------------------------------------------- 1 | 2 | And 3 | Or 4 | 5 | 6 | 13 | 14 | 22 | 23 | 24 | 28 | 32 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/shared/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { 5 | MatAutocompleteModule, 6 | MatButtonModule, 7 | MatButtonToggleModule, 8 | MatCardModule, 9 | MatDatepickerModule, 10 | MatDividerModule, 11 | MatIconModule, 12 | MatInputModule, 13 | MatFormFieldModule, 14 | MatMenuModule, 15 | MatNativeDateModule, 16 | MatProgressSpinnerModule, 17 | MatSelectModule 18 | } from '@angular/material'; 19 | 20 | @NgModule({ 21 | declarations: [], 22 | imports: [ 23 | CommonModule, 24 | MatAutocompleteModule, 25 | MatButtonModule, 26 | MatButtonToggleModule, 27 | MatCardModule, 28 | MatDatepickerModule, 29 | MatDividerModule, 30 | MatIconModule, 31 | MatInputModule, 32 | MatFormFieldModule, 33 | MatMenuModule, 34 | MatNativeDateModule, 35 | MatProgressSpinnerModule, 36 | MatSelectModule 37 | 38 | ], 39 | exports: [ 40 | MatAutocompleteModule, 41 | MatButtonModule, 42 | MatButtonToggleModule, 43 | MatCardModule, 44 | MatDatepickerModule, 45 | MatDividerModule, 46 | MatIconModule, 47 | MatInputModule, 48 | MatFormFieldModule, 49 | MatMenuModule, 50 | MatNativeDateModule, 51 | MatProgressSpinnerModule, 52 | MatSelectModule 53 | ] 54 | }) 55 | export class MaterialModule { } 56 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/expression-builder/expression-builder.component.scss: -------------------------------------------------------------------------------- 1 | $border-color: #e0e0e0; 2 | $gap: 5px; 3 | $border-radius: 5px; 4 | 5 | div.header, div.item { 6 | box-sizing: border-box; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | div.header { 12 | padding-top: $gap; 13 | } 14 | 15 | div.item { 16 | padding-left: 10px; 17 | border-left: 1px solid $border-color; 18 | border-right: 1px solid $border-color; 19 | border-bottom: 1px solid $border-color; 20 | } 21 | 22 | div.item.first { 23 | border-top: 1px solid $border-color; 24 | border-top-left-radius: $border-radius; 25 | border-top-right-radius: $border-radius; 26 | } 27 | 28 | div.item.last { 29 | border-bottom-left-radius: $border-radius; 30 | border-bottom-right-radius: $border-radius; 31 | } 32 | 33 | li:first-child > div:first-child { 34 | padding-top: $gap; 35 | } 36 | 37 | /* 38 | Tree Lines Start 39 | Thanks to: https://stackoverflow.com/a/14424029 40 | */ 41 | 42 | ul { 43 | padding: 0 0 0 10px; 44 | margin: 0; 45 | list-style-type: none; 46 | position: relative; 47 | } 48 | li { 49 | list-style-type: none; 50 | border-left: 1px solid $border-color; 51 | margin-left: 1em; 52 | } 53 | li > div { 54 | padding-left: 1em; 55 | position: relative; 56 | } 57 | li > div::before { 58 | content:''; 59 | position: absolute; 60 | top: 0; 61 | left: -1px; 62 | bottom: 50%; 63 | width: 0.75em; 64 | border: 1px solid $border-color; 65 | border-top: 0 none transparent; 66 | border-right: 0 none transparent; 67 | } 68 | ul > li:last-child { 69 | border-left: 1px solid transparent; 70 | } 71 | 72 | /* Tree Lines Ends */ 73 | 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-expression-builder", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.2.0", 15 | "@angular/cdk": "~7.3.7", 16 | "@angular/common": "~7.2.0", 17 | "@angular/compiler": "~7.2.0", 18 | "@angular/core": "~7.2.0", 19 | "@angular/forms": "~7.2.0", 20 | "@angular/material": "^7.3.7", 21 | "@angular/platform-browser": "~7.2.0", 22 | "@angular/platform-browser-dynamic": "~7.2.0", 23 | "@angular/router": "~7.2.0", 24 | "core-js": "^2.5.4", 25 | "hammerjs": "^2.0.8", 26 | "rxjs": "~6.3.3", 27 | "tslib": "^1.9.0", 28 | "zone.js": "~0.8.26" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~0.13.0", 32 | "@angular-devkit/build-ng-packagr": "~0.13.0", 33 | "@angular/cli": "~7.3.3", 34 | "@angular/compiler-cli": "~7.2.0", 35 | "@angular/language-service": "~7.2.0", 36 | "@types/node": "~8.9.4", 37 | "@types/jasmine": "~2.8.8", 38 | "@types/jasminewd2": "~2.0.3", 39 | "codelyzer": "~4.5.0", 40 | "jasmine-core": "~2.99.1", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~4.0.0", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.1", 45 | "karma-jasmine": "~1.1.2", 46 | "karma-jasmine-html-reporter": "^0.2.2", 47 | "ng-packagr": "^4.2.0", 48 | "protractor": "~5.4.0", 49 | "ts-node": "~7.0.0", 50 | "tsickle": ">=0.34.0", 51 | "tslib": "^1.9.0", 52 | "tslint": "~5.11.0", 53 | "typescript": "~3.2.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-use-before-declare": true, 52 | "no-var-requires": false, 53 | "object-literal-key-quotes": [ 54 | true, 55 | "as-needed" 56 | ], 57 | "object-literal-sort-keys": false, 58 | "ordered-imports": false, 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "trailing-comma": false, 64 | "no-output-on-prefix": true, 65 | "use-input-property-decorator": true, 66 | "use-output-property-decorator": true, 67 | "use-host-property-decorator": true, 68 | "no-input-rename": true, 69 | "no-output-rename": true, 70 | "use-life-cycle-interface": true, 71 | "use-pipe-transform-interface": true, 72 | "component-class-suffix": true, 73 | "directive-class-suffix": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/logical-operator/logical-operator.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, forwardRef, Output, EventEmitter, Input } from '@angular/core'; 2 | import { MatButtonToggleChange } from '@angular/material'; 3 | import { LogicalOperator } from '../../models/models'; 4 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 5 | 6 | @Component({ 7 | selector: 'logical-operator', 8 | templateUrl: './logical-operator.component.html', 9 | styleUrls: ['./logical-operator.component.css'], 10 | providers: [ 11 | { 12 | provide: NG_VALUE_ACCESSOR, 13 | useExisting: forwardRef(() => LogicalOperatorComponent), 14 | multi: true 15 | } 16 | ], 17 | }) 18 | export class LogicalOperatorComponent implements OnInit, ControlValueAccessor { 19 | 20 | @Input() hideRemove: boolean; 21 | @Output() addGroup: EventEmitter = new EventEmitter(); 22 | @Output() addCondition: EventEmitter = new EventEmitter(); 23 | @Output() remove: EventEmitter = new EventEmitter(); 24 | operator: LogicalOperator = LogicalOperator.And 25 | disable: boolean; 26 | propagateChange: (value: any) => void = (value: any) => {}; 27 | 28 | constructor() { } 29 | 30 | ngOnInit() { 31 | } 32 | 33 | newCondition(): void { 34 | this.addCondition.emit(); 35 | } 36 | 37 | newGroup(): void { 38 | this.addGroup.emit(); 39 | } 40 | 41 | change(e: MatButtonToggleChange): void { 42 | this.operator = e.value; 43 | this.propagateChange(e.value); 44 | } 45 | 46 | removeGroup(): void { 47 | this.remove.emit(); 48 | } 49 | 50 | /* ControlValueAccessor implementation */ 51 | 52 | writeValue(value: any): void { 53 | this.operator = value || LogicalOperator.And; 54 | } 55 | 56 | registerOnChange(fn: any): void { 57 | this.propagateChange = fn; 58 | } 59 | 60 | registerOnTouched(fn: any): void { 61 | } 62 | 63 | setDisabledState?(isDisabled: boolean): void { 64 | this.disable = isDisabled; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/expression-builder/expression-builder.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 | 5 |
6 |
7 |
8 | 9 | 10 | 11 |
  • 12 | 13 |
    14 | 20 | 21 |
    22 | 23 |
      24 | 25 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 |
    38 | 39 |
  • 40 | 41 |
    42 | 43 | 44 | 45 |
  • 46 |
    47 |
    48 | 49 |
    50 |
    51 |
  • 52 |
    -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/sample.remote.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnDestroy } from '@angular/core'; 2 | import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; 3 | import { share, distinctUntilChanged, debounceTime, switchMap, tap, map } from 'rxjs/operators'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { LookupService } from '../../../../projects/emerbrito/expression-builder/src/lib/models/models'; 6 | 7 | export interface Person { 8 | UserName: string; 9 | FirstName: string; 10 | LastName: string; 11 | MiddleName?: any; 12 | Gender: string; 13 | Age?: any; 14 | Emails: string[]; 15 | FavoriteFeature: string; 16 | Features: string[]; 17 | HomeAddress?: any; 18 | } 19 | 20 | @Injectable({ 21 | providedIn: 'root' 22 | }) 23 | export class SampleRemoteService implements OnDestroy, LookupService { 24 | 25 | private _data: BehaviorSubject = new BehaviorSubject([]); 26 | private _search: Subject = new Subject(); 27 | private _loading: boolean; 28 | private searchSubs: Subscription; 29 | 30 | error: (err: any) => void; 31 | 32 | get data(): Observable { 33 | return this._data.pipe(share()); 34 | } 35 | 36 | get loading(): boolean { 37 | return this._loading; 38 | } 39 | 40 | constructor( 41 | private http: HttpClient 42 | ) 43 | { 44 | this.setup(); 45 | } 46 | 47 | setup() { 48 | 49 | this.searchSubs = this._search.pipe( 50 | debounceTime(200), 51 | distinctUntilChanged(), 52 | tap(data => this._loading = true), 53 | switchMap(value => this.retrieveRemote(value)) 54 | ) 55 | .subscribe(data => { 56 | this._data.next(data); 57 | this._loading = false; 58 | }, err => { 59 | this._loading = false; 60 | this.emitError(err); 61 | }); 62 | 63 | } 64 | 65 | ngOnDestroy() { 66 | if(this.searchSubs) this.searchSubs.unsubscribe(); 67 | this._data.complete(); 68 | this._search.complete(); 69 | } 70 | 71 | public search(nameContains: string): void { 72 | this._search.next(nameContains); 73 | } 74 | 75 | private retrieveRemote(nameContains: string): Observable { 76 | 77 | let url = 'https://services.odata.org/TripPinRESTierService/People'; 78 | 79 | if(nameContains) { 80 | url += `?$filter=contains(FirstName,'${nameContains}')`; 81 | } 82 | 83 | return this.http.get(url).pipe( 84 | map((resp: any) => resp.value) 85 | ); 86 | } 87 | 88 | private emitError(err: any): void { 89 | if(this.error) { 90 | this.error(err); 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/app/models/sample-data.ts: -------------------------------------------------------------------------------- 1 | //import { Field, FieldType, ConditionExpression } from '@emerbrito/expression-builder'; 2 | import { Field, FieldType, ConditionExpression } from '../../../../emerbrito/expression-builder/src/lib/models/models' 3 | import { SampleRemoteService } from '../sample.remote.service'; 4 | 5 | 6 | export const sampleFields: Field[] = [ 7 | { name: 'firstName', label: 'First Name', type: FieldType.Text }, 8 | { name: 'lastName', label: 'Last Name', type: FieldType.Text }, 9 | { name: 'applicatonDate', label: 'Application Date', type: FieldType.Date }, 10 | { name: 'age', label: 'Age', type: FieldType.Number }, 11 | { name: 'driversLicense', label: 'Driver Licesne', type: FieldType.Text }, 12 | { 13 | name: 'manager', 14 | label: 'Manager', 15 | type: FieldType.Lookup, 16 | lookup: { 17 | textField: 'FirstName', 18 | valueField: 'UserName', 19 | service: SampleRemoteService 20 | } 21 | }, 22 | { 23 | name: 'married', 24 | label: 'Married', 25 | type: FieldType.Boolean, 26 | values: [ 27 | { value: 'true', label: 'Yes'}, 28 | { value: 'false', label: 'No'}, 29 | ] 30 | }, 31 | { 32 | name: 'voter', 33 | label: 'Registered to Vote', 34 | type: FieldType.Boolean, 35 | values: [ 36 | { value: 'true', label: 'Yes'}, 37 | { value: 'false', label: 'No'}, 38 | ] 39 | }, 40 | { 41 | name: 'gender', 42 | label: 'Gender', 43 | type: FieldType.Text, 44 | values: [ 45 | { value: 'male', label: 'Male'}, 46 | { value: 'female', label: 'Female'}, 47 | ] 48 | } 49 | ]; 50 | 51 | export const sampleData = { 52 | "operator": "and", 53 | "rules": [ 54 | { 55 | "fieldName": "voter", 56 | "condition": "eq", 57 | "value": "true" 58 | }, 59 | { 60 | "fieldName": "driversLicense", 61 | "condition": "notnull" 62 | }, 63 | { 64 | "operator": "or", 65 | "rules": [ 66 | { 67 | "fieldName": "married", 68 | "condition": "eq", 69 | "value": "true" 70 | }, { 71 | "fieldName": "age", 72 | "condition": "ge", 73 | "value": "21" 74 | }, 75 | { 76 | "fieldName": "manager", 77 | "condition": "eq", 78 | "value": { 79 | FirstName: 'Georgina', 80 | UserName: 'georginabarlow' 81 | } 82 | } 83 | ] 84 | } 85 | ] 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/models/models.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorFn } from '@angular/forms'; 2 | import { Type } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | 5 | export enum FieldType { 6 | Boolean = 'bool', 7 | Date = 'date', 8 | Lookup = 'lookup', 9 | Number = 'number', 10 | Text = 'text', 11 | } 12 | 13 | export enum ConditionOperator { 14 | Equals = 'eq', 15 | NotEquals = 'ne', 16 | GreaterThan = 'gt', 17 | GreaterEqual = 'ge', 18 | LessThan = 'lt', 19 | LessEqual = 'le', 20 | Contains = 'contains', 21 | Null = 'null', 22 | NotNull = 'notnull' 23 | } 24 | 25 | export interface ExpressionChangeEvent { 26 | valid: boolean, 27 | expression: QueryExpression 28 | } 29 | 30 | export enum LogicalOperator { 31 | And = 'and', 32 | Or = 'or' 33 | } 34 | 35 | export interface Field { 36 | label: string, 37 | name: string, 38 | type: FieldType, 39 | values?: OptionValue[], 40 | lookup?: LookupSettings 41 | } 42 | 43 | export interface LookupSettings { 44 | valueField: string, 45 | textField: string, 46 | service: Type 47 | } 48 | 49 | export interface LookupService { 50 | data: Observable; 51 | loading: boolean; 52 | 53 | error: (err: any) => void; 54 | search: (value: string) => void; 55 | } 56 | 57 | export interface FieldTypeOperators { 58 | type: FieldType | string, 59 | operators: ConditionOperator[], 60 | validators?: ValidatorFn[] // for internal use 61 | } 62 | 63 | export interface ConditionExpression { 64 | fieldName: string, 65 | condition: ConditionOperator, 66 | value: any 67 | } 68 | 69 | export interface QueryExpression { 70 | operator: LogicalOperator, 71 | rules: (ConditionExpression|QueryExpression)[]; 72 | } 73 | 74 | export interface OptionValue { 75 | value: any, 76 | label: string 77 | } 78 | 79 | export interface KeyValuePair { 80 | key: string, 81 | value: T 82 | } 83 | 84 | export class KeyValueCollection { 85 | 86 | private items:KeyValuePair[] = []; 87 | 88 | add(key: string, value: T): void { 89 | this[key] = value; 90 | this.items.push({ key: key, value: value }); 91 | } 92 | 93 | addItem(item: KeyValuePair): void { 94 | this[item.key] = item.value; 95 | this.items.push(item); 96 | } 97 | 98 | getItems(): KeyValuePair[] { 99 | return this.items; 100 | } 101 | 102 | hasKey(key: string): boolean { 103 | return Object.hasOwnProperty.call(this, key); 104 | } 105 | 106 | value(key: string): T { 107 | let value = null; 108 | 109 | if(key) { 110 | value = this[key]; 111 | } 112 | 113 | return value; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /projects/expression-builder-demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Material Expression Builder 2 | 3 | ![Expression Builder](https://user-images.githubusercontent.com/5498239/56397092-3cc43080-6210-11e9-9f44-f64984736e34.png) 4 | 5 | A simple to use expression builder created with [Angular Material][1] components. 6 | Outputs a data structure (JSON) representing the expression which you can use to build queries and filters. 7 | 8 | ````javascript 9 | npm install @emerbrito/expression-builder 10 | ```` 11 | 12 | ## Getting Started 13 | 14 | The first step is to configure the fields available to the end user when building expressions. 15 | A field is defined by the `Field` interface: 16 | 17 | ````typescript 18 | export interface Field { 19 | label: string, 20 | name: string, 21 | type: FieldType, 22 | values?: OptionValue[] 23 | } 24 | ```` 25 | 26 | Field type is used for input validation and to render the appropriated control. 27 | Property `values` is optional. When specified a dropdown with the options will be rendered indifferent of the field type, which still applies to validation. 28 | 29 | Bellow is an example of some field definitions: 30 | 31 | ````typescript 32 | const fields = [ 33 | { 34 | name: "firstName", 35 | label: "First Name", 36 | type: FieldType.Text 37 | }, 38 | { 39 | "name": "married", 40 | "label": "Married", 41 | "type": FieldType.Boolean, 42 | "values": [ 43 | { 44 | "value": "true ", 45 | "label ": "Yes" 46 | }, 47 | { 48 | "value": "false ", 49 | "label ": "No" 50 | } 51 | ] 52 | } 53 | ]; 54 | ```` 55 | Use the component's input property to pass the field configurations: 56 | 57 | ````html 58 | 62 | 63 | ```` 64 | ### Component properties 65 | 66 | #### @Input() fields 67 | Type: `Field[]` 68 | Required. Array containing the fields available trough the expression builder. 69 | 70 | #### @Input() data 71 | Type: `QueryExpression` 72 | Optional. 73 | Initial expression. 74 | 75 | #### @Output() valuechange 76 | Argument: `ExpressionChangeEvent` 77 | Fires every time the expression changes. 78 | Contains the current expression and a flag indicating whether or not it is in a valid state. 79 | 80 | ### Sample Expression 81 | Bellow is a sample of the expression produce by the component: 82 | 83 | ````javascript 84 | { 85 | "operator": "and", 86 | "rules": [ 87 | { 88 | "fieldName": "voter", 89 | "condition": "eq", 90 | "value": "true" 91 | }, 92 | { 93 | "operator": "or", 94 | "rules": [ 95 | { 96 | "fieldName": "married", 97 | "condition": "eq", 98 | "value": "true" 99 | }, 100 | { 101 | "fieldName": "age", 102 | "condition": "ge", 103 | "value": "21" 104 | } 105 | ] 106 | } 107 | ] 108 | } 109 | ```` 110 | 111 | ### Before You Start 112 | This package dependens on [Angular Material][1]. 113 | Before you start make sure you add it to your project. 114 | 115 | ````javascript 116 | ng add @angular/material 117 | ```` 118 | 119 | More details on [Angular Material][1] installation can be found [here][2]. 120 | 121 | [1]: https://material.angular.io/ 122 | [2]: https://material.angular.io/guide/getting-started -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/README.md: -------------------------------------------------------------------------------- 1 | # Angular Material Expression Builder 2 | 3 | ![Expression Builder](https://user-images.githubusercontent.com/5498239/56397092-3cc43080-6210-11e9-9f44-f64984736e34.png) 4 | 5 | A simple to use expression builder created with [Angular Material][1] components. 6 | Outputs a data structure (JSON) representing the expression which you can use to build queries and filters. 7 | 8 | ````javascript 9 | npm install @emerbrito/expression-builder 10 | ```` 11 | 12 | ## Getting Started 13 | 14 | The first step is to configure the fields available to the end user when building expressions. 15 | A field is defined by the `Field` interface: 16 | 17 | ````typescript 18 | export interface Field { 19 | label: string, 20 | name: string, 21 | type: FieldType, 22 | values?: OptionValue[] 23 | } 24 | ```` 25 | 26 | Field type is used for input validation and to render the appropriated control. 27 | Property `values` is optional. When specified a dropdown with the options will be rendered indifferent of the field type, which still applies to validation. 28 | 29 | Bellow is an example of some field definitions: 30 | 31 | ````typescript 32 | const fields = [ 33 | { 34 | name: "firstName", 35 | label: "First Name", 36 | type: FieldType.Text 37 | }, 38 | { 39 | "name": "married", 40 | "label": "Married", 41 | "type": FieldType.Boolean, 42 | "values": [ 43 | { 44 | "value": "true ", 45 | "label ": "Yes" 46 | }, 47 | { 48 | "value": "false ", 49 | "label ": "No" 50 | } 51 | ] 52 | } 53 | ]; 54 | ```` 55 | Use the component's input property to pass the field configurations: 56 | 57 | ````html 58 | 62 | 63 | ```` 64 | ### Component properties 65 | 66 | #### @Input() fields 67 | Type: `Field[]` 68 | Required. Array containing the fields available trough the expression builder. 69 | 70 | #### @Input() data 71 | Type: `QueryExpression` 72 | Optional. 73 | Initial expression. 74 | 75 | #### @Output() valuechange 76 | Argument: `ExpressionChangeEvent` 77 | Fires every time the expression changes. 78 | Contains the current expression and a flag indicating whether or not it is in a valid state. 79 | 80 | ### Sample Expression 81 | Bellow is a sample of the expression produce by the component: 82 | 83 | ````javascript 84 | { 85 | "operator": "and", 86 | "rules": [ 87 | { 88 | "fieldName": "voter", 89 | "condition": "eq", 90 | "value": "true" 91 | }, 92 | { 93 | "operator": "or", 94 | "rules": [ 95 | { 96 | "fieldName": "married", 97 | "condition": "eq", 98 | "value": "true" 99 | }, 100 | { 101 | "fieldName": "age", 102 | "condition": "ge", 103 | "value": "21" 104 | } 105 | ] 106 | } 107 | ] 108 | } 109 | ```` 110 | 111 | ### Before You Start 112 | This package dependens on [Angular Material][1]. 113 | Before you start make sure you add it to your project. 114 | 115 | ````javascript 116 | ng add @angular/material 117 | ```` 118 | 119 | More details on [Angular Material][1] installation can be found [here][2]. 120 | 121 | [1]: https://material.angular.io/ 122 | [2]: https://material.angular.io/guide/getting-started -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/condition/condition.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | Condition 14 | 15 | 16 | {{option.label}} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Options 27 | 28 | 29 | {{option.label}} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 74 | 75 | {{option[fieldOptions.lookup.textField]}} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 85 | 86 |
    -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/expression-builder/expression-builder.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; 2 | import { FormBuilder, FormGroup, AbstractControl, FormArray } from '@angular/forms'; 3 | import { Observable, Subscription } from 'rxjs'; 4 | 5 | import { ExpressionService } from '../../services/expression.service'; 6 | import { QueryExpression, Field, LogicalOperator, ExpressionChangeEvent } from '../../models/models'; 7 | 8 | @Component({ 9 | selector: 'expression-builder', 10 | templateUrl: './expression-builder.component.html', 11 | styleUrls: ['./expression-builder.component.scss'], 12 | providers: [ExpressionService] 13 | }) 14 | export class ExpressionBuilderComponent implements OnInit, OnChanges { 15 | 16 | @Input() data: QueryExpression; 17 | @Input() fields: Field[] = []; 18 | @Output() valuechange: EventEmitter = new EventEmitter; 19 | invalid: boolean = false; 20 | valid: boolean = true; 21 | form: FormGroup; 22 | formValueSubs: Subscription; 23 | 24 | get valueChanges(): Observable { 25 | return this.form.valueChanges; 26 | } 27 | 28 | get value(): Observable { 29 | return this.form.value; 30 | } 31 | 32 | constructor( 33 | private expService: ExpressionService, 34 | private fb: FormBuilder 35 | ) { } 36 | 37 | ngOnInit() { 38 | 39 | this.expService.setFields(this.fields); 40 | this.initialize(); 41 | this.subscribeToForm(); 42 | 43 | } 44 | 45 | ngOnChanges(changes: SimpleChanges): void { 46 | 47 | const dataInput: SimpleChange = changes.data; 48 | 49 | if(dataInput) { 50 | 51 | if(this.formValueSubs) { 52 | this.formValueSubs.unsubscribe(); 53 | } 54 | this.initialize(); 55 | this.subscribeToForm(); 56 | this.emitChanges(); 57 | } 58 | 59 | } 60 | 61 | addCondition(host: FormGroup): void { 62 | 63 | let rules = host.get('rules') as FormArray; 64 | let msg = 'Method addCondition() failed.'; 65 | 66 | if(!rules) { 67 | throw new Error(`${msg} Form control doesn't have a property called 'rules'.`); 68 | } 69 | 70 | if(rules instanceof FormArray === false) { 71 | throw new Error(`${msg} Control 'rules' is not a FormArray.`); 72 | } 73 | 74 | rules.push(this.expService.createCondition('', null, '')); 75 | 76 | } 77 | 78 | addGroup(host: FormGroup): void { 79 | 80 | let rules = host.get('rules') as FormArray; 81 | let msg = 'Method addGroup() failed.'; 82 | 83 | if(!rules) { 84 | throw new Error(`${msg} Form control doesn't have a property called 'rules'.`); 85 | } 86 | 87 | if(rules instanceof FormArray === false) { 88 | throw new Error(`${msg} Control 'rules' is not a FormArray.`); 89 | } 90 | 91 | rules.push(this.expService.createGroup(LogicalOperator.And)); 92 | 93 | } 94 | 95 | emitChanges(): void { 96 | this.valuechange.emit({ 97 | valid: this.form.valid, 98 | expression: this.form.value 99 | }); 100 | } 101 | 102 | extractRules(formGroup: FormGroup): AbstractControl[] { 103 | let result: AbstractControl[] = [] 104 | 105 | if(formGroup) { 106 | let rules = formGroup.get('rules') as FormArray; 107 | 108 | if(rules && rules.controls && rules.controls.length > 0) { 109 | result = rules.controls; 110 | } 111 | } 112 | 113 | return result; 114 | } 115 | 116 | initialize(): void { 117 | 118 | if(this.data && this.expService.validate(this.data)) { 119 | console.log('Valid expression.'); 120 | this.form = this.expService.toFormGroup(this.data); 121 | } else { 122 | console.log('Unable to validate expression.'); 123 | this.form = this.fb.group({ 124 | operator: [LogicalOperator.And], 125 | rules: this.fb.array([]) 126 | }); 127 | 128 | } 129 | 130 | } 131 | 132 | isCondition(value: AbstractControl): boolean { 133 | return value.get('fieldName') != null; 134 | } 135 | 136 | isGroup(value: AbstractControl): boolean { 137 | return value.get('rules') != null; 138 | } 139 | 140 | isFirstCondition(index: number, rules: FormArray): boolean { 141 | 142 | let firstCondIndex = -1; 143 | 144 | for(let i = 0; i < rules.length; i++) { 145 | if(this.isCondition(rules[i])) { 146 | firstCondIndex = i; 147 | break; 148 | } 149 | } 150 | 151 | return index === firstCondIndex; 152 | 153 | } 154 | 155 | isLastCondition(index: number, rules: FormArray): boolean { 156 | 157 | if(index >= (rules.length - 1)) { 158 | return true; 159 | } 160 | 161 | let result = false; 162 | 163 | for(let i = (index + 1); i < rules.length; i++) { 164 | result = this.isGroup(rules[i]); 165 | if(!result) { 166 | break; 167 | } 168 | } 169 | 170 | return result; 171 | 172 | } 173 | 174 | removeItem(index: number, parent: FormGroup): void { 175 | 176 | if(!parent) { 177 | return; 178 | } 179 | 180 | let rules = parent.get('rules') as FormArray; 181 | let msg = 'Method removeGroup() failed.'; 182 | 183 | if(!rules) { 184 | throw new Error(`${msg} Form control doesn't have a property called 'rules'.`); 185 | } 186 | 187 | if(rules instanceof FormArray === false) { 188 | throw new Error(`${msg} Control 'rules' is not a FormArray.`); 189 | } 190 | 191 | rules.removeAt(index); 192 | 193 | } 194 | 195 | subscribeToForm(): void { 196 | this.formValueSubs = this.form.valueChanges.subscribe(data => { 197 | this.valid = this.form.valid; 198 | this.invalid = this.form.invalid; 199 | this.emitChanges(); 200 | }) 201 | } 202 | 203 | validateField(control: AbstractControl) { 204 | let value = control && control.value ? control.value : ''; 205 | let results = this.fields.filter(field => field.name === value); 206 | 207 | return results.length > 0; 208 | } 209 | 210 | 211 | } 212 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "@emerbrito/expression-builder": { 7 | "root": "projects/emerbrito/expression-builder", 8 | "sourceRoot": "projects/emerbrito/expression-builder/src", 9 | "projectType": "library", 10 | "prefix": "eb", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-ng-packagr:build", 14 | "options": { 15 | "tsConfig": "projects/emerbrito/expression-builder/tsconfig.lib.json", 16 | "project": "projects/emerbrito/expression-builder/ng-package.json" 17 | } 18 | }, 19 | "test": { 20 | "builder": "@angular-devkit/build-angular:karma", 21 | "options": { 22 | "main": "projects/emerbrito/expression-builder/src/test.ts", 23 | "tsConfig": "projects/emerbrito/expression-builder/tsconfig.spec.json", 24 | "karmaConfig": "projects/emerbrito/expression-builder/karma.conf.js" 25 | } 26 | }, 27 | "lint": { 28 | "builder": "@angular-devkit/build-angular:tslint", 29 | "options": { 30 | "tsConfig": [ 31 | "projects/emerbrito/expression-builder/tsconfig.lib.json", 32 | "projects/emerbrito/expression-builder/tsconfig.spec.json" 33 | ], 34 | "exclude": [ 35 | "**/node_modules/**" 36 | ] 37 | } 38 | } 39 | } 40 | }, 41 | "expression-builder-demo": { 42 | "root": "projects/expression-builder-demo/", 43 | "sourceRoot": "projects/expression-builder-demo/src", 44 | "projectType": "application", 45 | "prefix": "eb", 46 | "schematics": { 47 | "@schematics/angular:component": { 48 | "style": "scss" 49 | } 50 | }, 51 | "architect": { 52 | "build": { 53 | "builder": "@angular-devkit/build-angular:browser", 54 | "options": { 55 | "outputPath": "dist/expression-builder-demo", 56 | "index": "projects/expression-builder-demo/src/index.html", 57 | "main": "projects/expression-builder-demo/src/main.ts", 58 | "polyfills": "projects/expression-builder-demo/src/polyfills.ts", 59 | "tsConfig": "projects/expression-builder-demo/tsconfig.app.json", 60 | "assets": [ 61 | "projects/expression-builder-demo/src/favicon.ico", 62 | "projects/expression-builder-demo/src/assets" 63 | ], 64 | "styles": [ 65 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 66 | "projects/expression-builder-demo/src/styles.scss" 67 | ], 68 | "scripts": [], 69 | "es5BrowserSupport": true 70 | }, 71 | "configurations": { 72 | "production": { 73 | "fileReplacements": [ 74 | { 75 | "replace": "projects/expression-builder-demo/src/environments/environment.ts", 76 | "with": "projects/expression-builder-demo/src/environments/environment.prod.ts" 77 | } 78 | ], 79 | "optimization": true, 80 | "outputHashing": "all", 81 | "sourceMap": false, 82 | "extractCss": true, 83 | "namedChunks": false, 84 | "aot": true, 85 | "extractLicenses": true, 86 | "vendorChunk": false, 87 | "buildOptimizer": true, 88 | "budgets": [ 89 | { 90 | "type": "initial", 91 | "maximumWarning": "2mb", 92 | "maximumError": "5mb" 93 | } 94 | ] 95 | } 96 | } 97 | }, 98 | "serve": { 99 | "builder": "@angular-devkit/build-angular:dev-server", 100 | "options": { 101 | "browserTarget": "expression-builder-demo:build" 102 | }, 103 | "configurations": { 104 | "production": { 105 | "browserTarget": "expression-builder-demo:build:production" 106 | } 107 | } 108 | }, 109 | "extract-i18n": { 110 | "builder": "@angular-devkit/build-angular:extract-i18n", 111 | "options": { 112 | "browserTarget": "expression-builder-demo:build" 113 | } 114 | }, 115 | "test": { 116 | "builder": "@angular-devkit/build-angular:karma", 117 | "options": { 118 | "main": "projects/expression-builder-demo/src/test.ts", 119 | "polyfills": "projects/expression-builder-demo/src/polyfills.ts", 120 | "tsConfig": "projects/expression-builder-demo/tsconfig.spec.json", 121 | "karmaConfig": "projects/expression-builder-demo/karma.conf.js", 122 | "styles": [ 123 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 124 | "projects/expression-builder-demo/src/styles.scss" 125 | ], 126 | "scripts": [], 127 | "assets": [ 128 | "projects/expression-builder-demo/src/favicon.ico", 129 | "projects/expression-builder-demo/src/assets" 130 | ] 131 | } 132 | }, 133 | "lint": { 134 | "builder": "@angular-devkit/build-angular:tslint", 135 | "options": { 136 | "tsConfig": [ 137 | "projects/expression-builder-demo/tsconfig.app.json", 138 | "projects/expression-builder-demo/tsconfig.spec.json" 139 | ], 140 | "exclude": [ 141 | "**/node_modules/**" 142 | ] 143 | } 144 | } 145 | } 146 | }, 147 | "expression-builder-demo-e2e": { 148 | "root": "projects/expression-builder-demo-e2e/", 149 | "projectType": "application", 150 | "prefix": "", 151 | "architect": { 152 | "e2e": { 153 | "builder": "@angular-devkit/build-angular:protractor", 154 | "options": { 155 | "protractorConfig": "projects/expression-builder-demo-e2e/protractor.conf.js", 156 | "devServerTarget": "expression-builder-demo:serve" 157 | }, 158 | "configurations": { 159 | "production": { 160 | "devServerTarget": "expression-builder-demo:serve:production" 161 | } 162 | } 163 | }, 164 | "lint": { 165 | "builder": "@angular-devkit/build-angular:tslint", 166 | "options": { 167 | "tsConfig": "projects/expression-builder-demo-e2e/tsconfig.e2e.json", 168 | "exclude": [ 169 | "**/node_modules/**" 170 | ] 171 | } 172 | } 173 | } 174 | } 175 | }, 176 | "defaultProject": "@emerbrito/expression-builder" 177 | } -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/condition/condition.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ViewChild, Injector } from '@angular/core'; 2 | import { FormGroup, FormControl, ValidatorFn, AbstractControl } from '@angular/forms'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { ExpressionService } from '../../services/expression.service'; 6 | import { Field, OptionValue, ConditionOperator, FieldTypeOperators, LookupService, FieldType } from '../../models/models'; 7 | import { FieldSelectComponent } from '../field-select/field-select.component'; 8 | import { MatAutocompleteTrigger } from '@angular/material'; 9 | import { first } from 'rxjs/operators'; 10 | 11 | @Component({ 12 | selector: 'condition', 13 | templateUrl: './condition.component.html', 14 | styleUrls: ['./condition.component.css'] 15 | }) 16 | export class ConditionComponent implements OnInit, OnDestroy { 17 | 18 | @Input() allFields: Field[]; 19 | @Input() formGroup: FormGroup; 20 | @Output() remove: EventEmitter = new EventEmitter(); 21 | @ViewChild('fieldSelector') fieldSelector: FieldSelectComponent; 22 | @ViewChild('lookupinput', { read: MatAutocompleteTrigger }) lookupInput: MatAutocompleteTrigger; 23 | operators: OptionValue[] = []; 24 | fieldSubs: Subscription; 25 | valueSubs: Subscription; 26 | conditionSubs: Subscription; 27 | lookupService: LookupService; 28 | 29 | get field(): FormControl { 30 | return this.formGroup.get('fieldName') as FormControl; 31 | } 32 | 33 | get fieldName(): string { 34 | return this.field.value; 35 | } 36 | 37 | get fieldOptions(): Field { 38 | return this.expService.fields.value(this.fieldName); 39 | } 40 | 41 | get typeOptions(): FieldTypeOperators { 42 | if(this.fieldOptions) { 43 | return this.expService.typeOptions(this.fieldOptions.type); 44 | } 45 | return null; 46 | } 47 | 48 | get condition(): FormControl { 49 | return this.formGroup.get('condition') as FormControl; 50 | } 51 | 52 | get value(): FormControl { 53 | return this.formGroup.get('value') as FormControl; 54 | } 55 | 56 | constructor( 57 | private expService: ExpressionService, 58 | private injector: Injector 59 | ) 60 | { 61 | this.lookupDisplay = this.lookupDisplay.bind(this); 62 | } 63 | 64 | ngOnInit() { 65 | 66 | this.fieldSubs = this.field.valueChanges 67 | .subscribe(value => this.fieldChange(value)); 68 | 69 | this.valueSubs = this.value.valueChanges 70 | .subscribe(value => this.valueChange(value)); 71 | 72 | this.conditionSubs = this.condition.valueChanges 73 | .subscribe(value => this.conditionChange(value)) 74 | 75 | if(this.fieldOptions) { 76 | this.operatorFilter(this.fieldOptions); 77 | } 78 | 79 | if(this.field && !this.field.value || this.field.invalid) { 80 | if(this.value) { 81 | this.value.disable(); 82 | } 83 | } 84 | 85 | this.initLookup(); 86 | 87 | } 88 | 89 | ngOnDestroy() { 90 | if(this.fieldSubs) this.fieldSubs.unsubscribe(); 91 | if(this.valueSubs) this.valueSubs.unsubscribe(); 92 | if(this.conditionSubs) this.conditionSubs.unsubscribe(); 93 | } 94 | 95 | clearLookup(): void { 96 | 97 | if(this.value) { 98 | this.value.setValue(''); 99 | } 100 | 101 | if(this.lookupService) { 102 | this.lookupService.search(''); 103 | } 104 | 105 | // if(this.lookupInput) { 106 | // ; 107 | // } 108 | } 109 | 110 | conditionChange(condition: string): void { 111 | 112 | if(condition === ConditionOperator.NotNull || condition === ConditionOperator.Null) { 113 | this.value.setValue(''); 114 | this.value.disable(); 115 | } 116 | else { 117 | this.value.enable(); 118 | } 119 | 120 | } 121 | 122 | fieldChange(fieldName: string): void { 123 | 124 | let field = this.expService.fieldOptions(fieldName); 125 | 126 | this.condition.setValue(''); 127 | this.value.setValue(''); 128 | 129 | if(field) { 130 | const validators = this.expService.validadorsByType(field.type); 131 | if(field.type === FieldType.Lookup) { 132 | validators.push(this.lookupValidator(field.lookup.textField, field.lookup.valueField)); 133 | } 134 | this.value.setValidators(validators) 135 | } 136 | else { 137 | this.value.setValidators(null); 138 | } 139 | 140 | if(this.field.value && this.field.valid) { 141 | this.value.enable(); 142 | } 143 | else { 144 | this.value.setValue(null); 145 | this.value.disable(); 146 | } 147 | 148 | this.initLookup(); 149 | this.operatorFilter(field); 150 | } 151 | 152 | initLookup(): void { 153 | 154 | if(this.fieldOptions && this.fieldOptions.lookup && this.fieldOptions.lookup.service) { 155 | this.lookupService = this.injector.get(this.fieldOptions.lookup.service); 156 | this.lookupService.search(''); 157 | } 158 | else { 159 | this.lookupService = null; 160 | } 161 | } 162 | 163 | lookupDisplay(value: any): string | undefined { 164 | 165 | let label: string; 166 | 167 | if(this.fieldOptions && this.fieldOptions.lookup) { 168 | label = value[this.fieldOptions.lookup.textField]; 169 | } 170 | return label || undefined; 171 | } 172 | 173 | operatorFilter(fieldOptions: Field): void { 174 | this.operators = []; 175 | 176 | if(fieldOptions) { 177 | if(fieldOptions.values && fieldOptions.values.length > 0) { 178 | this.operators = this.expService.optionSetOperators(); 179 | } 180 | else { 181 | this.operators = this.expService.operatorsByType(fieldOptions.type); 182 | } 183 | } 184 | 185 | if(this.operators.length > 0) { 186 | this.condition.enable(); 187 | } 188 | else { 189 | this.condition.disable(); 190 | } 191 | 192 | } 193 | 194 | lookupValidator(textField: string, valueField: string): ValidatorFn { 195 | return (control: AbstractControl): {[key: string]: any} | null => { 196 | const value = control.value; 197 | 198 | if( 199 | value && 200 | ((typeof value === 'string') || 201 | (!value.hasOwnProperty(textField) || !value.hasOwnProperty(valueField))) 202 | ) { 203 | return { 'LookupInvalidOption' : {value: control.value} } 204 | } 205 | 206 | return null; 207 | }; 208 | } 209 | 210 | operatorDisplayFn(operator: ConditionOperator): string { 211 | 212 | let name: string = ''; 213 | let result = this.operators.filter(item => item.value === operator); 214 | 215 | if(result && result.length > 0) { 216 | name = result[0].label; 217 | } 218 | 219 | return name; 220 | } 221 | 222 | valueChange(value: any): void { 223 | 224 | let filter: string = ''; 225 | 226 | if(typeof value === 'string') { 227 | filter = value; 228 | } 229 | else if(this.fieldOptions && this.fieldOptions.lookup){ 230 | filter = value[this.fieldOptions.lookup.textField]; 231 | } 232 | 233 | if(this.lookupService) { 234 | this.lookupService.search(filter); 235 | } 236 | } 237 | 238 | removeCondition(): void { 239 | this.remove.emit(); 240 | } 241 | 242 | valueControl(): string { 243 | 244 | let name: string = 'text'; 245 | 246 | if(this.fieldOptions) { 247 | 248 | if(this.fieldOptions.values && this.fieldOptions.values.length > 0) { 249 | name = 'options' 250 | } 251 | else if (this.fieldOptions.type && this.fieldOptions.type === FieldType.Lookup) { 252 | name = 'lookup' 253 | } 254 | else { 255 | name = this.fieldOptions.type; 256 | } 257 | } 258 | 259 | return name; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/components/field-select/field-select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Input, ViewChild, ElementRef, HostBinding, Optional, Self, Output, EventEmitter } from '@angular/core'; 2 | import { ControlValueAccessor, NgControl } from '@angular/forms'; 3 | import { BehaviorSubject, Subscription, Subject } from 'rxjs'; 4 | import { debounceTime } from 'rxjs/operators'; 5 | 6 | import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger, MatFormFieldControl, MatSelectChange } from '@angular/material'; 7 | import { FocusMonitor } from '@angular/cdk/a11y'; 8 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; 9 | 10 | import { Field } from '../../models/models'; 11 | import { ExpressionService } from '../../services/expression.service'; 12 | 13 | @Component({ 14 | selector: 'field-select', 15 | templateUrl: './field-select.component.html', 16 | styleUrls: ['./field-select.component.css'], 17 | providers: [{ 18 | provide: MatFormFieldControl, 19 | useExisting: FieldSelectComponent} 20 | ], 21 | }) 22 | export class FieldSelectComponent implements OnInit, OnDestroy, ControlValueAccessor, MatFormFieldControl { 23 | 24 | private _disabled: boolean; 25 | 26 | @Input('fields') allFields: Field[]; 27 | @Output() fieldSelected: EventEmitter = new EventEmitter(); 28 | @ViewChild('input', {read: ElementRef}) private input: ElementRef; 29 | @ViewChild('selectTrigger') selectTrigger: MatAutocompleteTrigger; 30 | 31 | fielteredOptions: BehaviorSubject = new BehaviorSubject([]); 32 | inputValueChange: Subject = new Subject(); 33 | inputValueSubs: Subscription; 34 | inputValue: string = ''; 35 | selectedFieldChange: Subject = new Subject(); 36 | selectedFieldSubs: Subscription; 37 | selectedField: Field; 38 | 39 | @Input() 40 | get disabled(): boolean { 41 | return this._disabled; 42 | } 43 | set disabled(value: boolean) { 44 | this._disabled = coerceBooleanProperty(value); 45 | this.stateChanges.next(); 46 | } 47 | 48 | constructor( 49 | private expService: ExpressionService, 50 | private fm: FocusMonitor, private elRef: ElementRef, 51 | @Optional() @Self() public ngControl: NgControl, 52 | ) { 53 | 54 | this.labelByValue = this.labelByValue.bind(this); 55 | 56 | if (this.ngControl != null) { 57 | // Setting the value accessor directly (instead of using 58 | // the providers) to avoid running into a circular import. 59 | this.ngControl.valueAccessor = this; 60 | } 61 | 62 | fm.monitor(elRef.nativeElement, true).subscribe(origin => { 63 | this.focused = !!origin; 64 | this.stateChanges.next(); 65 | }); 66 | 67 | } 68 | 69 | ngOnInit() { 70 | 71 | this.fielteredOptions.next(this.allFields); 72 | 73 | this.selectedFieldSubs = this.selectedFieldChange.subscribe(data => { 74 | this.selectedField = data; 75 | this.propagateChange(data ? data.name : ''); 76 | this.errorState = this.ngControl ? this.ngControl.invalid : false; 77 | }); 78 | 79 | this.inputValueSubs = this.inputValueChange 80 | .pipe( 81 | debounceTime(150) 82 | ) 83 | .subscribe(data => { 84 | this.filterOptions(data); 85 | }); 86 | 87 | } 88 | 89 | ngOnDestroy() { 90 | if(this.inputValueSubs) this.inputValueSubs.unsubscribe; 91 | if(this.selectedFieldSubs) this.selectedFieldSubs.unsubscribe; 92 | 93 | this.stateChanges.complete(); 94 | this.fm.stopMonitoring(this.elRef.nativeElement); 95 | 96 | this.fielteredOptions.complete(); 97 | this.inputValueChange.complete(); 98 | this.selectedFieldChange.complete(); 99 | } 100 | 101 | blur(inputValue: string): void { 102 | this.setFromLabel(inputValue); 103 | 104 | if(this.ngControl) { 105 | this.errorState = this.ngControl.invalid; 106 | } 107 | } 108 | 109 | public clear(): void { 110 | this.setFromName(''); 111 | this.filterOptions(''); 112 | this.input.nativeElement.value = ''; 113 | setTimeout(() => { 114 | this.selectTrigger.openPanel(); 115 | this.input.nativeElement.focus() 116 | }); 117 | } 118 | 119 | filterOptions(contains: string): void { 120 | 121 | if(contains) { 122 | let values = this.allFields.filter(item => item.label.toLowerCase().indexOf(contains.toLowerCase()) >= 0); 123 | this.fielteredOptions.next(values); 124 | } 125 | else { 126 | this.fielteredOptions.next(this.allFields); 127 | } 128 | 129 | } 130 | 131 | keyup(inputValue: string): void { 132 | this.inputValueChange.next(inputValue); 133 | } 134 | 135 | labelByValue(value: string): string { 136 | return this.expService.fieldLabel(value); 137 | } 138 | 139 | applyChange(value: Field): void { 140 | this.selectedFieldChange.next(value); 141 | } 142 | 143 | optionSelected(e: MatAutocompleteSelectedEvent): void { 144 | this.setFromName(e.option.value); 145 | this.fieldSelected.emit(e.option.value); 146 | } 147 | 148 | setFromLabel(label: string): void { 149 | let option = this.expService.fieldByLabel(label); 150 | 151 | if(!option && !this.selectedField) { 152 | return; 153 | } 154 | 155 | if((!option && this.selectedField) || (option && !this.selectedField)) { 156 | this.applyChange(option); 157 | return; 158 | } 159 | 160 | if(option && option.name !== this.selectedField.name) { 161 | this.applyChange(option); 162 | } 163 | 164 | } 165 | 166 | setFromName(fieldName: string): void { 167 | let option = this.expService.fieldOptions(fieldName); 168 | this.applyChange(option); 169 | } 170 | 171 | /* ControlValueAccessor implementation */ 172 | 173 | propagateChange: (value: any) => void = (value: any) => {}; 174 | 175 | writeValue(value: any): void { 176 | 177 | this.selectedField = this.expService.fieldOptions(value); 178 | 179 | if(this.selectedField) { 180 | this.inputValue = this.selectedField.label; 181 | } 182 | 183 | this.stateChanges.next(); 184 | } 185 | 186 | registerOnChange(fn: any): void { 187 | this.propagateChange = fn; 188 | } 189 | 190 | registerOnTouched(fn: any): void { 191 | } 192 | 193 | setDisabledState?(isDisabled: boolean): void { 194 | this._disabled = isDisabled; 195 | } 196 | 197 | /* MatFormFieldControl implementation */ 198 | 199 | controlType = 'field-select-input'; 200 | focused = false; 201 | errorState = false; 202 | stateChanges = new Subject(); 203 | static nextId = 0; 204 | @HostBinding() id = `field-select-input-${FieldSelectComponent.nextId++}`; 205 | @HostBinding('attr.aria-describedby') describedBy = ''; 206 | 207 | get empty() { 208 | return (this.selectedField == null); 209 | } 210 | 211 | @Input() 212 | get value(): string { 213 | return this.selectedField.name; 214 | } 215 | set value(value: string) { 216 | this.writeValue(value); 217 | } 218 | 219 | @Input() 220 | get placeholder(): string { 221 | return this._placeholder; 222 | } 223 | set placeholder(plh) { 224 | this._placeholder = plh; 225 | this.stateChanges.next(); 226 | } 227 | private _placeholder: string; 228 | 229 | @HostBinding('class.floating') 230 | get shouldLabelFloat() { 231 | return this.focused || !this.empty; 232 | } 233 | 234 | @Input() 235 | get required() { 236 | return this._required; 237 | } 238 | set required(req) { 239 | this._required = coerceBooleanProperty(req); 240 | this.stateChanges.next(); 241 | } 242 | private _required = false; 243 | 244 | setDescribedByIds(ids: string[]) { 245 | this.describedBy = ids.join(' '); 246 | } 247 | 248 | onContainerClick(event: MouseEvent) { 249 | if ((event.target as Element).tagName.toLowerCase() != 'input') { 250 | this.elRef.nativeElement.querySelector('input').focus(); 251 | } 252 | } 253 | 254 | } -------------------------------------------------------------------------------- /projects/emerbrito/expression-builder/src/lib/services/expression.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FieldTypeOperators, FieldType, ConditionOperator, Field, OptionValue, KeyValueCollection, LogicalOperator } from '../models/models'; 3 | import { FormGroup, FormBuilder, FormArray, Validators, AbstractControl, ValidatorFn } from '@angular/forms'; 4 | import { ConditionLabels } from '../models/labels'; 5 | import { typeSourceSpan } from '@angular/compiler'; 6 | import { QueryExpression, ConditionExpression } from '../models/models'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ExpressionService { 12 | 13 | private _typeOperators: KeyValueCollection = new KeyValueCollection(); 14 | private _types: KeyValueCollection; 15 | private _fields: KeyValueCollection; 16 | private _labels: KeyValueCollection; 17 | 18 | get fields(): KeyValueCollection { 19 | return this._fields; 20 | } 21 | 22 | get labels(): KeyValueCollection { 23 | 24 | if(this._labels) { 25 | return this._labels; 26 | } 27 | 28 | this.initLabels(); 29 | return this._labels; 30 | } 31 | 32 | get types(): KeyValueCollection { 33 | 34 | if(this._types) { 35 | return this._types; 36 | } 37 | 38 | this.initTypes(); 39 | return this._types; 40 | } 41 | 42 | constructor(private fb: FormBuilder) { } 43 | 44 | fieldByLabel(fieldLabel: string): Field { 45 | 46 | if(!this._fields || !fieldLabel) { 47 | return null; 48 | } 49 | 50 | let item: Field = null; 51 | let items = this._fields.getItems(); 52 | let filteredItems = items.filter(item => item.value.label.toLowerCase() === fieldLabel.toLowerCase()); 53 | 54 | if(filteredItems.length > 0) { 55 | item = filteredItems[0].value; 56 | } 57 | 58 | return item; 59 | } 60 | 61 | fieldLabel(fieldName: string): string { 62 | 63 | if(this._fields) { 64 | let options = this.fieldOptions(fieldName); 65 | 66 | if(options) { 67 | return options.label; 68 | } 69 | } 70 | 71 | return ''; 72 | } 73 | 74 | fieldOptions(fieldName: string): Field { 75 | 76 | if(this._fields) { 77 | return this._fields.value(fieldName); 78 | } 79 | 80 | return null; 81 | } 82 | 83 | operatorsByType(type: FieldType): OptionValue[] { 84 | 85 | if(this._typeOperators.hasKey(type)) { 86 | return this._typeOperators.value(type); 87 | } 88 | 89 | let values: OptionValue[] = []; 90 | let typeInfo = this.types.value(type); 91 | 92 | if(typeInfo){ 93 | 94 | typeInfo.operators.forEach(item => { 95 | values.push({ 96 | value: item, 97 | label: ConditionLabels[item] 98 | }) 99 | }); 100 | 101 | this._typeOperators.add(type, values); 102 | 103 | } 104 | 105 | return values; 106 | } 107 | 108 | optionSetOperators(): OptionValue[] { 109 | let values: OptionValue[] = [ 110 | { value: ConditionOperator.Equals, label: ConditionLabels[ConditionOperator.Equals]}, 111 | { value: ConditionOperator.NotEquals, label: ConditionLabels[ConditionOperator.NotEquals]}, 112 | { value: ConditionOperator.Contains, label: ConditionLabels[ConditionOperator.Contains]}, 113 | { value: ConditionOperator.Null, label: ConditionLabels[ConditionOperator.Null]}, 114 | { value: ConditionOperator.NotNull, label: ConditionLabels[ConditionOperator.NotNull]}, 115 | ]; 116 | 117 | return values; 118 | } 119 | 120 | setFields(fields: Field[]): void { 121 | 122 | this._fields = new KeyValueCollection(); 123 | 124 | if(fields) { 125 | fields.forEach(field => { 126 | this._fields.add(field.name, field) 127 | }); 128 | } 129 | 130 | } 131 | 132 | toFormGroup(expression: QueryExpression): FormGroup { 133 | 134 | if(!expression) { 135 | return null; 136 | } 137 | 138 | let group: FormGroup = this.createGroup(expression.operator); 139 | let rules = group.get('rules') as FormArray; 140 | 141 | for(let i = 0; i < expression.rules.length; i++) { 142 | 143 | let item = expression.rules[i]; 144 | 145 | if(this.isCondition(item)) { 146 | let c = item as ConditionExpression; 147 | rules.push(this.createCondition(c.fieldName, c.condition, c.value)); 148 | } 149 | else if(this.isGroup(item)) { 150 | 151 | let g = item as QueryExpression; 152 | rules.push(this.toFormGroup(g)); 153 | 154 | } 155 | 156 | } 157 | 158 | return group; 159 | 160 | } 161 | 162 | typeOptions(type: FieldType): FieldTypeOperators { 163 | return this.types.value(type); 164 | } 165 | 166 | validate(expression: QueryExpression): boolean { 167 | 168 | if(!expression) { 169 | return false; 170 | } 171 | 172 | if(!expression.operator) { 173 | return false; 174 | } 175 | 176 | if(!expression.rules) { 177 | return false; 178 | } 179 | 180 | for(let i = 0; i < expression.rules.length; i++) { 181 | 182 | let item = expression.rules[i]; 183 | 184 | if(this.isCondition(item)) { 185 | let c = item as ConditionExpression; 186 | if(!c.fieldName || !c.condition) { 187 | return false; 188 | } 189 | } 190 | else if(this.isGroup(item)) { 191 | 192 | let g = item as QueryExpression; 193 | if(!g.operator || !g.rules) { 194 | return false; 195 | } 196 | this.validate(g); 197 | } 198 | else { 199 | return false; 200 | } 201 | 202 | } 203 | 204 | return true; 205 | } 206 | 207 | validadorsByType(type: FieldType): ValidatorFn[] { 208 | 209 | let fieldType = this.types.value(type); 210 | let validators: ValidatorFn[] = null; 211 | 212 | if(fieldType) { 213 | validators = fieldType.validators; 214 | 215 | if(validators && validators.length === 0){ 216 | validators = null; 217 | } 218 | 219 | } 220 | 221 | return validators; 222 | } 223 | 224 | public createGroup(operator: LogicalOperator): FormGroup { 225 | return this.fb.group({ 226 | operator: [operator], 227 | rules: this.fb.array([]) 228 | }); 229 | } 230 | 231 | public createCondition(fieldName?: string, condition?: ConditionOperator, value?: any): FormGroup { 232 | return this.fb.group({ 233 | fieldName: [fieldName, [Validators.required, this.validateField.bind(this)]], 234 | condition: [{value: condition, disabled: true}, [Validators.required]], 235 | value: [value, [Validators.required]] 236 | }); 237 | } 238 | 239 | private initLabels(): void { 240 | 241 | this._labels = new KeyValueCollection() 242 | 243 | for (const prop in ConditionLabels) { 244 | if (ConditionLabels.hasOwnProperty(prop)) { 245 | const element = ConditionLabels[prop]; 246 | this._labels.add(prop, element); 247 | } 248 | } 249 | 250 | } 251 | 252 | private initTypes(): void { 253 | 254 | this._types = new KeyValueCollection(); 255 | 256 | this._types.add(FieldType.Boolean, { 257 | type: FieldType.Boolean, 258 | operators: [ 259 | ConditionOperator.Equals, 260 | ConditionOperator.NotEquals, 261 | ConditionOperator.Null, 262 | ConditionOperator.NotNull 263 | ], 264 | validators: [Validators.required, Validators.pattern('^(true|false|1|0)$')] 265 | }); 266 | 267 | this._types.add(FieldType.Date, { 268 | type: FieldType.Date, 269 | operators: [ 270 | ConditionOperator.Equals, 271 | ConditionOperator.GreaterEqual, 272 | ConditionOperator.GreaterThan, 273 | ConditionOperator.LessEqual, 274 | ConditionOperator.LessThan, 275 | ConditionOperator.NotEquals, 276 | ConditionOperator.Null, 277 | ConditionOperator.NotNull 278 | ], 279 | validators: [Validators.required] 280 | }); 281 | 282 | this._types.add(FieldType.Lookup, { 283 | type: FieldType.Lookup, 284 | operators: [ 285 | ConditionOperator.Equals, 286 | ConditionOperator.NotEquals, 287 | ConditionOperator.Null, 288 | ConditionOperator.NotNull 289 | ], 290 | validators: [Validators.required] 291 | }); 292 | 293 | this._types.add(FieldType.Number, { 294 | type: FieldType.Date, 295 | operators: [ 296 | ConditionOperator.Equals, 297 | ConditionOperator.GreaterEqual, 298 | ConditionOperator.GreaterThan, 299 | ConditionOperator.LessEqual, 300 | ConditionOperator.LessThan, 301 | ConditionOperator.NotEquals, 302 | ConditionOperator.Null, 303 | ConditionOperator.NotNull 304 | ], 305 | validators: [Validators.required, Validators.pattern('^((\-?)([0-9]*)|(\-?)(([0-9]*)\.([0-9]*)))$')] 306 | }); 307 | 308 | this._types.add(FieldType.Text, { 309 | type: FieldType.Date, 310 | operators: [ 311 | ConditionOperator.Contains, 312 | ConditionOperator.Equals, 313 | ConditionOperator.NotEquals, 314 | ConditionOperator.Null, 315 | ConditionOperator.NotNull 316 | ], 317 | validators: [Validators.required] 318 | }); 319 | 320 | } 321 | 322 | public isCondition(value: ConditionExpression | QueryExpression): boolean { 323 | 324 | if(!value) { 325 | return false; 326 | } 327 | 328 | let item = value as ConditionExpression; 329 | return item.hasOwnProperty('fieldName') && item.hasOwnProperty('condition'); 330 | } 331 | 332 | public isGroup(value: ConditionExpression | QueryExpression): boolean { 333 | 334 | if(!value) { 335 | return false; 336 | } 337 | 338 | let item = value as QueryExpression; 339 | return item.hasOwnProperty('operator') && item.hasOwnProperty('rules'); 340 | } 341 | 342 | validateField(control: AbstractControl) { 343 | let value = control && control.value ? control.value : ''; 344 | let result: Field = null; 345 | 346 | if(value && this.fields) { 347 | result = this.fields.value(value); 348 | } 349 | 350 | return result != null; 351 | } 352 | 353 | } 354 | --------------------------------------------------------------------------------