├── .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 |
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 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------