├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── demo ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── editor │ ├── index.html │ └── main.js ├── gulpfile.js ├── package.json ├── src │ ├── app │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.module.ts │ ├── assets │ │ └── .gitkeep │ ├── browserslist │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json ├── tslint.json └── yarn.lock ├── karma-test-entry.ts ├── karma.conf.ts ├── package.json ├── projects └── angular2-query-builder │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── angular2-query-builder.module.ts │ │ └── query-builder │ │ │ ├── query-arrow-icon.directive.ts │ │ │ ├── query-builder.component.html │ │ │ ├── query-builder.component.scss │ │ │ ├── query-builder.component.spec.ts │ │ │ ├── query-builder.component.ts │ │ │ ├── query-builder.interfaces.ts │ │ │ ├── query-button-group.directive.ts │ │ │ ├── query-empty-warning.directive.ts │ │ │ ├── query-entity.directive.ts │ │ │ ├── query-field.directive.ts │ │ │ ├── query-input.directive.ts │ │ │ ├── query-operator.directive.ts │ │ │ ├── query-remove-button.directive.ts │ │ │ └── query-switch-group.directive.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig-aot.json ├── tsconfig.json ├── tslint.json ├── yarn.lock └── yarn.lock.orig /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | npm-debug.log 4 | package-lock.json 5 | 6 | # Yarn 7 | yarn-error.log 8 | 9 | # JetBrains 10 | .idea/ 11 | 12 | # VS Code 13 | .vscode/ 14 | 15 | # Windows 16 | Thumbs.db 17 | Desktop.ini 18 | 19 | # Mac 20 | .DS_Store 21 | 22 | # Temporary files 23 | coverage/ 24 | dist 25 | docs 26 | tmp 27 | .ignore 28 | 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | npm-debug.log 4 | 5 | # Yarn 6 | yarn-error.log 7 | 8 | # JetBrains 9 | .idea/ 10 | 11 | # VS Code 12 | .vscode/ 13 | 14 | # Windows 15 | Thumbs.db 16 | Desktop.ini 17 | 18 | # Mac 19 | .DS_Store 20 | 21 | # Temporary files 22 | coverage/ 23 | demo/ 24 | docs 25 | tmp 26 | 27 | # Library files 28 | src/ 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | addons: 4 | apt: 5 | sources: 6 | - google-chrome 7 | packages: 8 | - google-chrome-stable 9 | language: node_js 10 | node_js: 11 | - node 12 | script: 13 | - npm run ci 14 | before_script: 15 | - export DISPLAY=:99.0 16 | - sh -e /etc/init.d/xvfb start 17 | - sleep 3 18 | cache: 19 | yarn: true 20 | notifications: 21 | email: false 22 | after_success: 23 | - npm run codecov 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Angular2-QueryBuilder Changelog 2 | =============== 3 | 4 | Version 0.5.0 5 | ----------- 6 | - Upgrade to Angular 8 7 | - Add option to persist value on rule change (see documentation for details) 8 | 9 | Version 0.4.0 10 | ----------- 11 | - Fix issue switching operators changing select to multiple and causing invalid value error. (#69) 12 | - Add a `coerceValueForOperator` to handle value changes between operator switches to `config` object. 13 | - Add `treeContainer`, `collapsed`, `arrowIcon`, `arrowIconButton` fields to `classNames` object. 14 | - Add `queryArrowIcon` structural directive to override collapse arrow icon. 15 | - Added `allowCollapse` to enable accordion/collapse mode. (#66) 16 | 17 | Version 0.3.3 18 | ----------- 19 | - Fix `queryEmptyWarning` directive not being passed to nested rules 20 | 21 | Version 0.3.2 22 | ----------- 23 | - Add `queryEmptyWarning` directive for customizing empty warning messages 24 | - Add `[emptyMessage]` to change the default empty message text 25 | 26 | Version 0.3.1 27 | ----------- 28 | - Change: add Rule will use `defaultValue` of `Field` as the default 29 | - Updated Angular Materials to 6.0 in demos 30 | - Fix touched state not being updated when changing the query condition (AND/OR) 31 | - Add `[disabled]` feature #61 32 | - Tweaks to Vanilla CSS styling 33 | 34 | Version 0.3.0 35 | ----------- 36 | - Breaking: Renamed `changeField` callback to `onChange` for `queryField` directive. 37 | - Add `onChange` callback to `queryEntity`, `queryInput`, `queryOperator`, `queryInput` for proper reactive form validation and touched behavior on custom components 38 | - Add proper touched behavior to reactive form usage (See #49) 39 | - Add entity mode (See #22) 40 | - Fix `[value]` unrecognized property binding 41 | - Fix QueryBuilderClassNames not being exported as interface 42 | - Fix `in` operator causing multi-select to be displayed for all types (only limited to `category`, `boolean` now) 43 | - Minor tweaks to CSS styling of default component 44 | 45 | Version 0.2.5 46 | ----------- 47 | - Fix root remove ruleset button showing 48 | - Fix default value bug where only the first character of the operator is shown 49 | - Fix inability to override multiselect operators (is in, is not in) 50 | 51 | Version 0.2.4 52 | ----------- 53 | - Fix serious issue with validation causing `ngModel` value to be wrong 54 | - New `QueryBuilderClassNames` interface 55 | - Rewrite CSS for more extensible CSS classes 56 | - New bootstrap 4 example 57 | 58 | Version 0.2.3 59 | ----------- 60 | - Fix IE11 not working (target ES5) 61 | - Fix invalid/valid state 62 | - New validator function on `Field` config 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zeb Zhao 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular-QueryBuilder 2 | A modernized Angular 4+ query builder based on jQuery QueryBuilder. Support for heavy customization with Angular components and provides a flexible way to handle custom data types. 3 | 4 | # Getting Started 5 | 6 | ## Install 7 | 8 | - Angular 9, use 0.6.0 9 | - Angular 8, use 0.5.1 10 | - Angular 6-7, use 0.4.2 11 | - Angular 4-5, use 0.3.2 12 | 13 | `npm install angular2-query-builder` 14 | 15 | ## Demo 16 | Play with the [Demo here](https://zebzhao.github.io/Angular-QueryBuilder/demo/). 17 | 18 | [Editable Demo](https://zebzhao.github.io/Angular-QueryBuilder/editor/) 19 | 20 | ## Documentation 21 | 22 | [Documentation link](https://zebzhao.github.io/Angular-QueryBuilder/) 23 | 24 | # Examples 25 | 26 | ## Basic Usage 27 | 28 | ##### `app.module.ts` 29 | ```javascript 30 | import { QueryBuilderModule } from "angular2-query-builder"; 31 | import { AppComponent } from "./app.component" 32 | 33 | @NgModule(imports: [ 34 | ..., 35 | QueryBuilderModule, 36 | IonicModule.forRoot(AppComponent) // (Optional) for IonicFramework 2+ 37 | ]) 38 | export class AppModule { } 39 | ``` 40 | 41 | ##### `app.component.html` 42 | ```html 43 | ... 44 | 45 | ... 46 | ``` 47 | ##### `app.component.ts` 48 | ```javascript 49 | import { QueryBuilderConfig } from 'angular2-query-builder'; 50 | 51 | export class AppComponent { 52 | query = { 53 | condition: 'and', 54 | rules: [ 55 | {field: 'age', operator: '<=', value: 'Bob'}, 56 | {field: 'gender', operator: '>=', value: 'm'} 57 | ] 58 | }; 59 | 60 | config: QueryBuilderConfig = { 61 | fields: { 62 | age: {name: 'Age', type: 'number'}, 63 | gender: { 64 | name: 'Gender', 65 | type: 'category', 66 | options: [ 67 | {name: 'Male', value: 'm'}, 68 | {name: 'Female', value: 'f'} 69 | ] 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## Custom Input Components 77 | 78 | ##### `app.component.html` 79 | ```html 80 | 81 | 82 | 83 | 84 | 85 | ``` 86 | 87 | ##### `app.component.ts` 88 | ```javascript 89 | query = { 90 | condition: 'and', 91 | rules: [ 92 | {field: 'birthday', operator: '=', value: new Date()} 93 | ] 94 | }; 95 | 96 | config: QueryBuilderConfig = { 97 | fields: { 98 | birthday: {name: 'Birthday', type: 'date', operators: ['=', '<=', '>'] 99 | defaultValue: (() => return new Date()) 100 | }, 101 | } 102 | } 103 | ``` 104 | 105 | ## Custom Styling (with Bootstrap 4) 106 | 107 | [Bootstrap demo](https://zebzhao.github.io/Angular-QueryBuilder/demo/). 108 | 109 | ##### `app.component.html` 110 | ```html 111 | 112 | ``` 113 | ##### `app.component.ts` 114 | ```javascript 115 | classNames: QueryBuilderClassNames = { 116 | removeIcon: 'fa fa-minus', 117 | addIcon: 'fa fa-plus', 118 | arrowIcon: 'fa fa-chevron-right px-2', 119 | button: 'btn', 120 | buttonGroup: 'btn-group', 121 | rightAlign: 'order-12 ml-auto', 122 | switchRow: 'd-flex px-2', 123 | switchGroup: 'd-flex align-items-center', 124 | switchRadio: 'custom-control-input', 125 | switchLabel: 'custom-control-label', 126 | switchControl: 'custom-control custom-radio custom-control-inline', 127 | row: 'row p-2 m-1', 128 | rule: 'border', 129 | ruleSet: 'border', 130 | invalidRuleSet: 'alert alert-danger', 131 | emptyWarning: 'text-danger mx-auto', 132 | operatorControl: 'form-control', 133 | operatorControlSize: 'col-auto pr-0', 134 | fieldControl: 'form-control', 135 | fieldControlSize: 'col-auto pr-0', 136 | entityControl: 'form-control', 137 | entityControlSize: 'col-auto pr-0', 138 | inputControl: 'form-control', 139 | inputControlSize: 'col-auto' 140 | } 141 | ``` 142 | 143 | ## Customizing with Angular Material 144 | 145 | Example of how you can completely customize the query component with another library like Angular Material. For the full example, please look at the [source code](https://github.com/zebzhao/Angular-QueryBuilder/blob/master/demo/src/app/app.component.ts) provided in the demo. 146 | 147 | #### `app.component.html` 148 | 149 | ```html 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 160 | 161 | 162 | 163 | And 164 | Or 165 | 166 | 167 | 168 | 169 | 170 | {{field.name}} 171 | 172 | 173 | 174 | 175 | 176 | 177 | {{value}} 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | {{ opt.name }} 191 | 192 | 193 | 194 | 195 | ... 196 | 197 | ``` 198 | 199 | ## Property Bindings Quick Reference 200 | 201 | See [documentation](https://zebzhao.github.io/Angular-QueryBuilder/) for more details on interfaces and properties. 202 | 203 | #### `query-builder` 204 | |Name|Type|Required|Default|Description| 205 | |:--- |:--- |:--- |:--- |:--- | 206 | |`allowRuleset`|`boolean`|Optional|`true`| Displays the `+ Ruleset` button if `true`. | 207 | |`allowCollapse`|`boolean`|Optional|`false`| Enables collapsible rule sets if `true`. ([See Demo](https://zebzhao.github.io/Angular-QueryBuilder/demo/)) | 208 | |`classNames`|`object`|Optional|| CSS class names for different child elements in `query-builder` component. | 209 | |`config`|`QueryBuilderConfig`|Required|| Configuration object for the main component. | 210 | |`data`|`Ruleset`|Optional|| (Use `ngModel` or `value` instead.) | 211 | |`emptyMessage`|`string`|Optional|| Message to display for an empty Ruleset if empty rulesets are not allowed. | 212 | |`ngModel`| `Ruleset` |Optional|| Object that stores the state of the component. Supports 2-way binding. | 213 | |`operatorMap`|`{ [key: string]: string[] }`|Optional|| Used to map field types to list of operators. | 214 | |`persistValueOnFieldChange`|`boolean`|Optional|`false`| If `true`, when a field changes to another of the same type, and the type is one of: string, number, time, date, or boolean, persist the previous value. This option is ignored if config.calculateFieldChangeValue is provided. | 215 | |`config.calculateFieldChangeValue`|`(currentField: Field, nextField: Field, currentValue: any) => any`|Optional|| Used to calculate the new value when a rule's field changes. | 216 | |`value`| `Ruleset` |Optional|| Object that stores the state of the component. | 217 | 218 | ## Structural Directives 219 | 220 | Use these directives to replace different parts of query builder with custom components. See [example](#customizing-with-angular-material), or [demo](https://zebzhao.github.io/Angular-QueryBuilder/demo/) to see how it's done. 221 | 222 | #### `queryInput` 223 | 224 | Used to replace the input component. Specify the type/queryInputType to match specific field types to input template. 225 | 226 | |Context Name|Type|Description| 227 | |:--- |:--- |:--- | 228 | |`$implicit`|`Rule`|Current rule object which contains the field, value, and operator| 229 | |`field`|`Field`|Current field object which contains the field's value and name| 230 | |`options`|`Option[]`|List of options for the field, returned by `getOptions`| 231 | |`onChange`|`() => void`|Callback to handle changes to the input component| 232 | 233 | #### `queryOperator` 234 | 235 | Used to replace the query operator selection component. 236 | 237 | |Context Name|Type|Description| 238 | |:--- |:--- |:--- | 239 | |`$implicit`|`Rule`|Current rule object which contains the field, value, and operator| 240 | |`operators`|`string[]`|List of operators for the field, returned by `getOperators`| 241 | |`onChange`|`() => void`|Callback to handle changes to the operator component| 242 | |`type`|`string`|Input binding specifying the field type mapped to this input template, specified using syntax in above example| 243 | 244 | #### `queryField` 245 | 246 | Used this directive to replace the query field selection component. 247 | 248 | |Context Name|Type|Description| 249 | |:--- |:--- |:--- | 250 | |`$implicit`|`Rule`|Current rule object which contains the field, value, and operator| 251 | |`getFields`|`(entityName: string) => void`|Get the list of fields corresponding to an entity| 252 | |`fields`|`Field[]`|List of fields for the component, specified by `config`| 253 | |`onChange`|`(fieldValue: string, rule: Rule) => void`|Callback to handle changes to the field component| 254 | 255 | #### `queryEntity` 256 | 257 | Used to replace entity selection component. 258 | 259 | |Context Name|Type|Description| 260 | |:--- |:--- |:--- | 261 | |`$implicit`|`Rule`|Current rule object which contains the field, value, and operator| 262 | |`entities`|`Entity[]`|List of entities for the component, specified by `config`| 263 | |`onChange`|`(entityValue: string, rule: Rule) => void`|Callback to handle changes to the entity component| 264 | 265 | #### `querySwitchGroup` 266 | 267 | Useful for replacing the switch controls, for example the AND/OR conditions. More custom conditions can be specified by using this directive to override the default component. 268 | 269 | |Context Name|Type|Description| 270 | |:--- |:--- |:--- | 271 | |`$implicit`|`RuleSet`|Current rule set object which contain a list of child rules| 272 | |`onChange`|`() => void`|Callback to handle changes to the switch group component| 273 | 274 | #### `queryArrowIcon` 275 | 276 | Directive to replace the expand arrow used in collapse/accordion mode of the query builder. 277 | 278 | |Context Name|Type|Description| 279 | |:--- |:--- |:--- | 280 | |`$implicit`|`RuleSet`|Current rule set object which contain a list of child rules| 281 | 282 | #### `queryEmptyWarning` 283 | 284 | Can be used to customize the default empty warning message, alternatively can specify the `emptyMessage` property binding. 285 | 286 | |Context Name|Type|Description| 287 | |:--- |:--- |:--- | 288 | |`$implicit`|`RuleSet`|Current rule set object which contain a list of child rules| 289 | |`message`|`string`|Value passed to `emptyMessage`| 290 | 291 | #### `queryButtonGroup` 292 | 293 | For replacing the default button group for Add, Add Ruleset, Remove Ruleset buttons. 294 | 295 | |Context Name|Type|Description| 296 | |:--- |:--- |:--- | 297 | |`$implicit`|`RuleSet`|Current rule set object which contain a list of child rules| 298 | |`addRule`|`() => void`|Function to handle adding a new rule| 299 | |`addRuleSet`|`() => void`|Function to handle adding a new rule set| 300 | |`removeRuleSet`|`() => void`|Function to handle removing the current rule set| 301 | 302 | #### `queryRemoveButton` 303 | 304 | Directive to replace the default remove single rule button component. 305 | 306 | |Context Name|Type|Description| 307 | |:--- |:--- |:--- | 308 | |`$implicit`|`Rule`|Current rule object which contains the field, value, and operator| 309 | |`removeRule`|`(rule: Rule) => void`|Function to handle removing a rule| 310 | 311 | ## Dependencies 312 | - Angular 8+ 313 | 314 | That's it. 315 | 316 | # Workflow 317 | See the [angular-library-seed](https://github.com/trekhleb/angular-library-seed) project for details on how to build and run tests. 318 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular2-query-builder": { 7 | "projectType": "library", 8 | "root": "projects/angular2-query-builder", 9 | "sourceRoot": "projects/angular2-query-builder/src", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-ng-packagr:build", 13 | "options": { 14 | "tsConfig": "projects/angular2-query-builder/tsconfig.lib.json", 15 | "project": "projects/angular2-query-builder/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/angular2-query-builder/tsconfig.lib.prod.json" 20 | } 21 | } 22 | }, 23 | "test": { 24 | "builder": "@angular-devkit/build-angular:karma", 25 | "options": { 26 | "main": "projects/angular2-query-builder/src/test.ts", 27 | "tsConfig": "projects/angular2-query-builder/tsconfig.spec.json", 28 | "karmaConfig": "projects/angular2-query-builder/karma.conf.js" 29 | } 30 | }, 31 | "lint": { 32 | "builder": "@angular-devkit/build-angular:tslint", 33 | "options": { 34 | "tsConfig": [ 35 | "projects/angular2-query-builder/tsconfig.lib.json", 36 | "projects/angular2-query-builder/tsconfig.spec.json" 37 | ], 38 | "exclude": [ 39 | "**/node_modules/**" 40 | ] 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | "defaultProject": "angular2-query-builder" 47 | } 48 | -------------------------------------------------------------------------------- /demo/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | -------------------------------------------------------------------------------- /demo/.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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /demo/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "aot": true, 17 | "outputPath": "dist/demo", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "src/tsconfig.app.json", 22 | "preserveSymlinks": true, 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets" 26 | ], 27 | "styles": [ 28 | "src/styles.css" 29 | ], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "budgets": [ 35 | { 36 | "type": "anyComponentStyle", 37 | "maximumWarning": "6kb" 38 | } 39 | ], 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ], 46 | "optimization": true, 47 | "outputHashing": "all", 48 | "sourceMap": false, 49 | "extractCss": true, 50 | "namedChunks": false, 51 | "aot": true, 52 | "extractLicenses": true, 53 | "vendorChunk": false, 54 | "buildOptimizer": true 55 | } 56 | } 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "options": { 61 | "browserTarget": "demo:build" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "browserTarget": "demo:build:production" 66 | } 67 | } 68 | }, 69 | "extract-i18n": { 70 | "builder": "@angular-devkit/build-angular:extract-i18n", 71 | "options": { 72 | "browserTarget": "demo:build" 73 | } 74 | }, 75 | "test": { 76 | "builder": "@angular-devkit/build-angular:karma", 77 | "options": { 78 | "main": "src/test.ts", 79 | "polyfills": "src/polyfills.ts", 80 | "tsConfig": "src/tsconfig.spec.json", 81 | "karmaConfig": "src/karma.conf.js", 82 | "styles": [ 83 | "src/styles.css" 84 | ], 85 | "scripts": [], 86 | "assets": [ 87 | "src/favicon.ico", 88 | "src/assets" 89 | ] 90 | } 91 | }, 92 | "lint": { 93 | "builder": "@angular-devkit/build-angular:tslint", 94 | "options": { 95 | "tsConfig": [ 96 | "src/tsconfig.app.json", 97 | "src/tsconfig.spec.json" 98 | ], 99 | "exclude": [ 100 | "**/node_modules/**" 101 | ] 102 | } 103 | } 104 | } 105 | }, 106 | "demo-e2e": { 107 | "root": "e2e/", 108 | "projectType": "application", 109 | "architect": { 110 | "e2e": { 111 | "builder": "@angular-devkit/build-angular:protractor", 112 | "options": { 113 | "protractorConfig": "e2e/protractor.conf.js", 114 | "devServerTarget": "demo:serve" 115 | }, 116 | "configurations": { 117 | "production": { 118 | "devServerTarget": "demo:serve:production" 119 | } 120 | } 121 | }, 122 | "lint": { 123 | "builder": "@angular-devkit/build-angular:tslint", 124 | "options": { 125 | "tsConfig": "e2e/tsconfig.e2e.json", 126 | "exclude": [ 127 | "**/node_modules/**" 128 | ] 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "demo" 135 | } 136 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /demo/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to demo!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /demo/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /demo/editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Editable Demo 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/editor/main.js: -------------------------------------------------------------------------------- 1 | var editor = new SpckEditor('#editor'); 2 | var files = [{ 3 | path: 'index.html', 4 | text: multiline(function () {/* 5 | 6 | 7 | 8 | Angular Query Builder Demo 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | Loading... 22 | 23 | 24 | */})}, { 25 | path: 'systemjs.config.js', 26 | text: multiline(function () {/* 27 | var angularVersion = '@8.0.1'; 28 | 29 | System.config({ 30 | transpiler: 'ts', 31 | typescriptOptions: { 32 | emitDecoratorMetadata: true, 33 | experimentalDecorators: true 34 | }, 35 | packages: { 36 | app: { 37 | main: './app/main.ts', 38 | defaultExtension: 'ts' 39 | }, 40 | rxjs: { 41 | main: 'index.js', 42 | defaultExtension: 'js' 43 | }, 44 | "rxjs/operators": { 45 | main: 'index.js', 46 | defaultExtension: 'js' 47 | } 48 | }, 49 | meta: { 50 | 'typescript': { 'exports': 'ts' } 51 | }, 52 | paths: { 53 | 'npm:': 'https://unpkg.com/' 54 | }, 55 | map: { 56 | '@angular/core': 'npm:@angular/core' + angularVersion + '/bundles/core.umd.min.js', 57 | '@angular/common': 'npm:@angular/common' + angularVersion + '/bundles/common.umd.min.js', 58 | '@angular/compiler': 'npm:@angular/compiler' + angularVersion + '/bundles/compiler.umd.min.js', 59 | '@angular/forms': 'npm:@angular/forms' + angularVersion + '/bundles/forms.umd.min.js', 60 | '@angular/platform-browser': 'npm:@angular/platform-browser' + angularVersion + '/bundles/platform-browser.umd.js', 61 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic' + angularVersion + '/bundles/platform-browser-dynamic.umd.js', 62 | 'rxjs': 'npm:rxjs@6.2.1', 63 | 'rxjs-compat': 'npm:rxjs-compat@6.2.1', 64 | 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 65 | 'typescript': 'npm:typescript@2.9.2/lib/typescript.js', 66 | 'angular2-query-builder': 'npm:angular2-query-builder@0.5.0/dist/angular2-query-builder/bundles/angular2-query-builder.umd.js' 67 | } 68 | }); 69 | */})}, { 70 | path: 'app/main.ts', 71 | text: multiline(function () {/* 72 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 73 | 74 | import { AppModule } from './app.module'; 75 | 76 | const platform = platformBrowserDynamic(); 77 | platform.bootstrapModule(AppModule); 78 | */})}, { 79 | path: 'app/app.module.ts', 80 | text: multiline(function () {/* 81 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 82 | import { NgModule } from '@angular/core'; 83 | import { BrowserModule } from '@angular/platform-browser'; 84 | import { AppComponent } from './app.component'; 85 | import { QueryBuilderModule } from 'angular2-query-builder'; 86 | 87 | @NgModule({ 88 | imports: [ 89 | BrowserModule, 90 | FormsModule, 91 | ReactiveFormsModule, 92 | QueryBuilderModule 93 | ], 94 | declarations: [ AppComponent ], 95 | bootstrap: [ AppComponent ] 96 | }) 97 | export class AppModule {} 98 | */})}, { 99 | path: 'app/app.component.scss', 100 | text: multiline(function () {/* 101 | /deep/ html { 102 | font: 14px sans-serif; 103 | margin: 30px; 104 | } 105 | 106 | .text-input { 107 | padding: 4px 8px; 108 | border-radius: 4px; 109 | border: 1px solid #ccc; 110 | } 111 | 112 | .text-area { 113 | width: 300px; 114 | height: 100px; 115 | } 116 | 117 | .output { 118 | width: 100%; 119 | height: 300px; 120 | } 121 | */})}, { 122 | path: 'app/app.component.html', 123 | text: multiline(function () {/* 124 |

Vanilla

125 |
126 | 127 | 128 | 130 | 131 | 132 |
133 |
134 |
135 |

Control Valid (Vanilla): {{ queryCtrl.valid }}

136 |
137 | 138 |
139 |
140 |
141 |

Control Touched (Vanilla): {{ queryCtrl.touched }}

142 |
143 | 144 |
145 |
146 | 147 |
148 |
149 |

Bootstrap

150 |
151 | 152 |
153 | 155 |
156 |
157 | */})}, { 158 | path: 'app/app.component.ts', 159 | text: multiline(function () {/* 160 | import { FormBuilder, FormControl } from '@angular/forms'; 161 | import { Component } from '@angular/core'; 162 | import { QueryBuilderConfig, QueryBuilderClassNames } from "angular2-query-builder"; 163 | 164 | declare const __moduleName: string; 165 | 166 | @Component({ 167 | selector: 'my-app', 168 | moduleId: __moduleName, 169 | templateUrl: './app.component.html', 170 | styleUrls: ['./app.component.scss'] 171 | }) 172 | export class AppComponent { 173 | public queryCtrl: FormControl; 174 | 175 | public bootstrapClassNames: QueryBuilderClassNames = { 176 | removeIcon: 'fa fa-minus', 177 | addIcon: 'fa fa-plus', 178 | button: 'btn', 179 | buttonGroup: 'btn-group', 180 | rightAlign: 'order-12 ml-auto', 181 | switchRow: 'd-flex px-2', 182 | switchGroup: 'd-flex align-items-center', 183 | switchRadio: 'custom-control-input', 184 | switchLabel: 'custom-control-label', 185 | switchControl: 'custom-control custom-radio custom-control-inline', 186 | row: 'row p-2 m-1', 187 | rule: 'border', 188 | ruleSet: 'border', 189 | invalidRuleSet: 'alert alert-danger', 190 | emptyWarning: 'text-danger mx-auto', 191 | operatorControl: 'form-control', 192 | operatorControlSize: 'col-auto pr-0', 193 | fieldControl: 'form-control', 194 | fieldControlSize: 'col-auto pr-0', 195 | entityControl: 'form-control', 196 | entityControlSize: 'col-auto pr-0', 197 | inputControl: 'form-control', 198 | inputControlSize: 'col-auto' 199 | }; 200 | 201 | public query = { 202 | condition: 'and', 203 | rules: [ 204 | {field: 'age', operator: '<=', entity: 'physical'}, 205 | {field: 'birthday', operator: '=', value: new Date(), entity: 'nonphysical'}, 206 | { 207 | condition: 'or', 208 | rules: [ 209 | {field: 'gender', operator: '=', entity: 'physical'}, 210 | {field: 'occupation', operator: 'in', entity: 'nonphysical'}, 211 | {field: 'school', operator: 'is null', entity: 'nonphysical'}, 212 | {field: 'notes', operator: '=', entity: 'nonphysical'} 213 | ] 214 | } 215 | ] 216 | }; 217 | 218 | public entityConfig: QueryBuilderConfig = { 219 | entities: { 220 | physical: {name: 'Physical Attributes'}, 221 | nonphysical: {name: 'Nonphysical Attributes'} 222 | }, 223 | fields: { 224 | age: {name: 'Age', type: 'number', entity: 'physical'}, 225 | gender: { 226 | name: 'Gender', 227 | entity: 'physical', 228 | type: 'category', 229 | options: [ 230 | {name: 'Male', value: 'm'}, 231 | {name: 'Female', value: 'f'} 232 | ] 233 | }, 234 | name: {name: 'Name', type: 'string', entity: 'nonphysical'}, 235 | notes: {name: 'Notes', type: 'textarea', operators: ['=', '!='], entity: 'nonphysical'}, 236 | educated: {name: 'College Degree?', type: 'boolean', entity: 'nonphysical'}, 237 | birthday: {name: 'Birthday', type: 'date', operators: ['=', '<=', '>'], 238 | defaultValue: (() => new Date()), entity: 'nonphysical' 239 | }, 240 | school: {name: 'School', type: 'string', nullable: true, entity: 'nonphysical'}, 241 | occupation: { 242 | name: 'Occupation', 243 | entity: 'nonphysical', 244 | type: 'category', 245 | options: [ 246 | {name: 'Student', value: 'student'}, 247 | {name: 'Teacher', value: 'teacher'}, 248 | {name: 'Unemployed', value: 'unemployed'}, 249 | {name: 'Scientist', value: 'scientist'} 250 | ] 251 | } 252 | } 253 | }; 254 | 255 | public config: QueryBuilderConfig = { 256 | fields: { 257 | age: {name: 'Age', type: 'number'}, 258 | gender: { 259 | name: 'Gender', 260 | type: 'category', 261 | options: [ 262 | {name: 'Male', value: 'm'}, 263 | {name: 'Female', value: 'f'} 264 | ] 265 | }, 266 | name: {name: 'Name', type: 'string'}, 267 | notes: {name: 'Notes', type: 'textarea', operators: ['=', '!=']}, 268 | educated: {name: 'College Degree?', type: 'boolean'}, 269 | birthday: {name: 'Birthday', type: 'date', operators: ['=', '<=', '>'], 270 | defaultValue: (() => new Date()) 271 | }, 272 | school: {name: 'School', type: 'string', nullable: true}, 273 | occupation: { 274 | name: 'Occupation', 275 | type: 'category', 276 | options: [ 277 | {name: 'Student', value: 'student'}, 278 | {name: 'Teacher', value: 'teacher'}, 279 | {name: 'Unemployed', value: 'unemployed'}, 280 | {name: 'Scientist', value: 'scientist'} 281 | ] 282 | } 283 | } 284 | }; 285 | 286 | public currentConfig: QueryBuilderConfig; 287 | 288 | constructor( 289 | private formBuilder: FormBuilder 290 | ) { 291 | this.queryCtrl = this.formBuilder.control(this.query); 292 | this.currentConfig = this.config; 293 | } 294 | 295 | switchModes(event: Event) { 296 | this.currentConfig = (event.target).checked ? this.entityConfig : this.config; 297 | } 298 | 299 | changeDisabled(event: Event) { 300 | (event.target).checked ? this.queryCtrl.disable() : this.queryCtrl.enable(); 301 | } 302 | } 303 | */})} 304 | ]; 305 | 306 | editor.connect().then(() => editor.send({ 307 | project: 'Angular-QueryBuilder', 308 | files: files, 309 | open: 'app/app.component.ts' 310 | })) 311 | 312 | function multiline(f) { 313 | return f.toString() 314 | .replace(/^[^\/]+\/\*[\s\n\r]*/, '') 315 | .replace(/\*\/[^\/]+$/, '') 316 | .replace(/[\r]?\n/gi, '\r\n'); 317 | } 318 | -------------------------------------------------------------------------------- /demo/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const copyfiles = require('copyfiles'); 3 | 4 | const LIBRARY_SRC = '../dist/**/*'; 5 | const LIBRARY_DIST = 'lib'; 6 | 7 | function copyLib(callback) { 8 | copyfiles([ LIBRARY_SRC, LIBRARY_DIST ], 2, callback); 9 | } 10 | 11 | function copyLibWatch() { 12 | return gulp.watch(LIBRARY_SRC, copyLib); 13 | } 14 | 15 | module.exports = { 16 | copyLib, 17 | copyLibWatch 18 | } 19 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "concurrently --raw \"gulp copyLibWatch\" \"ng serve\"", 7 | "build": "npm run build:aot && npm run build:jit && npm run copy-editor", 8 | "build:aot": "ng build --prod", 9 | "build:jit": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e", 13 | "copy-editor": "copyfiles \"./editor/**/*\" dist", 14 | "copy-lib": "npm install ../dist/angular2-query-builder", 15 | "prebuild:aot": "npm run copy-lib", 16 | "prebuild:jit": "npm run copy-lib" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/animations": "^9.0.2", 21 | "@angular/cdk": "^9.0.1", 22 | "@angular/common": "^9.0.2", 23 | "@angular/compiler": "^9.0.2", 24 | "@angular/core": "^9.0.2", 25 | "@angular/forms": "^9.0.2", 26 | "@angular/material": "^9.0.1", 27 | "@angular/platform-browser": "^9.0.2", 28 | "@angular/platform-browser-dynamic": "^9.0.2", 29 | "@angular/router": "^9.0.2", 30 | "angular2-query-builder": "file:../dist/angular2-query-builder", 31 | "core-js": "^2.6.5", 32 | "rxjs": "~6.5.2", 33 | "tslib": "^1.10.0", 34 | "zone.js": "~0.10.2" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "~0.900.3", 38 | "@angular-devkit/build-ng-packagr": "~0.900.3", 39 | "@angular/cli": "^9.0.3", 40 | "@angular/compiler-cli": "^9.0.2", 41 | "@angular/language-service": "^9.0.2", 42 | "@types/jasmine": "^3.3.9", 43 | "@types/jasminewd2": "^2.0.6", 44 | "@types/node": "^12.11.1", 45 | "codelyzer": "^5.1.2", 46 | "concurrently": "^5.1.0", 47 | "copyfiles": "^1.2.0", 48 | "gulp": "^4.0.2", 49 | "jasmine-core": "~3.3.0", 50 | "jasmine-spec-reporter": "~4.2.1", 51 | "karma": "~4.0.0", 52 | "karma-chrome-launcher": "~2.2.0", 53 | "karma-coverage-istanbul-reporter": "~2.0.5", 54 | "karma-jasmine": "~2.0.1", 55 | "karma-jasmine-html-reporter": "^1.4.0", 56 | "protractor": "~5.4.2", 57 | "rimraf": "^2.6.1", 58 | "spck-embed": "^0.1.0", 59 | "ts-node": "~8.0.2", 60 | "tslint": "~5.12.1", 61 | "typescript": "~3.7.5" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'demo'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('demo'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to demo!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { FormBuilder, FormControl } from '@angular/forms'; 2 | import { Component } from '@angular/core'; 3 | import { QueryBuilderClassNames, QueryBuilderConfig } from 'angular2-query-builder'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | template: ` 8 |

Vanilla

9 |
10 | 11 | 12 | 14 | 15 | 16 |
17 |
18 |
19 |

Control Valid (Vanilla): {{ queryCtrl.valid }}

20 |
21 | 22 |
23 |
24 |
25 |

Control Touched (Vanilla): {{ queryCtrl.touched }}

26 |
27 | 28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 | 43 |
44 |
45 |

Custom Material

46 |
47 | 48 | 49 | 50 | 52 | 54 | 56 | 57 | 58 | chevron_right 59 | 60 | 61 | 64 | 65 | 66 | 67 | And 68 | Or 69 | 70 | 71 | 72 | 73 | 74 | 75 | {{entity.name}} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | {{ field.name }} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {{ value }} 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {{ opt.name }} 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | {{ opt.name }} 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 140 | 141 | 142 | 143 | 144 |
145 |

Bootstrap

146 |
147 | 148 |
149 | 151 |
152 |
153 | `, 154 | styles: [` 155 | /deep/ html { 156 | font: 14px sans-serif; 157 | margin: 30px; 158 | } 159 | 160 | .mat-icon-button { 161 | outline: none; 162 | } 163 | 164 | .mat-arrow-icon { 165 | outline: none; 166 | line-height: 32px; 167 | } 168 | 169 | .mat-form-field { 170 | padding-left: 5px; 171 | padding-right: 5px; 172 | } 173 | 174 | .text-input { 175 | padding: 4px 8px; 176 | border-radius: 4px; 177 | border: 1px solid #ccc; 178 | } 179 | 180 | .text-area { 181 | width: 300px; 182 | height: 100px; 183 | } 184 | 185 | .output { 186 | width: 100%; 187 | height: 300px; 188 | } 189 | `] 190 | }) 191 | export class AppComponent { 192 | public queryCtrl: FormControl; 193 | 194 | public bootstrapClassNames: QueryBuilderClassNames = { 195 | removeIcon: 'fa fa-minus', 196 | addIcon: 'fa fa-plus', 197 | arrowIcon: 'fa fa-chevron-right px-2', 198 | button: 'btn', 199 | buttonGroup: 'btn-group', 200 | rightAlign: 'order-12 ml-auto', 201 | switchRow: 'd-flex px-2', 202 | switchGroup: 'd-flex align-items-center', 203 | switchRadio: 'custom-control-input', 204 | switchLabel: 'custom-control-label', 205 | switchControl: 'custom-control custom-radio custom-control-inline', 206 | row: 'row p-2 m-1', 207 | rule: 'border', 208 | ruleSet: 'border', 209 | invalidRuleSet: 'alert alert-danger', 210 | emptyWarning: 'text-danger mx-auto', 211 | operatorControl: 'form-control', 212 | operatorControlSize: 'col-auto pr-0', 213 | fieldControl: 'form-control', 214 | fieldControlSize: 'col-auto pr-0', 215 | entityControl: 'form-control', 216 | entityControlSize: 'col-auto pr-0', 217 | inputControl: 'form-control', 218 | inputControlSize: 'col-auto' 219 | }; 220 | 221 | public query = { 222 | condition: 'and', 223 | rules: [ 224 | {field: 'age', operator: '<=', entity: 'physical'}, 225 | {field: 'birthday', operator: '=', value: new Date(), entity: 'nonphysical'}, 226 | { 227 | condition: 'or', 228 | rules: [ 229 | {field: 'gender', operator: '=', entity: 'physical'}, 230 | {field: 'occupation', operator: 'in', entity: 'nonphysical'}, 231 | {field: 'school', operator: 'is null', entity: 'nonphysical'}, 232 | {field: 'notes', operator: '=', entity: 'nonphysical'} 233 | ] 234 | } 235 | ] 236 | }; 237 | 238 | public entityConfig: QueryBuilderConfig = { 239 | entities: { 240 | physical: {name: 'Physical Attributes'}, 241 | nonphysical: {name: 'Nonphysical Attributes'} 242 | }, 243 | fields: { 244 | age: {name: 'Age', type: 'number', entity: 'physical'}, 245 | gender: { 246 | name: 'Gender', 247 | entity: 'physical', 248 | type: 'category', 249 | options: [ 250 | {name: 'Male', value: 'm'}, 251 | {name: 'Female', value: 'f'} 252 | ] 253 | }, 254 | name: {name: 'Name', type: 'string', entity: 'nonphysical'}, 255 | notes: {name: 'Notes', type: 'textarea', operators: ['=', '!='], entity: 'nonphysical'}, 256 | educated: {name: 'College Degree?', type: 'boolean', entity: 'nonphysical'}, 257 | birthday: {name: 'Birthday', type: 'date', operators: ['=', '<=', '>'], 258 | defaultValue: (() => new Date()), entity: 'nonphysical' 259 | }, 260 | school: {name: 'School', type: 'string', nullable: true, entity: 'nonphysical'}, 261 | occupation: { 262 | name: 'Occupation', 263 | entity: 'nonphysical', 264 | type: 'category', 265 | options: [ 266 | {name: 'Student', value: 'student'}, 267 | {name: 'Teacher', value: 'teacher'}, 268 | {name: 'Unemployed', value: 'unemployed'}, 269 | {name: 'Scientist', value: 'scientist'} 270 | ] 271 | } 272 | } 273 | }; 274 | 275 | public config: QueryBuilderConfig = { 276 | fields: { 277 | age: {name: 'Age', type: 'number'}, 278 | gender: { 279 | name: 'Gender', 280 | type: 'category', 281 | options: [ 282 | {name: 'Male', value: 'm'}, 283 | {name: 'Female', value: 'f'} 284 | ] 285 | }, 286 | name: {name: 'Name', type: 'string'}, 287 | notes: {name: 'Notes', type: 'textarea', operators: ['=', '!=']}, 288 | educated: {name: 'College Degree?', type: 'boolean'}, 289 | birthday: {name: 'Birthday', type: 'date', operators: ['=', '<=', '>'], 290 | defaultValue: (() => new Date()) 291 | }, 292 | school: {name: 'School', type: 'string', nullable: true}, 293 | occupation: { 294 | name: 'Occupation', 295 | type: 'category', 296 | options: [ 297 | {name: 'Student', value: 'student'}, 298 | {name: 'Teacher', value: 'teacher'}, 299 | {name: 'Unemployed', value: 'unemployed'}, 300 | {name: 'Scientist', value: 'scientist'} 301 | ] 302 | } 303 | } 304 | }; 305 | 306 | public currentConfig: QueryBuilderConfig; 307 | public allowRuleset: boolean = true; 308 | public allowCollapse: boolean; 309 | public persistValueOnFieldChange: boolean = false; 310 | 311 | constructor( 312 | private formBuilder: FormBuilder 313 | ) { 314 | this.queryCtrl = this.formBuilder.control(this.query); 315 | this.currentConfig = this.config; 316 | } 317 | 318 | switchModes(event: Event) { 319 | this.currentConfig = (event.target).checked ? this.entityConfig : this.config; 320 | } 321 | 322 | changeDisabled(event: Event) { 323 | (event.target).checked ? this.queryCtrl.disable() : this.queryCtrl.enable(); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { AppComponent } from './app.component'; 5 | 6 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatCardModule } from '@angular/material/card'; 9 | import { MatCheckboxModule } from '@angular/material/checkbox'; 10 | import { MatNativeDateModule } from '@angular/material/core'; 11 | import { MatDatepickerModule } from '@angular/material/datepicker'; 12 | import { MatIconModule } from '@angular/material/icon'; 13 | import { MatInputModule } from '@angular/material/input'; 14 | import { MatRadioModule } from '@angular/material/radio'; 15 | import { MatSelectModule } from '@angular/material/select'; 16 | 17 | import { QueryBuilderModule } from 'angular2-query-builder'; 18 | 19 | @NgModule({ 20 | imports: [ 21 | BrowserModule, 22 | FormsModule, 23 | ReactiveFormsModule, 24 | NoopAnimationsModule, 25 | MatButtonModule, 26 | MatCheckboxModule, 27 | MatSelectModule, 28 | MatInputModule, 29 | MatDatepickerModule, 30 | MatNativeDateModule, 31 | MatRadioModule, 32 | MatIconModule, 33 | MatCardModule, 34 | QueryBuilderModule 35 | ], 36 | declarations: [ AppComponent ], 37 | bootstrap: [ AppComponent ] 38 | }) 39 | export class AppModule { 40 | } 41 | -------------------------------------------------------------------------------- /demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zebzhao/Angular-QueryBuilder/34c070bfab3a54c61b6fd00b680a4c6a79eb6e60/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /demo/src/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 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zebzhao/Angular-QueryBuilder/34c070bfab3a54c61b6fd00b680a4c6a79eb6e60/demo/src/favicon.ico -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/src/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'), 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 | }); 31 | }; -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /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/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/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 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /karma-test-entry.ts: -------------------------------------------------------------------------------- 1 | import 'core-js'; 2 | import 'rxjs/Rx'; 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/long-stack-trace-zone'; 5 | import 'zone.js/dist/async-test'; 6 | import 'zone.js/dist/fake-async-test'; 7 | import 'zone.js/dist/sync-test'; 8 | import 'zone.js/dist/proxy'; 9 | import 'zone.js/dist/jasmine-patch'; 10 | 11 | import { TestBed } from '@angular/core/testing'; 12 | 13 | import { 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting 16 | } from '@angular/platform-browser-dynamic/testing'; 17 | 18 | TestBed.initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | 23 | const testsContext: any = require.context('./src', true, /\.spec/); 24 | testsContext.keys().forEach(testsContext); 25 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | import webpackTestConfig from './webpack-test.config'; 2 | import { ConfigOptions } from 'karma'; 3 | 4 | export default (config) => { 5 | config.set({ 6 | // Base path that will be used to resolve all patterns (eg. files, exclude). 7 | basePath: './', 8 | 9 | // Frameworks to use. 10 | // Available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['jasmine'], 12 | 13 | // List of files to load in the browser. 14 | files: [ 15 | 'karma-test-entry.ts' 16 | ], 17 | 18 | // Preprocess matching files before serving them to the browser. 19 | // Available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 20 | preprocessors: { 21 | 'karma-test-entry.ts': ['webpack', 'sourcemap'] 22 | }, 23 | 24 | webpack: webpackTestConfig, 25 | 26 | // Webpack please don't spam the console when running in karma! 27 | webpackMiddleware: { 28 | noInfo: true, 29 | // Use stats to turn off verbose output. 30 | stats: { 31 | chunks: false 32 | } 33 | }, 34 | 35 | mime: { 36 | 'text/x-typescript': [ 'ts' ] 37 | }, 38 | 39 | coverageIstanbulReporter: { 40 | reports: ['text-summary', 'html', 'lcovonly'], 41 | fixWebpackSourcePaths: true 42 | }, 43 | 44 | // Test results reporter to use. 45 | // Possible values: 'dots', 'progress'. 46 | // Available reporters: https://npmjs.org/browse/keyword/karma-reporter 47 | reporters: ['mocha', 'coverage-istanbul'], 48 | 49 | // Level of logging 50 | // Possible values: 51 | // - config.LOG_DISABLE 52 | // - config.LOG_ERROR 53 | // - config.LOG_WARN 54 | // - config.LOG_INFO 55 | // - config.LOG_DEBUG 56 | logLevel: config.LOG_WARN, 57 | 58 | // Start these browsers. 59 | // Available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 60 | browsers: ['Chrome'], 61 | 62 | browserConsoleLogOptions: { 63 | terminal: true, 64 | level: 'log' 65 | }, 66 | 67 | singleRun: true, 68 | colors: true 69 | } as ConfigOptions); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-query-builder", 3 | "version": "0.6.2", 4 | "description": "A modernized Angular 2+ query builder based on jquery QueryBuilder", 5 | "main": "./dist/angular2-query-builder/bundles/angular2-query-builder.umd.js", 6 | "module": "./dist/angular2-query-builder/esm5/angular2-query-builder.js", 7 | "typings": "./dist/angular2-query-builder/angular2-query-builder.d.ts", 8 | "license": "MIT", 9 | "private": false, 10 | "keywords": [ 11 | "angular", 12 | "angular8", 13 | "query", 14 | "builder", 15 | "jquery", 16 | "querybuilder", 17 | "visual" 18 | ], 19 | "author": { 20 | "name": "Zeb Zhao", 21 | "url": "https://github.com/zebzhao/" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/zebzhao/Angular-QueryBuilder" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/zebzhao/Angular-QueryBuilder/issues" 29 | }, 30 | "homepage": "https://github.com/zebzhao/Angular-QueryBuilder#readme", 31 | "scripts": { 32 | "ng": "ng", 33 | "start": "ng serve", 34 | "build": "ng build angular2-query-builder", 35 | "build:watch": "ng build angular2-query-builder --watch=true", 36 | "test": "ng test --watch=false", 37 | "lint": "ng lint", 38 | "e2e": "ng e2e", 39 | "ngcompile": "npm run build", 40 | "build:esm:watch": "npm build:watch", 41 | "build:umd": "npm run build", 42 | "build:umd:watch": "npm build:watch", 43 | "ci": "npm run lint && npm run test && npm run build && npm run docs", 44 | "clean:all": "npm run clean:tmp && rimraf node_modules", 45 | "clean:tmp": "rimraf coverage dist tmp docs", 46 | "codecov": "cat coverage/lcov.info | codecov", 47 | "docs": "compodoc -p tsconfig.json -d docs --disableCoverage", 48 | "explorer": "source-map-explorer ./dist/angular2-query-builder/bundles/angular2-query-builder.umd.js", 49 | "copyDemo": "copyfiles -u 2 demo/dist/**/* docs", 50 | "gh-pages": "rimraf docs && npm run docs && npm run copyDemo && gh-pages -d docs", 51 | "postversion": "git push && git push --tags", 52 | "prebuild": "rimraf dist tmp", 53 | "prebuild:watch": "rimraf dist tmp", 54 | "prepublishOnly": "npm run ci", 55 | "preversion": "npm run ci", 56 | "test:watch": "ng test --watch=true", 57 | "tslint": "ng lint" 58 | }, 59 | "peerDependencies": {}, 60 | "devDependencies": { 61 | "@angular-devkit/build-angular": "~0.900.3", 62 | "@angular-devkit/build-ng-packagr": "~0.900.3", 63 | "@angular/cli": "^9.0.3", 64 | "@angular/common": "^9.0.2", 65 | "@angular/compiler": "^9.0.2", 66 | "@angular/compiler-cli": "^9.0.2", 67 | "@angular/core": "^9.0.2", 68 | "@angular/forms": "^9.0.2", 69 | "@angular/language-service": "^9.0.2", 70 | "@angular/platform-browser": "^9.0.2", 71 | "@angular/platform-browser-dynamic": "^9.0.2", 72 | "@types/jasmine": "^3.3.8", 73 | "@types/jasminewd2": "^2.0.3", 74 | "@types/node": "^12.11.1", 75 | "codelyzer": "^5.1.2", 76 | "copyfiles": "^2.2.0", 77 | "compodoc": "^0.0.41", 78 | "core-js": "^2.6.5", 79 | "gh-pages": "^2.2.0", 80 | "jasmine-core": "^3.4.0", 81 | "jasmine-spec-reporter": "^4.2.1", 82 | "karma": "^4.1.0", 83 | "karma-chrome-launcher": "^2.2.0", 84 | "karma-coverage-istanbul-reporter": "^2.0.1", 85 | "karma-jasmine": "^2.0.1", 86 | "karma-jasmine-html-reporter": "^1.4.0", 87 | "ng-packagr": "^9.0.0", 88 | "node-sass": "^4.13.1", 89 | "protractor": "^5.4.0", 90 | "rxjs": "~6.5.4", 91 | "ts-node": "^7.0.0", 92 | "tslint": "^5.15.0", 93 | "typescript": "3.7.5", 94 | "zone.js": "~0.10.2" 95 | }, 96 | "dependencies": { 97 | "tslib": "^1.10.0" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/README.md: -------------------------------------------------------------------------------- 1 | # Angular2QueryBuilder 2 | 3 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.1. 4 | 5 | ## Code scaffolding 6 | 7 | Run `ng generate component component-name --project angular2-query-builder` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project angular2-query-builder`. 8 | > Note: Don't forget to add `--project angular2-query-builder` or else it will be added to the default project in your `angular.json` file. 9 | 10 | ## Build 11 | 12 | Run `ng build angular2-query-builder` to build the project. The build artifacts will be stored in the `dist/` directory. 13 | 14 | ## Publishing 15 | 16 | After building your library with `ng build angular2-query-builder`, go to the dist folder `cd dist/angular2-query-builder` and run `npm publish`. 17 | 18 | ## Running unit tests 19 | 20 | Run `ng test angular2-query-builder` to execute the unit tests via [Karma](https://karma-runner.github.io). 21 | 22 | ## Further help 23 | 24 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 25 | -------------------------------------------------------------------------------- /projects/angular2-query-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/angular2-query-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/angular2-query-builder/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular2-query-builder", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/angular2-query-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-query-builder", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": ">=8.0.0", 6 | "@angular/core": ">=8.0.0", 7 | "@angular/forms": ">=8.0.0", 8 | "rxjs": ">=6.0.0", 9 | "tslib": ">=1.8.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/angular2-query-builder.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, } from '@angular/forms'; 4 | 5 | import { QueryBuilderComponent } from './query-builder/query-builder.component'; 6 | 7 | import { QueryArrowIconDirective } from './query-builder/query-arrow-icon.directive'; 8 | import { QueryFieldDirective } from './query-builder/query-field.directive'; 9 | import { QueryInputDirective } from './query-builder/query-input.directive'; 10 | import { QueryEntityDirective } from './query-builder/query-entity.directive'; 11 | import { QueryOperatorDirective } from './query-builder/query-operator.directive'; 12 | import { QueryButtonGroupDirective } from './query-builder/query-button-group.directive'; 13 | import { QuerySwitchGroupDirective } from './query-builder/query-switch-group.directive'; 14 | import { QueryRemoveButtonDirective } from './query-builder/query-remove-button.directive'; 15 | import { QueryEmptyWarningDirective } from './query-builder/query-empty-warning.directive'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule 21 | ], 22 | declarations: [ 23 | QueryBuilderComponent, 24 | QueryInputDirective, 25 | QueryOperatorDirective, 26 | QueryFieldDirective, 27 | QueryEntityDirective, 28 | QueryButtonGroupDirective, 29 | QuerySwitchGroupDirective, 30 | QueryRemoveButtonDirective, 31 | QueryEmptyWarningDirective, 32 | QueryArrowIconDirective 33 | ], 34 | exports: [ 35 | QueryBuilderComponent, 36 | QueryInputDirective, 37 | QueryOperatorDirective, 38 | QueryFieldDirective, 39 | QueryEntityDirective, 40 | QueryButtonGroupDirective, 41 | QuerySwitchGroupDirective, 42 | QueryRemoveButtonDirective, 43 | QueryEmptyWarningDirective, 44 | QueryArrowIconDirective 45 | ] 46 | }) 47 | export class QueryBuilderModule { } 48 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-arrow-icon.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryArrowIcon]'}) 4 | export class QueryArrowIconDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-builder.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 | 23 | 26 | 27 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | 43 | 44 |
45 |
46 | 48 | 49 |
50 |
51 |
52 |
53 | 54 |
55 |
    56 | 57 | 58 | 59 |
  • 60 | 61 | 62 | 63 |
    64 | 65 |
    66 |
    67 | 68 | 69 |
    70 | 73 |
    74 |
    75 | 76 |
    77 | 78 | 79 | 80 |
    81 | 82 | 83 |
    84 | 90 |
    91 |
    92 | 93 | 94 | 95 | 96 | 97 | 98 |
    99 | 105 |
    106 |
    107 | 108 | 109 | 110 | 111 | 112 | 113 |
    114 | 120 |
    121 |
    122 | 123 | 124 | 125 | 126 | 127 | 128 |
    129 | 131 | 133 | 135 | 137 | 143 | 144 | 150 | 151 | 153 |
    154 |
    155 | 156 |
    157 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |

    175 | {{emptyMessage}} 176 |

    177 |
    178 |
  • 179 |
    180 |
    181 |
182 |
-------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-builder.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | width: 100%; 4 | 5 | .q-icon { 6 | font-style: normal; 7 | font-size: 12px; 8 | } 9 | 10 | .q-remove-icon { 11 | &::before { 12 | content: '❌' 13 | } 14 | } 15 | 16 | .q-arrow-icon-button { 17 | float: left; 18 | margin: 4px 6px 4px 0; 19 | transform: rotate(90deg); 20 | transition: linear 0.25s transform; 21 | cursor: pointer; 22 | 23 | &.q-collapsed { 24 | transform: rotate(0); 25 | } 26 | } 27 | 28 | .q-arrow-icon { 29 | &::before { 30 | content: '▶' 31 | } 32 | } 33 | 34 | .q-add-icon { 35 | color: #555; 36 | &::before { 37 | content: '➕' 38 | } 39 | } 40 | 41 | .q-remove-button { 42 | color: #B3415D; 43 | width: 31px; 44 | } 45 | 46 | .q-switch-group, .q-button-group { 47 | font-family: "Lucida Grande", Tahoma, Verdana, sans-serif; 48 | overflow: hidden; 49 | } 50 | 51 | .q-right-align { 52 | float: right; 53 | } 54 | 55 | .q-button { 56 | margin-left: 8px; 57 | padding: 0 8px; 58 | background-color: white; 59 | 60 | &:disabled { 61 | display: none; 62 | } 63 | } 64 | 65 | .q-control-size { 66 | display: inline-block; 67 | vertical-align: top; 68 | padding-right: 10px; 69 | } 70 | 71 | .q-input-control, .q-operator-control, .q-field-control, .q-entity-control { 72 | display: inline-block; 73 | padding: 5px 8px; 74 | color: #555; 75 | background-color: #fff; 76 | background-image: none; 77 | border: 1px solid #ccc; 78 | border-radius: 4px; 79 | box-sizing: border-box; 80 | width: auto; 81 | 82 | &:disabled { 83 | border-color: transparent; 84 | } 85 | } 86 | 87 | .q-operator-control, .q-field-control,.q-entity-control, .q-input-control:not([type='checkbox']) { 88 | min-height: 32px; 89 | -webkit-appearance: none; 90 | } 91 | 92 | .q-switch-label, .q-button { 93 | float: left; 94 | margin-bottom: 0; 95 | font-size: 14px; 96 | line-height: 30px; 97 | font-weight: normal; 98 | text-align: center; 99 | text-shadow: none; 100 | border: 1px solid rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | 103 | &:hover { 104 | cursor: pointer; 105 | background-color: #F0F0F0; 106 | } 107 | } 108 | 109 | .q-switch-label { 110 | background-color: #e4e4e4; 111 | padding: 0 8px; 112 | } 113 | 114 | .q-switch-radio { 115 | position: absolute; 116 | clip: rect(0, 0, 0, 0); 117 | height: 1px; 118 | width: 1px; 119 | border: 0; 120 | overflow: hidden; 121 | 122 | &:checked + .q-switch-label { 123 | border: 1px solid rgb(97, 158, 215); 124 | background: white; 125 | color: rgb(49, 118, 179); 126 | } 127 | 128 | &:disabled + .q-switch-label { 129 | display: none; 130 | } 131 | 132 | &:checked:disabled + .q-switch-label { 133 | display: initial; 134 | color: initial; 135 | cursor: default; 136 | border-color: transparent; 137 | } 138 | } 139 | 140 | .q-invalid-ruleset { 141 | border: 1px solid rgba(179, 65, 93, 0.5) !important; 142 | background: rgba(179, 65, 93, 0.1) !important; 143 | } 144 | 145 | .q-empty-warning { 146 | color: rgb(141, 37, 46); 147 | text-align: center; 148 | } 149 | 150 | .q-ruleset { 151 | border: 1px solid #CCC; 152 | } 153 | 154 | .q-rule { 155 | border: 1px solid #CCC; 156 | background: white; 157 | } 158 | 159 | .q-transition { 160 | -webkit-transition: all 0.1s ease-in-out; 161 | -moz-transition: all 0.1s ease-in-out; 162 | -ms-transition: all 0.1s ease-in-out; 163 | -o-transition: all 0.1s ease-in-out; 164 | transition: all 0.1s ease-in-out; 165 | } 166 | 167 | .q-tree-container { 168 | width: 100%; 169 | overflow: hidden; 170 | transition: ease-in 0.25s max-height; 171 | 172 | &.q-collapsed { 173 | max-height: 0 !important; 174 | } 175 | } 176 | 177 | .q-tree { 178 | list-style: none; 179 | margin: 4px 0 2px; 180 | } 181 | 182 | .q-row { 183 | padding: 6px 8px; 184 | margin-top: 6px; 185 | } 186 | 187 | .q-connector { 188 | position: relative; 189 | 190 | &::before { 191 | top: -5px; 192 | border-width: 0 0 2px 2px; 193 | } 194 | 195 | &::after { 196 | border-width: 0 0 0 2px; 197 | top: 50%; 198 | } 199 | 200 | &::before, &::after { 201 | content: ''; 202 | left: -12px; 203 | border-color: #CCC; 204 | border-style: solid; 205 | width: 9px; 206 | height: calc(50% + 6px); 207 | position: absolute; 208 | } 209 | 210 | &:last-child::after { 211 | content: none; 212 | } 213 | } 214 | 215 | .q-inline-block-display{ 216 | display: inline-block; 217 | vertical-align: top; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-builder.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, } from '@angular/forms'; 4 | import { QueryBuilderComponent } from './query-builder.component'; 5 | 6 | describe('QueryBuilderComponent', () => { 7 | let component: QueryBuilderComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [ CommonModule, FormsModule ], 13 | declarations: [ QueryBuilderComponent ] 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(QueryBuilderComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should be created', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-builder.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbstractControl, 3 | ControlValueAccessor, 4 | NG_VALUE_ACCESSOR, 5 | NG_VALIDATORS, 6 | ValidationErrors, 7 | Validator 8 | } from '@angular/forms'; 9 | import { QueryOperatorDirective } from './query-operator.directive'; 10 | import { QueryFieldDirective } from './query-field.directive'; 11 | import { QueryEntityDirective } from './query-entity.directive'; 12 | import { QuerySwitchGroupDirective } from './query-switch-group.directive'; 13 | import { QueryButtonGroupDirective } from './query-button-group.directive'; 14 | import { QueryInputDirective } from './query-input.directive'; 15 | import { QueryRemoveButtonDirective } from './query-remove-button.directive'; 16 | import { QueryEmptyWarningDirective } from './query-empty-warning.directive'; 17 | import { QueryArrowIconDirective } from './query-arrow-icon.directive'; 18 | import { 19 | ButtonGroupContext, 20 | Entity, 21 | Field, 22 | SwitchGroupContext, 23 | EntityContext, 24 | FieldContext, 25 | InputContext, 26 | LocalRuleMeta, 27 | OperatorContext, 28 | Option, 29 | QueryBuilderClassNames, 30 | QueryBuilderConfig, 31 | RemoveButtonContext, 32 | ArrowIconContext, 33 | Rule, 34 | RuleSet, 35 | EmptyWarningContext, 36 | } from './query-builder.interfaces'; 37 | import { 38 | ChangeDetectorRef, 39 | Component, 40 | ContentChild, 41 | ContentChildren, 42 | forwardRef, 43 | Input, 44 | OnChanges, 45 | OnInit, 46 | QueryList, 47 | SimpleChanges, 48 | TemplateRef, 49 | ViewChild, 50 | ElementRef 51 | } from '@angular/core'; 52 | 53 | export const CONTROL_VALUE_ACCESSOR: any = { 54 | provide: NG_VALUE_ACCESSOR, 55 | useExisting: forwardRef(() => QueryBuilderComponent), 56 | multi: true 57 | }; 58 | 59 | export const VALIDATOR: any = { 60 | provide: NG_VALIDATORS, 61 | useExisting: forwardRef(() => QueryBuilderComponent), 62 | multi: true 63 | }; 64 | 65 | @Component({ 66 | selector: 'query-builder', 67 | templateUrl: './query-builder.component.html', 68 | styleUrls: ['./query-builder.component.scss'], 69 | providers: [CONTROL_VALUE_ACCESSOR, VALIDATOR] 70 | }) 71 | export class QueryBuilderComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { 72 | public fields: Field[]; 73 | public filterFields: Field[]; 74 | public entities: Entity[]; 75 | public defaultClassNames: QueryBuilderClassNames = { 76 | arrowIconButton: 'q-arrow-icon-button', 77 | arrowIcon: 'q-icon q-arrow-icon', 78 | removeIcon: 'q-icon q-remove-icon', 79 | addIcon: 'q-icon q-add-icon', 80 | button: 'q-button', 81 | buttonGroup: 'q-button-group', 82 | removeButton: 'q-remove-button', 83 | switchGroup: 'q-switch-group', 84 | switchLabel: 'q-switch-label', 85 | switchRadio: 'q-switch-radio', 86 | rightAlign: 'q-right-align', 87 | transition: 'q-transition', 88 | collapsed: 'q-collapsed', 89 | treeContainer: 'q-tree-container', 90 | tree: 'q-tree', 91 | row: 'q-row', 92 | connector: 'q-connector', 93 | rule: 'q-rule', 94 | ruleSet: 'q-ruleset', 95 | invalidRuleSet: 'q-invalid-ruleset', 96 | emptyWarning: 'q-empty-warning', 97 | fieldControl: 'q-field-control', 98 | fieldControlSize: 'q-control-size', 99 | entityControl: 'q-entity-control', 100 | entityControlSize: 'q-control-size', 101 | operatorControl: 'q-operator-control', 102 | operatorControlSize: 'q-control-size', 103 | inputControl: 'q-input-control', 104 | inputControlSize: 'q-control-size' 105 | }; 106 | public defaultOperatorMap: { [key: string]: string[] } = { 107 | string: ['=', '!=', 'contains', 'like'], 108 | number: ['=', '!=', '>', '>=', '<', '<='], 109 | time: ['=', '!=', '>', '>=', '<', '<='], 110 | date: ['=', '!=', '>', '>=', '<', '<='], 111 | category: ['=', '!=', 'in', 'not in'], 112 | boolean: ['='] 113 | }; 114 | @Input() disabled: boolean; 115 | @Input() data: RuleSet = { condition: 'and', rules: [] }; 116 | 117 | // For ControlValueAccessor interface 118 | public onChangeCallback: () => void; 119 | public onTouchedCallback: () => any; 120 | 121 | @Input() allowRuleset: boolean = true; 122 | @Input() allowCollapse: boolean = false; 123 | @Input() emptyMessage: string = 'A ruleset cannot be empty. Please add a rule or remove it all together.'; 124 | @Input() classNames: QueryBuilderClassNames; 125 | @Input() operatorMap: { [key: string]: string[] }; 126 | @Input() parentValue: RuleSet; 127 | @Input() config: QueryBuilderConfig = { fields: {} }; 128 | @Input() parentArrowIconTemplate: QueryArrowIconDirective; 129 | @Input() parentInputTemplates: QueryList; 130 | @Input() parentOperatorTemplate: QueryOperatorDirective; 131 | @Input() parentFieldTemplate: QueryFieldDirective; 132 | @Input() parentEntityTemplate: QueryEntityDirective; 133 | @Input() parentSwitchGroupTemplate: QuerySwitchGroupDirective; 134 | @Input() parentButtonGroupTemplate: QueryButtonGroupDirective; 135 | @Input() parentRemoveButtonTemplate: QueryRemoveButtonDirective; 136 | @Input() parentEmptyWarningTemplate: QueryEmptyWarningDirective; 137 | @Input() parentChangeCallback: () => void; 138 | @Input() parentTouchedCallback: () => void; 139 | @Input() persistValueOnFieldChange: boolean = false; 140 | 141 | @ViewChild('treeContainer', {static: true}) treeContainer: ElementRef; 142 | 143 | @ContentChild(QueryButtonGroupDirective) buttonGroupTemplate: QueryButtonGroupDirective; 144 | @ContentChild(QuerySwitchGroupDirective) switchGroupTemplate: QuerySwitchGroupDirective; 145 | @ContentChild(QueryFieldDirective) fieldTemplate: QueryFieldDirective; 146 | @ContentChild(QueryEntityDirective) entityTemplate: QueryEntityDirective; 147 | @ContentChild(QueryOperatorDirective) operatorTemplate: QueryOperatorDirective; 148 | @ContentChild(QueryRemoveButtonDirective) removeButtonTemplate: QueryRemoveButtonDirective; 149 | @ContentChild(QueryEmptyWarningDirective) emptyWarningTemplate: QueryEmptyWarningDirective; 150 | @ContentChildren(QueryInputDirective) inputTemplates: QueryList; 151 | @ContentChild(QueryArrowIconDirective) arrowIconTemplate: QueryArrowIconDirective; 152 | 153 | private defaultTemplateTypes: string[] = [ 154 | 'string', 'number', 'time', 'date', 'category', 'boolean', 'multiselect']; 155 | private defaultPersistValueTypes: string[] = [ 156 | 'string', 'number', 'time', 'date', 'boolean']; 157 | private defaultEmptyList: any[] = []; 158 | private operatorsCache: { [key: string]: string[] }; 159 | private inputContextCache = new Map(); 160 | private operatorContextCache = new Map(); 161 | private fieldContextCache = new Map(); 162 | private entityContextCache = new Map(); 163 | private removeButtonContextCache = new Map(); 164 | private buttonGroupContext: ButtonGroupContext; 165 | 166 | constructor(private changeDetectorRef: ChangeDetectorRef) { } 167 | 168 | // ----------OnInit Implementation---------- 169 | 170 | ngOnInit() { } 171 | 172 | // ----------OnChanges Implementation---------- 173 | 174 | ngOnChanges(changes: SimpleChanges) { 175 | const config = this.config; 176 | const type = typeof config; 177 | if (type === 'object') { 178 | this.fields = Object.keys(config.fields).map((value) => { 179 | const field = config.fields[value]; 180 | field.value = field.value || value; 181 | return field; 182 | }); 183 | if (config.entities) { 184 | this.entities = Object.keys(config.entities).map((value) => { 185 | const entity = config.entities[value]; 186 | entity.value = entity.value || value; 187 | return entity; 188 | }); 189 | } else { 190 | this.entities = null; 191 | } 192 | this.operatorsCache = {}; 193 | } else { 194 | throw new Error(`Expected 'config' must be a valid object, got ${type} instead.`); 195 | } 196 | } 197 | 198 | // ----------Validator Implementation---------- 199 | 200 | validate(control: AbstractControl): ValidationErrors | null { 201 | const errors: { [key: string]: any } = {}; 202 | const ruleErrorStore = []; 203 | let hasErrors = false; 204 | 205 | if (!this.config.allowEmptyRulesets && this.checkEmptyRuleInRuleset(this.data)) { 206 | errors.empty = 'Empty rulesets are not allowed.'; 207 | hasErrors = true; 208 | } 209 | 210 | this.validateRulesInRuleset(this.data, ruleErrorStore); 211 | 212 | if (ruleErrorStore.length) { 213 | errors.rules = ruleErrorStore; 214 | hasErrors = true; 215 | } 216 | return hasErrors ? errors : null; 217 | } 218 | 219 | // ----------ControlValueAccessor Implementation---------- 220 | 221 | @Input() 222 | get value(): RuleSet { 223 | return this.data; 224 | } 225 | set value(value: RuleSet) { 226 | // When component is initialized without a formControl, null is passed to value 227 | this.data = value || { condition: 'and', rules: [] }; 228 | this.handleDataChange(); 229 | } 230 | 231 | writeValue(obj: any): void { 232 | this.value = obj; 233 | } 234 | registerOnChange(fn: any): void { 235 | this.onChangeCallback = () => fn(this.data); 236 | } 237 | registerOnTouched(fn: any): void { 238 | this.onTouchedCallback = () => fn(this.data); 239 | } 240 | setDisabledState(isDisabled: boolean): void { 241 | this.disabled = isDisabled; 242 | this.changeDetectorRef.detectChanges(); 243 | } 244 | 245 | // ----------END---------- 246 | 247 | getDisabledState = (): boolean => { 248 | return this.disabled; 249 | } 250 | 251 | findTemplateForRule(rule: Rule): TemplateRef { 252 | const type = this.getInputType(rule.field, rule.operator); 253 | if (type) { 254 | const queryInput = this.findQueryInput(type); 255 | if (queryInput) { 256 | return queryInput.template; 257 | } else { 258 | if (this.defaultTemplateTypes.indexOf(type) === -1) { 259 | console.warn(`Could not find template for field with type: ${type}`); 260 | } 261 | return null; 262 | } 263 | } 264 | } 265 | 266 | findQueryInput(type: string): QueryInputDirective { 267 | const templates = this.parentInputTemplates || this.inputTemplates; 268 | return templates.find((item) => item.queryInputType === type); 269 | } 270 | 271 | getOperators(field: string): string[] { 272 | if (this.operatorsCache[field]) { 273 | return this.operatorsCache[field]; 274 | } 275 | let operators = this.defaultEmptyList; 276 | const fieldObject = this.config.fields[field]; 277 | 278 | if (this.config.getOperators) { 279 | return this.config.getOperators(field, fieldObject); 280 | } 281 | 282 | const type = fieldObject.type; 283 | 284 | if (fieldObject && fieldObject.operators) { 285 | operators = fieldObject.operators; 286 | } else if (type) { 287 | operators = (this.operatorMap && this.operatorMap[type]) || this.defaultOperatorMap[type] || this.defaultEmptyList; 288 | if (operators.length === 0) { 289 | console.warn( 290 | `No operators found for field '${field}' with type ${fieldObject.type}. ` + 291 | `Please define an 'operators' property on the field or use the 'operatorMap' binding to fix this.`); 292 | } 293 | if (fieldObject.nullable) { 294 | operators = operators.concat(['is null', 'is not null']); 295 | } 296 | } else { 297 | console.warn(`No 'type' property found on field: '${field}'`); 298 | } 299 | 300 | // Cache reference to array object, so it won't be computed next time and trigger a rerender. 301 | this.operatorsCache[field] = operators; 302 | return operators; 303 | } 304 | 305 | getFields(entity: string): Field[] { 306 | if (this.entities && entity) { 307 | return this.fields.filter((field) => { 308 | return field && field.entity === entity; 309 | }); 310 | } else { 311 | return this.fields; 312 | } 313 | } 314 | 315 | getInputType(field: string, operator: string): string { 316 | if (this.config.getInputType) { 317 | return this.config.getInputType(field, operator); 318 | } 319 | 320 | if (!this.config.fields[field]) { 321 | throw new Error(`No configuration for field '${field}' could be found! Please add it to config.fields.`); 322 | } 323 | 324 | const type = this.config.fields[field].type; 325 | switch (operator) { 326 | case 'is null': 327 | case 'is not null': 328 | return null; // No displayed component 329 | case 'in': 330 | case 'not in': 331 | return type === 'category' || type === 'boolean' ? 'multiselect' : type; 332 | default: 333 | return type; 334 | } 335 | } 336 | 337 | getOptions(field: string): Option[] { 338 | if (this.config.getOptions) { 339 | return this.config.getOptions(field); 340 | } 341 | return this.config.fields[field].options || this.defaultEmptyList; 342 | } 343 | 344 | getClassNames(...args): string { 345 | const clsLookup = this.classNames ? this.classNames : this.defaultClassNames; 346 | const classNames = args.map((id) => clsLookup[id] || this.defaultClassNames[id]).filter((c) => !!c); 347 | return classNames.length ? classNames.join(' ') : null; 348 | } 349 | 350 | getDefaultField(entity: Entity): Field { 351 | if (!entity) { 352 | return null; 353 | } else if (entity.defaultField !== undefined) { 354 | return this.getDefaultValue(entity.defaultField); 355 | } else { 356 | const entityFields = this.fields.filter((field) => { 357 | return field && field.entity === entity.value; 358 | }); 359 | if (entityFields && entityFields.length) { 360 | return entityFields[0]; 361 | } else { 362 | console.warn(`No fields found for entity '${entity.name}'. ` + 363 | `A 'defaultOperator' is also not specified on the field config. Operator value will default to null.`); 364 | return null; 365 | } 366 | } 367 | } 368 | 369 | getDefaultOperator(field: Field): string { 370 | if (field && field.defaultOperator !== undefined) { 371 | return this.getDefaultValue(field.defaultOperator); 372 | } else { 373 | const operators = this.getOperators(field.value); 374 | if (operators && operators.length) { 375 | return operators[0]; 376 | } else { 377 | console.warn(`No operators found for field '${field.value}'. ` + 378 | `A 'defaultOperator' is also not specified on the field config. Operator value will default to null.`); 379 | return null; 380 | } 381 | } 382 | } 383 | 384 | addRule(parent?: RuleSet): void { 385 | if (this.disabled) { 386 | return; 387 | } 388 | 389 | parent = parent || this.data; 390 | if (this.config.addRule) { 391 | this.config.addRule(parent); 392 | } else { 393 | const field = this.fields[0]; 394 | parent.rules = parent.rules.concat([{ 395 | field: field.value, 396 | operator: this.getDefaultOperator(field), 397 | value: this.getDefaultValue(field.defaultValue), 398 | entity: field.entity 399 | }]); 400 | } 401 | 402 | this.handleTouched(); 403 | this.handleDataChange(); 404 | } 405 | 406 | removeRule(rule: Rule, parent?: RuleSet): void { 407 | if (this.disabled) { 408 | return; 409 | } 410 | 411 | parent = parent || this.data; 412 | if (this.config.removeRule) { 413 | this.config.removeRule(rule, parent); 414 | } else { 415 | parent.rules = parent.rules.filter((r) => r !== rule); 416 | } 417 | this.inputContextCache.delete(rule); 418 | this.operatorContextCache.delete(rule); 419 | this.fieldContextCache.delete(rule); 420 | this.entityContextCache.delete(rule); 421 | this.removeButtonContextCache.delete(rule); 422 | 423 | this.handleTouched(); 424 | this.handleDataChange(); 425 | } 426 | 427 | addRuleSet(parent?: RuleSet): void { 428 | if (this.disabled) { 429 | return; 430 | } 431 | 432 | parent = parent || this.data; 433 | if (this.config.addRuleSet) { 434 | this.config.addRuleSet(parent); 435 | } else { 436 | parent.rules = parent.rules.concat([{ condition: 'and', rules: [] }]); 437 | } 438 | 439 | this.handleTouched(); 440 | this.handleDataChange(); 441 | } 442 | 443 | removeRuleSet(ruleset?: RuleSet, parent?: RuleSet): void { 444 | if (this.disabled) { 445 | return; 446 | } 447 | 448 | ruleset = ruleset || this.data; 449 | parent = parent || this.parentValue; 450 | if (this.config.removeRuleSet) { 451 | this.config.removeRuleSet(ruleset, parent); 452 | } else { 453 | parent.rules = parent.rules.filter((r) => r !== ruleset); 454 | } 455 | 456 | this.handleTouched(); 457 | this.handleDataChange(); 458 | } 459 | 460 | transitionEnd(e: Event): void { 461 | this.treeContainer.nativeElement.style.maxHeight = null; 462 | } 463 | 464 | toggleCollapse(): void { 465 | this.computedTreeContainerHeight(); 466 | setTimeout(() => { 467 | this.data.collapsed = !this.data.collapsed; 468 | }, 100); 469 | } 470 | 471 | computedTreeContainerHeight(): void { 472 | const nativeElement: HTMLElement = this.treeContainer.nativeElement; 473 | if (nativeElement && nativeElement.firstElementChild) { 474 | nativeElement.style.maxHeight = (nativeElement.firstElementChild.clientHeight + 8) + 'px'; 475 | } 476 | } 477 | 478 | changeCondition(value: string): void { 479 | if (this.disabled) { 480 | return; 481 | } 482 | 483 | this.data.condition = value; 484 | this.handleTouched(); 485 | this.handleDataChange(); 486 | } 487 | 488 | changeOperator(rule: Rule): void { 489 | if (this.disabled) { 490 | return; 491 | } 492 | 493 | if (this.config.coerceValueForOperator) { 494 | rule.value = this.config.coerceValueForOperator(rule.operator, rule.value, rule); 495 | } else { 496 | rule.value = this.coerceValueForOperator(rule.operator, rule.value, rule); 497 | } 498 | 499 | this.handleTouched(); 500 | this.handleDataChange(); 501 | } 502 | 503 | coerceValueForOperator(operator: string, value: any, rule: Rule): any { 504 | const inputType: string = this.getInputType(rule.field, operator); 505 | if (inputType === 'multiselect' && !Array.isArray(value)) { 506 | return [value]; 507 | } 508 | return value; 509 | } 510 | 511 | changeInput(): void { 512 | if (this.disabled) { 513 | return; 514 | } 515 | 516 | this.handleTouched(); 517 | this.handleDataChange(); 518 | } 519 | 520 | changeField(fieldValue: string, rule: Rule): void { 521 | if (this.disabled) { 522 | return; 523 | } 524 | 525 | const inputContext = this.inputContextCache.get(rule); 526 | const currentField = inputContext && inputContext.field; 527 | 528 | const nextField: Field = this.config.fields[fieldValue]; 529 | 530 | const nextValue = this.calculateFieldChangeValue( 531 | currentField, nextField, rule.value); 532 | 533 | if (nextValue !== undefined) { 534 | rule.value = nextValue; 535 | } else { 536 | delete rule.value; 537 | } 538 | 539 | rule.operator = this.getDefaultOperator(nextField); 540 | 541 | // Create new context objects so templates will automatically update 542 | this.inputContextCache.delete(rule); 543 | this.operatorContextCache.delete(rule); 544 | this.fieldContextCache.delete(rule); 545 | this.entityContextCache.delete(rule); 546 | this.getInputContext(rule); 547 | this.getFieldContext(rule); 548 | this.getOperatorContext(rule); 549 | this.getEntityContext(rule); 550 | 551 | this.handleTouched(); 552 | this.handleDataChange(); 553 | } 554 | 555 | changeEntity(entityValue: string, rule: Rule, index: number, data: RuleSet): void { 556 | if (this.disabled) { 557 | return; 558 | } 559 | let i = index; 560 | let rs = data; 561 | const entity: Entity = this.entities.find((e) => e.value === entityValue); 562 | const defaultField: Field = this.getDefaultField(entity); 563 | if (!rs) { 564 | rs = this.data; 565 | i = rs.rules.findIndex((x) => x === rule); 566 | } 567 | rule.field = defaultField.value; 568 | rs.rules[i] = rule; 569 | if (defaultField) { 570 | this.changeField(defaultField.value, rule); 571 | } else { 572 | this.handleTouched(); 573 | this.handleDataChange(); 574 | } 575 | } 576 | 577 | getDefaultValue(defaultValue: any): any { 578 | switch (typeof defaultValue) { 579 | case 'function': 580 | return defaultValue(); 581 | default: 582 | return defaultValue; 583 | } 584 | } 585 | 586 | getOperatorTemplate(): TemplateRef { 587 | const t = this.parentOperatorTemplate || this.operatorTemplate; 588 | return t ? t.template : null; 589 | } 590 | 591 | getFieldTemplate(): TemplateRef { 592 | const t = this.parentFieldTemplate || this.fieldTemplate; 593 | return t ? t.template : null; 594 | } 595 | 596 | getEntityTemplate(): TemplateRef { 597 | const t = this.parentEntityTemplate || this.entityTemplate; 598 | return t ? t.template : null; 599 | } 600 | 601 | getArrowIconTemplate(): TemplateRef { 602 | const t = this.parentArrowIconTemplate || this.arrowIconTemplate; 603 | return t ? t.template : null; 604 | } 605 | 606 | getButtonGroupTemplate(): TemplateRef { 607 | const t = this.parentButtonGroupTemplate || this.buttonGroupTemplate; 608 | return t ? t.template : null; 609 | } 610 | 611 | getSwitchGroupTemplate(): TemplateRef { 612 | const t = this.parentSwitchGroupTemplate || this.switchGroupTemplate; 613 | return t ? t.template : null; 614 | } 615 | 616 | getRemoveButtonTemplate(): TemplateRef { 617 | const t = this.parentRemoveButtonTemplate || this.removeButtonTemplate; 618 | return t ? t.template : null; 619 | } 620 | 621 | getEmptyWarningTemplate(): TemplateRef { 622 | const t = this.parentEmptyWarningTemplate || this.emptyWarningTemplate; 623 | return t ? t.template : null; 624 | } 625 | 626 | getQueryItemClassName(local: LocalRuleMeta): string { 627 | let cls = this.getClassNames('row', 'connector', 'transition'); 628 | cls += ' ' + this.getClassNames(local.ruleset ? 'ruleSet' : 'rule'); 629 | if (local.invalid) { 630 | cls += ' ' + this.getClassNames('invalidRuleSet'); 631 | } 632 | return cls; 633 | } 634 | 635 | getButtonGroupContext(): ButtonGroupContext { 636 | if (!this.buttonGroupContext) { 637 | this.buttonGroupContext = { 638 | addRule: this.addRule.bind(this), 639 | addRuleSet: this.allowRuleset && this.addRuleSet.bind(this), 640 | removeRuleSet: this.allowRuleset && this.parentValue && this.removeRuleSet.bind(this), 641 | getDisabledState: this.getDisabledState, 642 | $implicit: this.data 643 | }; 644 | } 645 | return this.buttonGroupContext; 646 | } 647 | 648 | getRemoveButtonContext(rule: Rule): RemoveButtonContext { 649 | if (!this.removeButtonContextCache.has(rule)) { 650 | this.removeButtonContextCache.set(rule, { 651 | removeRule: this.removeRule.bind(this), 652 | getDisabledState: this.getDisabledState, 653 | $implicit: rule 654 | }); 655 | } 656 | return this.removeButtonContextCache.get(rule); 657 | } 658 | 659 | getFieldContext(rule: Rule): FieldContext { 660 | if (!this.fieldContextCache.has(rule)) { 661 | this.fieldContextCache.set(rule, { 662 | onChange: this.changeField.bind(this), 663 | getFields: this.getFields.bind(this), 664 | getDisabledState: this.getDisabledState, 665 | fields: this.fields, 666 | $implicit: rule 667 | }); 668 | } 669 | return this.fieldContextCache.get(rule); 670 | } 671 | 672 | getEntityContext(rule: Rule): EntityContext { 673 | if (!this.entityContextCache.has(rule)) { 674 | this.entityContextCache.set(rule, { 675 | onChange: this.changeEntity.bind(this), 676 | getDisabledState: this.getDisabledState, 677 | entities: this.entities, 678 | $implicit: rule 679 | }); 680 | } 681 | return this.entityContextCache.get(rule); 682 | } 683 | 684 | getSwitchGroupContext(): SwitchGroupContext { 685 | return { 686 | onChange: this.changeCondition.bind(this), 687 | getDisabledState: this.getDisabledState, 688 | $implicit: this.data 689 | }; 690 | } 691 | 692 | getArrowIconContext(): ArrowIconContext { 693 | return { 694 | getDisabledState: this.getDisabledState, 695 | $implicit: this.data 696 | }; 697 | } 698 | 699 | getEmptyWarningContext(): EmptyWarningContext { 700 | return { 701 | getDisabledState: this.getDisabledState, 702 | message: this.emptyMessage, 703 | $implicit: this.data 704 | }; 705 | } 706 | 707 | getOperatorContext(rule: Rule): OperatorContext { 708 | if (!this.operatorContextCache.has(rule)) { 709 | this.operatorContextCache.set(rule, { 710 | onChange: this.changeOperator.bind(this), 711 | getDisabledState: this.getDisabledState, 712 | operators: this.getOperators(rule.field), 713 | $implicit: rule 714 | }); 715 | } 716 | return this.operatorContextCache.get(rule); 717 | } 718 | 719 | getInputContext(rule: Rule): InputContext { 720 | if (!this.inputContextCache.has(rule)) { 721 | this.inputContextCache.set(rule, { 722 | onChange: this.changeInput.bind(this), 723 | getDisabledState: this.getDisabledState, 724 | options: this.getOptions(rule.field), 725 | field: this.config.fields[rule.field], 726 | $implicit: rule 727 | }); 728 | } 729 | return this.inputContextCache.get(rule); 730 | } 731 | 732 | private calculateFieldChangeValue( 733 | currentField: Field, 734 | nextField: Field, 735 | currentValue: any 736 | ): any { 737 | 738 | if (this.config.calculateFieldChangeValue != null) { 739 | return this.config.calculateFieldChangeValue( 740 | currentField, nextField, currentValue); 741 | } 742 | 743 | const canKeepValue = () => { 744 | if (currentField == null || nextField == null) { 745 | return false; 746 | } 747 | return currentField.type === nextField.type 748 | && this.defaultPersistValueTypes.indexOf(currentField.type) !== -1; 749 | }; 750 | 751 | if (this.persistValueOnFieldChange && canKeepValue()) { 752 | return currentValue; 753 | } 754 | 755 | if (nextField && nextField.defaultValue !== undefined) { 756 | return this.getDefaultValue(nextField.defaultValue); 757 | } 758 | 759 | return undefined; 760 | } 761 | 762 | private checkEmptyRuleInRuleset(ruleset: RuleSet): boolean { 763 | if (!ruleset || !ruleset.rules || ruleset.rules.length === 0) { 764 | return true; 765 | } else { 766 | return ruleset.rules.some((item: RuleSet) => { 767 | if (item.rules) { 768 | return this.checkEmptyRuleInRuleset(item); 769 | } else { 770 | return false; 771 | } 772 | }); 773 | } 774 | } 775 | 776 | private validateRulesInRuleset(ruleset: RuleSet, errorStore: any[]) { 777 | if (ruleset && ruleset.rules && ruleset.rules.length > 0) { 778 | ruleset.rules.forEach((item) => { 779 | if ((item as RuleSet).rules) { 780 | return this.validateRulesInRuleset(item as RuleSet, errorStore); 781 | } else if ((item as Rule).field) { 782 | const field = this.config.fields[(item as Rule).field]; 783 | if (field && field.validator && field.validator.apply) { 784 | const error = field.validator(item as Rule, ruleset); 785 | if (error != null) { 786 | errorStore.push(error); 787 | } 788 | } 789 | } 790 | }); 791 | } 792 | } 793 | 794 | private handleDataChange(): void { 795 | this.changeDetectorRef.markForCheck(); 796 | if (this.onChangeCallback) { 797 | this.onChangeCallback(); 798 | } 799 | if (this.parentChangeCallback) { 800 | this.parentChangeCallback(); 801 | } 802 | } 803 | 804 | private handleTouched(): void { 805 | if (this.onTouchedCallback) { 806 | this.onTouchedCallback(); 807 | } 808 | if (this.parentTouchedCallback) { 809 | this.parentTouchedCallback(); 810 | } 811 | } 812 | } 813 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-builder.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ValidationErrors } from '@angular/forms'; 2 | 3 | export interface RuleSet { 4 | condition: string; 5 | rules: Array; 6 | collapsed?: boolean; 7 | isChild?: boolean; 8 | } 9 | 10 | export interface Rule { 11 | field: string; 12 | value?: any; 13 | operator?: string; 14 | entity?: string; 15 | } 16 | 17 | export interface Option { 18 | name: string; 19 | value: any; 20 | } 21 | 22 | export interface FieldMap { 23 | [key: string]: Field; 24 | } 25 | 26 | export interface Field { 27 | name: string; 28 | value?: string; 29 | type: string; 30 | nullable?: boolean; 31 | options?: Option[]; 32 | operators?: string[]; 33 | defaultValue?: any; 34 | defaultOperator?: any; 35 | entity?: string; 36 | validator?: (rule: Rule, parent: RuleSet) => any | null; 37 | } 38 | 39 | export interface LocalRuleMeta { 40 | ruleset: boolean; 41 | invalid: boolean; 42 | } 43 | 44 | export interface EntityMap { 45 | [key: string]: Entity; 46 | } 47 | 48 | export interface Entity { 49 | name: string; 50 | value?: string; 51 | defaultField?: any; 52 | } 53 | 54 | export interface QueryBuilderClassNames { 55 | arrowIconButton?: string; 56 | arrowIcon?: string; 57 | removeIcon?: string; 58 | addIcon?: string; 59 | button?: string; 60 | buttonGroup?: string; 61 | removeButton?: string; 62 | removeButtonSize?: string; 63 | switchRow?: string; 64 | switchGroup?: string; 65 | switchLabel?: string; 66 | switchRadio?: string; 67 | switchControl?: string; 68 | rightAlign?: string; 69 | transition?: string; 70 | collapsed?: string; 71 | treeContainer?: string; 72 | tree?: string; 73 | row?: string; 74 | connector?: string; 75 | rule?: string; 76 | ruleSet?: string; 77 | invalidRuleSet?: string; 78 | emptyWarning?: string; 79 | fieldControl?: string; 80 | fieldControlSize?: string; 81 | entityControl?: string; 82 | entityControlSize?: string; 83 | operatorControl?: string; 84 | operatorControlSize?: string; 85 | inputControl?: string; 86 | inputControlSize?: string; 87 | } 88 | 89 | export interface QueryBuilderConfig { 90 | fields: FieldMap; 91 | entities?: EntityMap; 92 | allowEmptyRulesets?: boolean; 93 | getOperators?: (fieldName: string, field: Field) => string[]; 94 | getInputType?: (field: string, operator: string) => string; 95 | getOptions?: (field: string) => Option[]; 96 | addRuleSet?: (parent: RuleSet) => void; 97 | addRule?: (parent: RuleSet) => void; 98 | removeRuleSet?: (ruleset: RuleSet, parent: RuleSet) => void; 99 | removeRule?: (rule: Rule, parent: RuleSet) => void; 100 | coerceValueForOperator?: (operator: string, value: any, rule: Rule) => any; 101 | calculateFieldChangeValue?: (currentField: Field, 102 | nextField: Field, 103 | currentValue: any) => any; 104 | } 105 | 106 | export interface SwitchGroupContext { 107 | onChange: (conditionValue: string) => void; 108 | getDisabledState: () => boolean; 109 | $implicit: RuleSet; 110 | } 111 | 112 | export interface EmptyWarningContext { 113 | getDisabledState: () => boolean; 114 | message: string; 115 | $implicit: RuleSet; 116 | } 117 | 118 | export interface ArrowIconContext { 119 | getDisabledState: () => boolean; 120 | $implicit: RuleSet; 121 | } 122 | 123 | export interface EntityContext { 124 | onChange: (entityValue: string, rule: Rule) => void; 125 | getDisabledState: () => boolean; 126 | entities: Entity[]; 127 | $implicit: Rule; 128 | } 129 | 130 | export interface FieldContext { 131 | onChange: (fieldValue: string, rule: Rule) => void; 132 | getFields: (entityName: string) => void; 133 | getDisabledState: () => boolean; 134 | fields: Field[]; 135 | $implicit: Rule; 136 | } 137 | 138 | export interface OperatorContext { 139 | onChange: () => void; 140 | getDisabledState: () => boolean; 141 | operators: string[]; 142 | $implicit: Rule; 143 | } 144 | 145 | export interface InputContext { 146 | onChange: () => void; 147 | getDisabledState: () => boolean; 148 | options: Option[]; 149 | field: Field; 150 | $implicit: Rule; 151 | } 152 | 153 | export interface ButtonGroupContext { 154 | addRule: () => void; 155 | addRuleSet: () => void; 156 | removeRuleSet: () => void; 157 | getDisabledState: () => boolean; 158 | $implicit: RuleSet; 159 | } 160 | 161 | export interface RemoveButtonContext { 162 | removeRule: (rule: Rule) => void; 163 | getDisabledState: () => boolean; 164 | $implicit: Rule; 165 | } 166 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-button-group.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryButtonGroup]'}) 4 | export class QueryButtonGroupDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-empty-warning.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryEmptyWarning]'}) 4 | export class QueryEmptyWarningDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-entity.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryEntity]'}) 4 | export class QueryEntityDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-field.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryField]'}) 4 | export class QueryFieldDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-input.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryInput]'}) 4 | export class QueryInputDirective { 5 | /** Unique name for query input type. */ 6 | @Input() 7 | get queryInputType(): string { return this._type; } 8 | set queryInputType(value: string) { 9 | // If the directive is set without a type (updated programatically), then this setter will 10 | // trigger with an empty string and should not overwrite the programatically set value. 11 | if (!value) { return; } 12 | this._type = value; 13 | } 14 | private _type: string; 15 | 16 | constructor(public template: TemplateRef) {} 17 | } 18 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-operator.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryOperator]'}) 4 | export class QueryOperatorDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-remove-button.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[queryRemoveButton]'}) 4 | export class QueryRemoveButtonDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/lib/query-builder/query-switch-group.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({selector: '[querySwitchGroup]'}) 4 | export class QuerySwitchGroupDirective { 5 | constructor(public template: TemplateRef) {} 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of angular2-query-builder 3 | */ 4 | 5 | export * from './lib/query-builder/query-builder.interfaces'; 6 | 7 | export * from './lib/query-builder/query-builder.component'; 8 | 9 | export * from './lib/query-builder/query-button-group.directive'; 10 | export * from './lib/query-builder/query-entity.directive'; 11 | export * from './lib/query-builder/query-field.directive'; 12 | export * from './lib/query-builder/query-input.directive'; 13 | export * from './lib/query-builder/query-operator.directive'; 14 | export * from './lib/query-builder/query-switch-group.directive'; 15 | export * from './lib/query-builder/query-remove-button.directive'; 16 | export * from './lib/query-builder/query-empty-warning.directive'; 17 | export * from './lib/query-builder/query-arrow-icon.directive'; 18 | 19 | export * from './lib/angular2-query-builder.module'; 20 | -------------------------------------------------------------------------------- /projects/angular2-query-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 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting() 17 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declaration": true, 6 | "inlineSources": true, 7 | "types": [], 8 | "target": "es5", 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "removeComments": false, 13 | "noImplicitAny": false, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "lib": [ 17 | "es5", 18 | "es6", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "skipTemplateCodegen": true, 24 | "strictMetadataEmit": true, 25 | "fullTemplateTypeCheck": true, 26 | "strictInjectionParameters": true, 27 | "enableResourceInlining": true, 28 | "debug": false, 29 | "skipMetadataEmit": false, 30 | "enableIvy": false 31 | }, 32 | "exclude": [ 33 | "src/test.ts", 34 | "**/*.spec.ts" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /projects/angular2-query-builder/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "angularCompilerOptions": { 4 | "enableIvy": false 5 | } 6 | } -------------------------------------------------------------------------------- /projects/angular2-query-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/angular2-query-builder/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig-aot.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "removeComments": false, 9 | "noImplicitAny": false, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "lib": [ 13 | "es5", 14 | "es6", 15 | "dom" 16 | ], 17 | "allowUnreachableCode": false, 18 | "allowUnusedLabels": false, 19 | "pretty": true, 20 | "stripInternal": true, 21 | "skipLibCheck": true, 22 | "outDir": "dist", 23 | "rootDir": "./tmp/src-inlined" 24 | }, 25 | "files": [ 26 | "./tmp/src-inlined/index.ts" 27 | ], 28 | "angularCompilerOptions": { 29 | "genDir": "dist", 30 | "debug": false, 31 | "skipTemplateCodegen": true, 32 | "skipMetadataEmit": false, 33 | "strictMetadataEmit": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "target": "es5", 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "sourceMap": true, 13 | "removeComments": false, 14 | "noImplicitAny": false, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "lib": [ 18 | "es5", 19 | "es6", 20 | "dom" 21 | ], 22 | 23 | 24 | 25 | "baseUrl": "./", 26 | "downlevelIteration": true, 27 | "importHelpers": true, 28 | "typeRoots": [ 29 | "node_modules/@types" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rulesDirectory": ["node_modules/codelyzer"], 4 | "rules": { 5 | "component-class-suffix": true, 6 | "directive-class-suffix": true, 7 | "interface-name": false, 8 | "max-line-length": [true, 160], 9 | "no-access-missing-member": false, 10 | "member-access": false, 11 | "no-console": [true, "time", "timeEnd", "trace"], 12 | "no-forward-ref": false, 13 | "no-input-rename": true, 14 | "no-output-rename": true, 15 | "no-string-literal": false, 16 | "no-empty": false, 17 | "object-literal-shorthand": false, 18 | "object-literal-sort-keys": false, 19 | "ordered-imports": false, 20 | "quotemark": [true, "single", "avoid-escape"], 21 | "trailing-comma": [false, {"multiline": "always", "singleline": "never"}], 22 | "use-pipe-transform-interface": true, 23 | "variable-name": [true, "allow-leading-underscore", "ban-keywords", "check-format"] 24 | } 25 | } 26 | --------------------------------------------------------------------------------