├── .gitignore
├── README.md
├── gulpfile.js
├── gulpfile.ts
├── package.json
├── resources
└── radio-group-example.png
├── sample
├── sample1-simple-example
│ ├── Car.ts
│ ├── index.html
│ └── main.ts
├── sample2-select-items
│ ├── Car.ts
│ ├── index.html
│ └── main.ts
├── sample3-autocomplete
│ ├── Car.ts
│ ├── index.html
│ └── main.ts
├── sample4-select-dropdown
│ ├── Car.ts
│ ├── index.html
│ └── main.ts
└── sample5-select-tags
│ ├── Car.ts
│ ├── index.html
│ └── main.ts
├── src
├── Autocomplete.ts
├── Checkbox.ts
├── CheckboxGroup.ts
├── CheckboxItem.ts
├── ItemTemplate.ts
├── RadioBox.ts
├── RadioGroup.ts
├── RadioItem.ts
├── SelectControlsOptions.ts
├── SelectDropdown.ts
├── SelectItems.ts
├── SelectTags.ts
├── SelectValidator.ts
├── SelectValueAccessor.ts
├── Utils.ts
├── WidthCalculator.ts
└── index.ts
├── tsconfig.json
├── tslint.json
└── typings.json
/.gitignore:
--------------------------------------------------------------------------------
1 | typings/
2 | build/
3 | node_modules
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > This repository is for demonstration purposes of how it can be implemented in Angular and is not maintaned. Please fork and maintain your own version of this repository.
2 |
3 | # ng2-radio-group
4 |
5 | Ultimate set of select components that you ever need. All with angular2, no jquery.
6 | Checkbox group and radio group control for your angular2 applications. Does not depend of jquery.
7 | Please star a project if you liked it, or create an issue if you have problems with it.
8 |
9 | 
10 |
11 | ## Installation
12 |
13 | 1. Install npm module:
14 |
15 | `npm install ng2-radio-group --save`
16 |
17 | 2. If you are using system.js you may want to add this into `map` and `package` config:
18 |
19 | ```json
20 | {
21 | "map": {
22 | "ng2-radio-group": "node_modules/ng2-radio-group"
23 | },
24 | "packages": {
25 | "ng2-radio-group": { "main": "index.js", "defaultExtension": "js" }
26 | }
27 | }
28 | ```
29 |
30 | ## Simple checkboxes and radio boxes
31 |
32 | If you need a simple checkbox for your variable:
33 |
34 | ```html
35 | remember me?
36 | ```
37 |
38 | * `ngModel` is a model you are trying to change (rememberMe is a boolean variable in your component)
39 | * `value` is a value that should be written to the model when checkbox is checked. Default is **true**.
40 | * `uncheckedValue` is a value that should be written to the model when checkbox is unchecked. Default is **false**.
41 | * `required` you can set it to required, and you can use it with forms
42 |
43 | If your model is an array and you want to push multiple items into it, you can use it with this component:
44 |
45 | ```html
46 | Rating
47 | Date
48 | Watch count
49 | Comment count
50 | ```
51 |
52 | Don't forget to initialize your `orderBy` array.
53 |
54 |
55 | If you need to select only one item from the multiple options, you need a radio boxes:
56 |
57 | ```html
58 | Rating
59 | Date
60 | Watch count
61 | Comment count
62 | ```
63 |
64 | ## Checkbox and Radio groups
65 |
66 | To simplify this selection you can use checkbox and radio groups:
67 |
68 | ```html
69 |
70 | Rating
71 | Date
72 | Watch count
73 | Comment count
74 |
75 |
76 |
77 | Rating
78 | Date
79 | Watch count
80 | Comment count
81 |
82 | ```
83 |
84 | If you want to go deeper and make better (but less customizable) radio groups, then use radio-groups in composition
85 | with radio-items:
86 |
87 | ```html
88 |
89 | Rating
90 | Date
91 | Watch count
92 | Comment count
93 |
94 |
95 |
96 | Rating
97 | Date
98 | Watch count
99 | Comment count
100 |
101 | ```
102 |
103 | Last method allows you to click on labels and you click will treat like you clicked on a checkbox itself.
104 |
105 | ## Select items component
106 |
107 | tbd
108 |
109 | ## Select dropdown component
110 |
111 | tbd
112 |
113 | ## Autocomplete component
114 |
115 | tbd
116 |
117 | ## Select tags component
118 |
119 | tbd
120 |
121 | ## Sample
122 |
123 | Complete example of usage:
124 |
125 | ```typescript
126 | import {Component} from "@angular/core";
127 | import {RADIO_GROUP_DIRECTIVES} from "ng2-radio-group";
128 |
129 | @Component({
130 | selector: "app",
131 | template: `
132 |
133 |
Is something enabled: (non-multiple checkbox)
134 |
135 | isSomethingEnabled value: {{ isSomethingEnabled }}
136 |
137 | Order by: (multiple check boxes)
138 | Rating
139 | Date
140 | Watch count
141 | Comment count
142 |
143 | selected items: {{ order }}
144 |
145 |
146 | Sort by: (simple radio boxes)
147 | Rating
148 | Date
149 | Watch count
150 | Comment count
151 |
152 | selected item: {{ sortWithoutGroup }}
153 |
154 |
155 | Sort by: (radio boxes wrapped in the group)
156 |
157 | Rating
158 | Date
159 | Watch count
160 | Comment count
161 |
162 |
163 | selected item: {{ sortBy }}
164 |
165 |
166 | Order by: (check boxes wrapped in the group)
167 |
168 | Rating
169 | Date
170 | Watch count
171 | Comment count
172 |
173 |
174 | selected items: {{ order }}
175 |
176 |
177 | Sort by: (check boxes in group, less flexible, but simpler and the whole component is clickable)
178 |
179 | Rating
180 | Date
181 | Watch count
182 | Comment count
183 |
184 |
185 | selected item: {{ sortBy }}
186 |
187 |
188 | Order by: (radio boxes in group, less flexible, but simpler and the whole component is clickable)
189 |
190 | Rating
191 | Date
192 | Watch count
193 | Comment count
194 |
195 |
196 | selected items: {{ order }}
197 |
198 |
199 | Example with form:
200 |
201 |
223 |
224 | `,
225 | directives: [RADIO_GROUP_DIRECTIVES]
226 | })
227 | export class App {
228 |
229 | rememberMe: boolean = false;
230 | sortBy: string = "date";
231 | orderBy: string[] = ["rating", "comments"];
232 |
233 | }
234 | ```
235 |
236 | Take a look on samples in [./sample](https://github.com/pleerock/ng2-radio-group/tree/master/sample) for more examples of
237 | usages.
238 |
239 | ## Release notes
240 |
241 | **0.0.5**
242 |
243 | * `[(model)]` now should be `[(ngModel)]`
244 | * component now can be used with forms and validation can be applied (like `required`)
245 | * `check-box` and `radio-box` has been removed, now you simply need to make your input as checkbox or radio:
246 | ` ` or ` `
247 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | eval(require("typescript").transpile(require("fs").readFileSync("./gulpfile.ts").toString()));
--------------------------------------------------------------------------------
/gulpfile.ts:
--------------------------------------------------------------------------------
1 | import {Gulpclass, Task, SequenceTask} from "gulpclass/Decorators";
2 |
3 | const gulp = require("gulp");
4 | const del = require("del");
5 | const shell = require("gulp-shell");
6 | const replace = require("gulp-replace");
7 | const mocha = require("gulp-mocha");
8 | const chai = require("chai");
9 | const tslint = require("gulp-tslint");
10 | const stylish = require("tslint-stylish");
11 |
12 | @Gulpclass()
13 | export class Gulpfile {
14 |
15 | // -------------------------------------------------------------------------
16 | // General tasks
17 | // -------------------------------------------------------------------------
18 |
19 | /**
20 | * Cleans build folder.
21 | */
22 | @Task()
23 | clean(cb: Function) {
24 | return del(["./build/**"], cb);
25 | }
26 |
27 | /**
28 | * Runs typescript files compilation.
29 | */
30 | @Task()
31 | compile() {
32 | return gulp.src("*.js", { read: false })
33 | .pipe(shell(["tsc"]));
34 | }
35 |
36 | // -------------------------------------------------------------------------
37 | // Packaging and Publishing tasks
38 | // -------------------------------------------------------------------------
39 |
40 | /**
41 | * Publishes a package to npm from ./build/package directory.
42 | */
43 | @Task()
44 | npmPublish() {
45 | return gulp.src("*.js", { read: false })
46 | .pipe(shell([
47 | "cd ./build/package && npm publish"
48 | ]));
49 | }
50 |
51 | /**
52 | * Copies all files that will be in a package.
53 | */
54 | @Task()
55 | packageFiles() {
56 | return gulp.src("./build/es5/src/**/*")
57 | .pipe(gulp.dest("./build/package"));
58 | }
59 |
60 | /**
61 | * Change the "private" state of the packaged package.json file to public.
62 | */
63 | @Task()
64 | packagePreparePackageFile() {
65 | return gulp.src("./package.json")
66 | .pipe(replace("\"private\": true,", "\"private\": false,"))
67 | .pipe(gulp.dest("./build/package"));
68 | }
69 |
70 | /**
71 | * This task will replace all typescript code blocks in the README (since npm does not support typescript syntax
72 | * highlighting) and copy this README file into the package folder.
73 | */
74 | @Task()
75 | packageReadmeFile() {
76 | return gulp.src("./README.md")
77 | .pipe(replace(/```typescript([\s\S]*?)```/g, "```javascript$1```"))
78 | .pipe(gulp.dest("./build/package"));
79 | }
80 |
81 | /**
82 | * This task will copy typings.json file to the build package.
83 | */
84 | @Task()
85 | copyTypingsFile() {
86 | return gulp.src("./typings.json")
87 | .pipe(gulp.dest("./build/package"));
88 | }
89 |
90 | /**
91 | * Creates a package that can be published to npm.
92 | */
93 | @SequenceTask()
94 | package() {
95 | return [
96 | "clean",
97 | "compile",
98 | ["packageFiles", "packagePreparePackageFile", "packageReadmeFile", "copyTypingsFile"]
99 | ];
100 | }
101 |
102 | /**
103 | * Creates a package and publishes it to npm.
104 | */
105 | @SequenceTask()
106 | publish() {
107 | return ["package", "npmPublish"];
108 | }
109 |
110 | // -------------------------------------------------------------------------
111 | // Run tests tasks
112 | // -------------------------------------------------------------------------
113 |
114 | /**
115 | * Runs ts linting to validate source code.
116 | */
117 | @Task()
118 | tslint() {
119 | return gulp.src(["./src/**/*.ts", "./test/**/*.ts", "./sample/**/*.ts"])
120 | .pipe(tslint())
121 | .pipe(tslint.report(stylish, {
122 | emitError: true,
123 | sort: true,
124 | bell: true
125 | }));
126 | }
127 |
128 | /**
129 | * Runs unit-tests.
130 | */
131 | @Task()
132 | unit() {
133 | chai.should();
134 | chai.use(require("sinon-chai"));
135 | return gulp.src("./build/es5/test/unit/**/*.js")
136 | .pipe(mocha());
137 | }
138 |
139 | /**
140 | * Compiles the code and runs tests.
141 | */
142 | @SequenceTask()
143 | tests() {
144 | return ["clean", "compile", "tslint", "unit"];
145 | }
146 |
147 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng2-select-controls",
3 | "version": "0.0.1",
4 | "description": "Advanced select, checkbox and radio controls.",
5 | "license": "MIT",
6 | "readmeFilename": "README.md",
7 | "private": true,
8 | "author": {
9 | "name": "Umed Khudoiberdiev",
10 | "email": "pleerock.me@gmail.com"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/pleerock/ng2-select-controls.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/pleerock/ng2-select-controls/issues"
18 | },
19 | "tags": [
20 | "checkbox-group",
21 | "radio-group",
22 | "autocomplete",
23 | "select",
24 | "select2",
25 | "tags",
26 | "tags-input",
27 | "multiselect",
28 | "multiselect-dropdown",
29 | "dropdown",
30 | "dropdown-select",
31 | "angular2",
32 | "typescript",
33 | "angular2-autocomplete",
34 | "angular2-checkbox",
35 | "angular2-checkbox-group",
36 | "angular2-radio",
37 | "angular2-radio-group",
38 | "angular2-tags",
39 | "angular2-tags-input",
40 | "angular2-select",
41 | "angular2-select2",
42 | "angular2-dropdown",
43 | "angular2-multiselect",
44 | "angular2-multiselect-dropdown",
45 | "angular2-dropdown-select"
46 | ],
47 | "peerDependencies": {
48 | "@angular/core": "^2.0.0-rc.1",
49 | "@angular/forms": "^0.2.0"
50 | },
51 | "devDependencies": {
52 | "@angular/common": "^2.0.0-rc.4",
53 | "@angular/compiler": "^2.0.0-rc.4",
54 | "@angular/core": "2.0.0-rc.4",
55 | "@angular/forms": "^0.2.0",
56 | "@angular/http": "^2.0.0-rc.4",
57 | "@angular/platform-browser": "^2.0.0-rc.4",
58 | "@angular/platform-browser-dynamic": "^2.0.0-rc.4",
59 | "bootstrap": "^3.3.6",
60 | "chai": "^3.4.1",
61 | "del": "^2.2.0",
62 | "es6-promise": "^3.0.2",
63 | "es6-shim": "^0.33.3",
64 | "gulp": "^3.9.0",
65 | "gulp-mocha": "^2.2.0",
66 | "gulp-replace": "^0.5.4",
67 | "gulp-shell": "^0.5.1",
68 | "gulp-tslint": "^4.3.1",
69 | "gulpclass": "^0.1.0",
70 | "mocha": "^2.3.2",
71 | "reflect-metadata": "0.1.2",
72 | "rxjs": "5.0.0-beta.6",
73 | "sinon": "^1.17.2",
74 | "sinon-chai": "^2.8.0",
75 | "systemjs": "0.19.27",
76 | "tslint": "^3.3.0",
77 | "tslint-stylish": "^2.1.0-beta",
78 | "typescript": "^1.8.2",
79 | "typings": "^0.6.6",
80 | "zone.js": "^0.6.12"
81 | },
82 | "dependencies": {
83 | "ng2-dropdown": "^0.0.9"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/resources/radio-group-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pleerock/ngx-select-controls/44cedb11e9c029decab7bdfa20732b337cb01d0e/resources/radio-group-example.png
--------------------------------------------------------------------------------
/sample/sample1-simple-example/Car.ts:
--------------------------------------------------------------------------------
1 | export class Car {
2 | constructor(public id: number,
3 | public name: string,
4 | public year: number) {
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/sample1-simple-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | checkbox and radio groups
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
40 |
41 |
42 |
43 |
44 |
45 | Loading...
46 |
47 |
--------------------------------------------------------------------------------
/sample/sample1-simple-example/main.ts:
--------------------------------------------------------------------------------
1 | import {bootstrap} from "@angular/platform-browser-dynamic";
2 | import {Component} from "@angular/core";
3 | import {disableDeprecatedForms, provideForms } from "@angular/forms";
4 | import {SELECT_DIRECTIVES} from "../../src/index";
5 | import {Car} from "./Car";
6 |
7 | @Component({
8 | selector: "app",
9 | template: `
10 |
130 | `,
131 | directives: [SELECT_DIRECTIVES]
132 | })
133 | export class Sample1App {
134 |
135 | isSomethingEnabled: boolean = false;
136 | sortBy: string = "date";
137 | orderBy: string[] = ["rating", "comments"];
138 |
139 | selectedCars1: Car[] = [];
140 | selectedCars2: Car[] = [];
141 | cars: Car[];
142 |
143 | constructor() {
144 | this.cars = [
145 | new Car(1, "BMW", 2000),
146 | new Car(2, "Mercedes", 1999),
147 | new Car(3, "Opel", 2008)
148 | ];
149 | this.selectedCars2 = [
150 | new Car(2, "Mercedes", 1999),
151 | ];
152 | }
153 |
154 | }
155 |
156 | bootstrap(Sample1App, [
157 | disableDeprecatedForms(),
158 | provideForms(),
159 | ]);
--------------------------------------------------------------------------------
/sample/sample2-select-items/Car.ts:
--------------------------------------------------------------------------------
1 | export class Car {
2 | constructor(public id: number,
3 | public name: string,
4 | public year: number) {
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/sample2-select-items/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | select-items
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
40 |
41 |
42 |
66 |
67 |
68 |
69 |
70 | Loading...
71 |
72 |
--------------------------------------------------------------------------------
/sample/sample2-select-items/main.ts:
--------------------------------------------------------------------------------
1 | import {bootstrap} from "@angular/platform-browser-dynamic";
2 | import {Component} from "@angular/core";
3 | import {SELECT_DIRECTIVES} from "../../src/index";
4 | import {Car} from "./Car";
5 | import {ItemTemplate} from "../../src/ItemTemplate";
6 | import {provideForms, disableDeprecatedForms} from "@angular/forms";
7 |
8 | @Component({
9 | selector: "app",
10 | template: `
11 |
12 |
13 |
Simple select items:
14 |
17 |
18 |
19 |
model:
20 |
{{ selectedCars | json }}
21 |
22 |
Select list with search
23 |
28 |
29 |
model:
30 |
{{ selectedCars1 | json }}
31 |
32 |
Select list with ordering
33 |
37 |
38 |
model:
39 |
{{ selectedCars2 | json }}
40 |
41 |
Select list with descendant ordering
42 |
47 |
48 |
model:
49 |
{{ selectedCars3 | json }}
50 |
51 |
52 |
Select items with select all option
53 |
58 |
59 |
model:
60 |
{{ selectedCars4 | json }}
61 |
62 |
Select items with limited number of shown items:
63 |
67 |
68 |
model:
69 |
{{ selectedCars5 | json }}
70 |
71 |
Select items with limited number of shown items, with more button:
72 |
77 |
78 |
model:
79 |
{{ selectedCars6 | json }}
80 |
81 |
Select items with limited number of shown items, with more & hide button:
82 |
88 |
89 |
model:
90 |
{{ selectedCars7 | json }}
91 |
92 |
Select items where items can be removed:
93 |
97 |
98 |
model:
99 |
{{ selectedCars8 | json }}
100 |
101 |
Select items with no controls:
102 |
106 |
107 |
model:
108 |
{{ selectedCars9 | json }}
109 |
110 |
Select items where where selected items are not showing after they are selected:
111 |
115 |
116 |
model:
117 |
{{ selectedCars10 | json }}
118 |
119 |
Select items with maximal number of allowed selected items:
120 |
124 |
125 |
model:
126 |
{{ selectedCars11 | json }}
127 |
128 |
Select items with minimal number of allowed selected items:
129 |
133 |
134 |
model:
135 |
{{ selectedCars12 | json }}
136 |
137 |
Select items with track by, to track by another model:
138 |
142 |
143 |
model:
144 |
{{ secondSelectedCars | json }}
145 |
146 |
Select items with value by, to get more specific values:
147 |
151 |
152 |
model:
153 |
{{ selectedCarNames | json }}
154 |
155 |
Select items with grouping:
156 |
161 |
162 |
model:
163 |
{{ selectedCars13 | json }}
164 |
165 |
Select items with grouping and select-alls in groups:
166 |
172 |
173 |
model:
174 |
{{ selectedCars14 | json }}
175 |
176 |
Select items with grouping and select-alls in groups, but without a checkbox:
177 |
184 |
185 |
model:
186 |
{{ selectedCars15 | json }}
187 |
188 |
Select items with custom item template:
189 |
193 |
194 |
195 | #{{ item.id }} {{ item.name }} ({{ item.year }})
196 |
197 |
198 |
199 |
model:
200 |
{{ selectedCars17 | json }}
201 |
202 |
Select items with custom item template and hidden controls:
203 |
208 |
209 |
210 | #{{ item.id }} {{ item.name }} ({{ item.year }})
211 |
212 |
213 |
214 |
model:
215 |
{{ selectedCars17 | json }}
216 |
217 |
All-in-one select items:
218 |
234 |
235 |
model:
236 |
{{ selectedCars18 | json }}
237 |
238 |
239 |
240 |
241 | Single item:
242 |
243 |
244 |
Simple select items:
245 |
248 |
249 |
250 |
model:
251 |
{{ selectedCar | json }}
252 |
253 |
Select list with search
254 |
259 |
260 |
model:
261 |
{{ selectedCar | json }}
262 |
263 |
Select list with ordering
264 |
268 |
269 |
model:
270 |
{{ selectedCar | json }}
271 |
272 |
Select list with descendant ordering
273 |
278 |
279 |
model:
280 |
{{ selectedCar | json }}
281 |
282 |
283 |
Select items with nothing is selected
284 |
289 |
290 |
model:
291 |
{{ selectedCar | json }}
292 |
293 |
Select items with limited number of shown items:
294 |
298 |
299 |
model:
300 |
{{ selectedCar | json }}
301 |
302 |
Select items with limited number of shown items, with more button:
303 |
308 |
309 |
model:
310 |
{{ selectedCar | json }}
311 |
312 |
Select items with limited number of shown items, with more & hide button:
313 |
319 |
320 |
model:
321 |
{{ selectedCar | json }}
322 |
323 |
Select items where items can be removed:
324 |
328 |
329 |
model:
330 |
{{ selectedCar | json }}
331 |
332 |
Select items where with no controls:
333 |
337 |
338 |
model:
339 |
{{ selectedCar | json }}
340 |
341 |
Select items where where selected items are not showing after they are selected:
342 |
346 |
347 |
model:
348 |
{{ selectedCar | json }}
349 |
350 |
Select items with maximal number of allowed selected items:
351 |
355 |
356 |
model:
357 |
{{ selectedCar | json }}
358 |
359 |
Select items with minimal number of allowed selected items:
360 |
364 |
365 |
model:
366 |
{{ selectedCar | json }}
367 |
368 |
Select items with track by, to track by another model:
369 |
373 |
374 |
model:
375 |
{{ secondSelectedCar | json }}
376 |
377 |
Select items with value by, to get more specific values:
378 |
382 |
383 |
model:
384 |
{{ selectedCarName | json }}
385 |
386 |
Select items with explicitly set multiple option:
387 |
391 |
392 |
model:
393 |
{{ allNewSelectedCars | json }}
394 |
395 |
Select items single with grouping:
396 |
401 |
402 |
model:
403 |
{{ selectedCar | json }}
404 |
405 |
All-in-one select items:
406 |
423 |
424 |
model:
425 |
{{ selectedCar | json }}
426 |
427 |
428 |
429 | `,
430 | directives: [SELECT_DIRECTIVES]
431 | })
432 | export class Sample1App {
433 |
434 | cars: Car[] = [
435 | new Car(1, "BMW", 2000),
436 | new Car(2, "Mercedes", 1999),
437 | new Car(3, "Opel", 2008),
438 | new Car(4, "Porshe", 1940),
439 | new Car(5, "Ferrari", 2000),
440 | new Car(6, "Toyota", 2008),
441 | new Car(7, "Nissan", 1940)
442 | ];
443 |
444 | selectedCars: Car[] = [];
445 | selectedCars1: Car[] = [];
446 | selectedCars2: Car[] = [];
447 | selectedCars3: Car[] = [];
448 | selectedCars4: Car[] = [];
449 | selectedCars5: Car[] = [];
450 | selectedCars6: Car[] = [];
451 | selectedCars7: Car[] = [];
452 | selectedCars8: Car[] = [];
453 | selectedCars9: Car[] = [];
454 | selectedCars10: Car[] = [];
455 | selectedCars11: Car[] = [];
456 | selectedCars12: Car[] = [];
457 | selectedCars13: Car[] = [];
458 | selectedCars14: Car[] = [];
459 | selectedCars15: Car[] = [];
460 | selectedCars16: Car[] = [];
461 | selectedCars17: Car[] = [];
462 | selectedCars18: Car[] = [];
463 | selectedCars19: Car[] = [];
464 | secondSelectedCars: Car[] = [
465 | new Car(2, "Mercedes", 1999)
466 | ];
467 | selectedCarNames: string[] = [];
468 |
469 | selectedCar: Car;
470 | secondSelectedCar: Car = new Car(2, "Mercedes", 1999);
471 | selectedCarName: string;
472 |
473 | constructor() {
474 | }
475 |
476 | }
477 |
478 | bootstrap(Sample1App, [
479 | disableDeprecatedForms(),
480 | provideForms(),
481 | ]);
--------------------------------------------------------------------------------
/sample/sample3-autocomplete/Car.ts:
--------------------------------------------------------------------------------
1 | export class Car {
2 | constructor(public id: number,
3 | public name: string,
4 | public year: number) {
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/sample3-autocomplete/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | autocomplete
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
40 |
41 |
42 |
66 |
67 |
68 |
69 |
70 | Loading...
71 |
72 |
--------------------------------------------------------------------------------
/sample/sample3-autocomplete/main.ts:
--------------------------------------------------------------------------------
1 | import {bootstrap} from "@angular/platform-browser-dynamic";
2 | import {Component} from "@angular/core";
3 | import {SELECT_DIRECTIVES} from "../../src/index";
4 | import {Car} from "./Car";
5 | import {HTTP_PROVIDERS, Http} from "@angular/http";
6 | import {Observable} from "rxjs/Rx";
7 | import {disableDeprecatedForms, provideForms} from "@angular/forms";
8 |
9 | @Component({
10 | selector: "app",
11 | template: `
12 |
13 |
14 |
Autocomplete multiple:
15 |
19 |
20 |
Selected items:
21 |
28 |
29 |
model:
30 |
{{ selectedCars1 | json }}
31 |
32 |
Autocomplete multiple with persist and custom button label
33 |
39 |
40 |
model:
41 |
{{ selectedCars2 | json }}
42 |
43 |
Autocomplete single:
44 |
48 |
49 |
model:
50 |
{{ selectedCar1 | json }}
51 |
52 |
Autocomplete single with persist:
53 |
58 |
59 |
model:
60 |
{{ selectedCar2 | json }}
61 |
62 |
Autocomplete with predefined model:
63 |
68 |
69 |
model:
70 |
{{ newSelectedCar | json }}
71 |
reset model
72 |
73 |
Autocomplete multiple disabled:
74 |
80 |
81 |
model:
82 |
{{ selectedCars3 | json }}
83 |
84 |
Autocomplete explicit multiple:
85 |
91 |
92 |
model:
93 |
{{ allSelectedCars | json }}
94 |
95 |
Autocomplete with minimal number of characters to send a request:
96 |
101 |
102 |
model:
103 |
{{ selectedCars4 | json }}
104 |
105 |
Autocomplete with specific values selected:
106 |
111 |
112 |
model:
113 |
{{ selectedCars5 | json }}
114 |
115 |
Autocomplete with ordering enabled:
116 |
121 |
122 |
model:
123 |
{{ selectedCars6 | json }}
124 |
125 |
Autocomplete with descendant ordering enabled:
126 |
132 |
133 |
model:
134 |
{{ selectedCars7 | json }}
135 |
136 |
Autocomplete with limit:
137 |
142 |
143 |
model:
144 |
{{ selectedCars8 | json }}
145 |
146 |
Autocomplete with maximal allowed to selection:
147 |
152 |
153 |
model:
154 |
{{ selectedCars9 | json }}
155 |
156 |
Autocomplete with custom item constructor function:
157 |
163 |
164 |
model:
165 |
{{ selectedCars10 | json }}
166 |
167 |
Autocomplete with custom template:
168 |
175 |
176 |
177 |
178 | {{ item.name }} ({{ item.owner.login }})
179 |
180 |
181 |
182 |
183 |
184 |
model:
185 |
{{ selectedCars11 | json }}
186 |
187 |
188 | `,
189 | directives: [SELECT_DIRECTIVES]
190 | })
191 | export class Sample1App {
192 |
193 | cars: Car[] = [
194 | new Car(1, "BMW", 2000),
195 | new Car(2, "Mercedes", 1999),
196 | new Car(3, "Opel", 2008),
197 | new Car(4, "Porshe", 1940),
198 | new Car(4, "Ferrari", 2000)
199 | ];
200 | selectedCars1: Car[] = [];
201 | selectedCars2: Car[] = [];
202 | selectedCars3: Car[] = [];
203 | selectedCars4: Car[] = [];
204 | selectedCars5: string[] = [];
205 | selectedCars6: Car[] = [];
206 | selectedCars7: Car[] = [];
207 | selectedCars8: Car[] = [];
208 | selectedCars9: Car[] = [];
209 | selectedCars10: Car[] = [];
210 | selectedCars11: Car[] = [];
211 | selectedCar: Car;
212 | selectedCar2: Car;
213 | newSelectedCar: Car = new Car(1, "BMW", 2000);
214 |
215 | loader = (term: string) => {
216 | return this.http
217 | .get("https://api.github.com/search/repositories?q=" + term)
218 | .map(res => res.json().items) as Observable;
219 | };
220 |
221 | itemConstructor = (term: string) => {
222 | return new Car(0, term, 2016);
223 | };
224 |
225 | constructor(private http: Http) {
226 | }
227 |
228 | resetModel() {
229 | this.newSelectedCar = new Car(1, "BMW", 2000);
230 | }
231 |
232 | }
233 |
234 | bootstrap(Sample1App, [
235 | HTTP_PROVIDERS,
236 | disableDeprecatedForms(),
237 | provideForms(),
238 | ]);
--------------------------------------------------------------------------------
/sample/sample4-select-dropdown/Car.ts:
--------------------------------------------------------------------------------
1 | export class Car {
2 | constructor(public id: number,
3 | public name: string,
4 | public year: number) {
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/sample4-select-dropdown/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | select-dropdown
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
40 |
41 |
42 |
66 |
67 |
68 |
69 |
70 | Loading...
71 |
72 |
--------------------------------------------------------------------------------
/sample/sample4-select-dropdown/main.ts:
--------------------------------------------------------------------------------
1 | import {bootstrap} from "@angular/platform-browser-dynamic";
2 | import {Component} from "@angular/core";
3 | import {SELECT_DIRECTIVES} from "../../src/index";
4 | import {Car} from "./Car";
5 | import {HTTP_PROVIDERS, Http} from "@angular/http";
6 | import {disableDeprecatedForms, provideForms} from "@angular/forms";
7 |
8 | @Component({
9 | selector: "app",
10 | template: `
11 |
12 |
13 |
Select Dropdown multiple:
14 |
18 |
19 |
model:
20 |
{{ selectedCars1 | json }}
21 |
22 |
Select Dropdown single select:
23 |
27 |
28 |
model:
29 |
{{ selectedCar1 | json }}
30 |
31 |
Select Dropdown multiselect with explicit multiple:
32 |
37 |
38 |
model:
39 |
{{ anotherSelectedCars | json }}
40 |
41 |
Select Dropdown multiple with custom label:
42 |
47 |
48 |
model:
49 |
{{ selectedCars2 | json }}
50 |
51 |
Select Dropdown single select with no selection option:
52 |
57 |
58 |
model:
59 |
{{ selectedCar2 | json }}
60 |
61 |
Select Dropdown multiple with select all option:
62 |
67 |
68 |
model:
69 |
{{ selectedCars3 | json }}
70 |
71 |
Select Dropdown multiple with limit:
72 |
77 |
78 |
model:
79 |
{{ selectedCars4 | json }}
80 |
81 |
Select Dropdown disabled:
82 |
87 |
88 |
model:
89 |
{{ selectedCars5 | json }}
90 |
91 |
Select Dropdown ordered:
92 |
97 |
98 |
model:
99 |
{{ selectedCars6 | json }}
100 |
101 |
Select Dropdown ordered desc:
102 |
108 |
109 |
model:
110 |
{{ selectedCars7 | json }}
111 |
112 |
Select Dropdown specific values:
113 |
117 |
118 |
model:
119 |
{{ selectedCars8 | json }}
120 |
121 |
Select Dropdown with search:
122 |
128 |
129 |
model:
130 |
{{ selectedCars9 | json }}
131 |
132 |
Select Dropdown single with search:
133 |
139 |
140 |
model:
141 |
{{ selectedCar3 | json }}
142 |
143 |
Select Dropdown with hidden controls:
144 |
149 |
150 |
model:
151 |
{{ selectedCars10 | json }}
152 |
153 |
Select Dropdown multiple readonly:
154 |
159 |
160 |
model:
161 |
{{ selectedCars10 | json }}
162 |
163 |
Select Dropdown multiple readonly and readonly custom label:
164 |
170 |
171 |
model:
172 |
{{ selectedCars11 | json }}
173 |
174 |
Select Dropdown single readonly:
175 |
180 |
181 |
model:
182 |
{{ newSelectedCar | json }}
183 |
184 |
Select Dropdown readonly with custom readonly label:
185 |
191 |
192 |
model:
193 |
{{ notSelectedCar | json }}
194 |
195 |
196 | `,
197 | directives: [SELECT_DIRECTIVES]
198 | })
199 | export class Sample1App {
200 |
201 | cars: Car[] = [
202 | new Car(1, "BMW", 2000),
203 | new Car(2, "Mercedes", 1999),
204 | new Car(3, "Opel", 2008),
205 | new Car(4, "Porshe", 1940),
206 | new Car(4, "Ferrari", 2000)
207 | ];
208 | selectedCars1: Car[] = [];
209 | selectedCars2: Car[] = [];
210 | selectedCars3: Car[] = [];
211 | selectedCars4: Car[] = [];
212 | selectedCars5: string[] = [];
213 | selectedCars6: Car[] = [];
214 | selectedCars7: Car[] = [];
215 | selectedCars8: Car[] = [];
216 | selectedCars9: Car[] = [];
217 | selectedCars10: Car[] = [];
218 | selectedCars11: Car[] = [];
219 | selectedCars12: Car[] = [];
220 | selectedCar: Car;
221 | selectedCar2: Car;
222 | selectedCar3: Car;
223 | newSelectedCar: Car = new Car(1, "BMW", 2000);
224 |
225 | }
226 |
227 | bootstrap(Sample1App, [
228 | HTTP_PROVIDERS,
229 | disableDeprecatedForms(),
230 | provideForms(),
231 | ]);
--------------------------------------------------------------------------------
/sample/sample5-select-tags/Car.ts:
--------------------------------------------------------------------------------
1 | export class Car {
2 | constructor(public id: number,
3 | public name: string,
4 | public year: number) {
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/sample5-select-tags/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | select-tags
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
40 |
41 |
42 |
66 |
67 |
68 |
69 |
70 | Loading...
71 |
72 |
--------------------------------------------------------------------------------
/sample/sample5-select-tags/main.ts:
--------------------------------------------------------------------------------
1 | import {bootstrap} from "@angular/platform-browser-dynamic";
2 | import {Component} from "@angular/core";
3 | import {SELECT_DIRECTIVES} from "../../src/index";
4 | import {Car} from "./Car";
5 | import {HTTP_PROVIDERS, Http} from "@angular/http";
6 | import {Observable} from "rxjs/Rx";
7 | import {provideForms, disableDeprecatedForms} from "@angular/forms";
8 |
9 | @Component({
10 | selector: "app",
11 | template: `
12 |
13 |
14 |
Select Tags with static list:
15 |
19 |
20 |
model:
21 |
{{ selectedCars0 | json }}
22 |
23 |
Select Tags without persistence:
24 |
28 |
29 |
model:
30 |
{{ selectedCars1 | json }}
31 |
32 |
Select Tags with persistence:
33 |
39 |
40 |
model:
41 |
{{ selectedCars2 | json }}
42 |
43 |
Select Tags with pre-defined model:
44 |
48 |
49 |
model:
50 |
{{ selectedCars3 | json }}
51 |
52 |
Select Tags with track by:
53 |
58 |
59 |
model:
60 |
{{ selectedCars4 | json }}
61 |
62 |
Select Tags disabled:
63 |
68 |
69 |
model:
70 |
{{ selectedCars4 | json }}
71 |
72 |
Select Tags with specific values selected:
73 |
77 |
78 |
model:
79 |
{{ selectedCars5 | json }}
80 |
81 |
Select Tags with minimal number of characters to send a request:
82 |
87 |
88 |
model:
89 |
{{ selectedCars6 | json }}
90 |
91 |
Select Tags with ordering enabled:
92 |
97 |
98 |
model:
99 |
{{ selectedCars7 | json }}
100 |
101 |
Select Tags with descendant ordering enabled:
102 |
108 |
109 |
model:
110 |
{{ selectedCars8 | json }}
111 |
112 |
Select Tags with limit:
113 |
118 |
119 |
model:
120 |
{{ selectedCars9 | json }}
121 |
122 |
Select Tags with maximal allowed to selection:
123 |
128 |
129 |
model:
130 |
{{ selectedCars10 | json }}
131 |
132 |
Select Tags with custom item constructor function:
133 |
139 |
140 |
model:
141 |
{{ selectedCars11 | json }}
142 |
143 |
Select Tags with customized add button:
144 |
151 |
152 |
model:
153 |
{{ selectedCars12 | json }}
154 |
155 |
Select Tags with select all option:
156 |
163 |
164 |
model:
165 |
{{ selectedCars13 | json }}
166 |
167 |
Select Tags with unique names:
168 |
174 |
175 |
model:
176 |
{{ selectedCars14 | json }}
177 |
178 |
Select Tags readonly:
179 |
185 |
186 |
model:
187 |
{{ selectedCars15 | json }}
188 |
189 |
Select Tags with custom templates:
190 |
195 |
196 |
197 |
198 |
199 |
200 | {{ item.name }} ({{ item.year }})
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 | #{{ item.id }} {{ item.name }} ({{ item.year }})
210 |
211 |
212 |
213 |
214 |
215 |
216 |
model:
217 |
{{ selectedCars16 | json }}
218 |
219 |
220 | `,
221 | directives: [SELECT_DIRECTIVES]
222 | })
223 | export class Sample1App {
224 |
225 | cars: Car[] = [
226 | new Car(1, "BMW", 2000),
227 | new Car(2, "Mercedes", 1999),
228 | new Car(3, "Opel", 2008),
229 | new Car(4, "Porshe", 1940),
230 | new Car(4, "Ferrari", 2000)
231 | ];
232 | selectedCars0: Car[] = [];
233 | selectedCars1: Car[] = [];
234 | selectedCars2: Car[] = [];
235 | selectedCars3: Car[] = [
236 | this.cars[0]
237 | ];
238 | selectedCars4: Car[] = [
239 | new Car(1, "BMW", 2000),
240 | ];
241 | selectedCars5: string[] = [];
242 | selectedCars6: Car[] = [];
243 | selectedCars7: Car[] = [];
244 | selectedCars8: Car[] = [];
245 | selectedCars9: Car[] = [];
246 | selectedCars10: Car[] = [];
247 | selectedCars11: Car[] = [];
248 | selectedCars12: Car[] = [];
249 | selectedCars13: Car[] = [];
250 | selectedCars14: Car[] = [];
251 | selectedCars15: Car[] = [
252 | this.cars[0],
253 | this.cars[1],
254 | this.cars[2]
255 | ];
256 | selectedCars16: Car[] = [];
257 |
258 | loader = (term: string) => {
259 | return this.http
260 | .get("https://api.github.com/search/repositories?q=" + term)
261 | .map(res => res.json().items) as Observable;
262 | };
263 |
264 | itemConstructor = (term: string) => {
265 | return new Car(0, term, 2016);
266 | };
267 |
268 | constructor(private http: Http) {
269 | }
270 |
271 | }
272 |
273 | bootstrap(Sample1App, [
274 | HTTP_PROVIDERS,
275 | disableDeprecatedForms(),
276 | provideForms(),
277 | ]);
--------------------------------------------------------------------------------
/src/Autocomplete.ts:
--------------------------------------------------------------------------------
1 | import "rxjs/Rx";
2 | import {
3 | Component,
4 | Input,
5 | ViewEncapsulation,
6 | OnInit,
7 | Directive,
8 | ContentChildren,
9 | QueryList,
10 | ContentChild
11 | } from "@angular/core";
12 | import {NG_VALIDATORS, NG_VALUE_ACCESSOR} from "@angular/forms";
13 | import {SelectItems} from "./SelectItems";
14 | import {DROPDOWN_DIRECTIVES} from "ng2-dropdown";
15 | import {Observable, Subscription} from "rxjs/Rx";
16 | import {SelectValueAccessor} from "./SelectValueAccessor";
17 | import {SelectValidator} from "./SelectValidator";
18 | import {Utils} from "./Utils";
19 | import {ItemTemplate} from "./ItemTemplate";
20 |
21 | @Directive({
22 | selector: "autocomplete-dropdown-template"
23 | })
24 | export class AutocompleteDropdownTemplate {
25 |
26 | @ContentChildren(ItemTemplate)
27 | itemTemplates: QueryList;
28 |
29 | }
30 |
31 | @Component({
32 | selector: "autocomplete",
33 | template: `
34 | `,
71 | styles: [`
72 | .autocomplete .hidden {
73 | display: none !important;
74 | }
75 | .autocomplete .autocomplete-dropdown {
76 | position: relative;
77 | }
78 | .autocomplete .autocomplete-input input {
79 | width: 100%;
80 | }
81 | .autocomplete .autocomplete-dropdown-menu {
82 | position: absolute;
83 | top: 100%;
84 | left: 0;
85 | z-index: 1000;
86 | display: none;
87 | float: left;
88 | min-width: 160px;
89 | padding: 5px 0;
90 | margin: 2px 0 0;
91 | font-size: 14px;
92 | text-align: left;
93 | list-style: none;
94 | background-color: #fff;
95 | -webkit-background-clip: padding-box;
96 | background-clip: padding-box;
97 | border: 1px solid #ccc;
98 | border: 1px solid rgba(0, 0, 0, .15);
99 | }
100 | .autocomplete .autocomplete-dropdown.open .dropdown-menu {
101 | display: block;
102 | }
103 | .autocomplete .autocomplete-dropdown-menu .select-items .no-selection,
104 | .autocomplete .autocomplete-dropdown-menu .select-items .select-all,
105 | .autocomplete .autocomplete-dropdown-menu .select-items .checkbox-item,
106 | .autocomplete .autocomplete-dropdown-menu .select-items .radio-item {
107 | padding: 3px 15px;
108 | clear: both;
109 | font-weight: normal;
110 | line-height: 1.42857;
111 | color: #333333;
112 | white-space: nowrap;
113 | display: block;
114 | }
115 | .autocomplete .autocomplete-dropdown-menu .select-items .no-selection:hover,
116 | .autocomplete .autocomplete-dropdown-menu .select-items .select-all:hover,
117 | .autocomplete .autocomplete-dropdown-menu .select-items .checkbox-item:hover,
118 | .autocomplete .autocomplete-dropdown-menu .select-items .radio-item:hover {
119 | text-decoration: none;
120 | color: #fff;
121 | background-color: #0095cc;
122 | cursor: pointer;
123 | }
124 | .autocomplete .select-items .checkbox-item.disabled:hover,
125 | .autocomplete .select-items .radio-item.disabled:hover {
126 | color: #333;
127 | background-color: #eeeeee;
128 | cursor: not-allowed;
129 | }
130 | .autocomplete .autocomplete-add-button {
131 | float: right;
132 | font-size: 0.75em;
133 | color: #999;
134 | }
135 | .autocomplete .autocomplete-add-button a {
136 | border-bottom: 1px dotted;
137 | }
138 | `],
139 | encapsulation: ViewEncapsulation.None,
140 | directives: [
141 | SelectItems,
142 | DROPDOWN_DIRECTIVES
143 | ],
144 | providers: [
145 | Utils,
146 | SelectValueAccessor,
147 | SelectValidator,
148 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
149 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
150 | ]
151 | })
152 | export class Autocomplete implements OnInit {
153 |
154 | // -------------------------------------------------------------------------
155 | // Inputs
156 | // -------------------------------------------------------------------------
157 |
158 | @Input()
159 | placeholder: string = "";
160 |
161 | @Input()
162 | multiple: boolean;
163 |
164 | @Input()
165 | debounceTime = 500;
166 |
167 | @Input()
168 | minQueryLength = 2;
169 |
170 | @Input()
171 | persist: boolean = false;
172 |
173 | @Input()
174 | itemLabelBy: string|((item: any) => string);
175 |
176 | @Input()
177 | labelBy: string|((item: any) => string);
178 |
179 | @Input()
180 | disableBy: string|((item: any) => string);
181 |
182 | @Input()
183 | orderBy: string|((item1: any, item2: any) => number);
184 |
185 | @Input()
186 | orderDirection: "asc"|"desc";
187 |
188 | @Input()
189 | disabled: boolean = false;
190 |
191 | @Input()
192 | limit: number;
193 |
194 | @Input()
195 | maxModelSize: number;
196 |
197 | @Input()
198 | loader: (term: string) => Observable;
199 |
200 | @Input()
201 | itemConstructor: ((term: string) => any);
202 |
203 | @Input()
204 | addButtonLabel: string = "add";
205 |
206 | @Input()
207 | addButtonSecondaryLabel: string = "(or press enter)";
208 |
209 | // -------------------------------------------------------------------------
210 | // Input accessors
211 | // -------------------------------------------------------------------------
212 |
213 | @Input()
214 | set valueBy(valueBy: string|((item: any) => string)) {
215 | this.valueAccessor.valueBy = valueBy;
216 | }
217 |
218 | get valueBy() {
219 | return this.valueAccessor.valueBy;
220 | }
221 |
222 | @Input()
223 | set trackBy(trackBy: string|((item: any) => string)) {
224 | this.valueAccessor.trackBy = trackBy;
225 | }
226 |
227 | get trackBy() {
228 | return this.valueAccessor.trackBy;
229 | }
230 |
231 | @Input()
232 | set required(required: boolean) {
233 | this.validator.options.required = required;
234 | }
235 |
236 | get required() {
237 | return this.validator.options.required;
238 | }
239 |
240 | // -------------------------------------------------------------------------
241 | // Public Properties
242 | // -------------------------------------------------------------------------
243 |
244 | term: string;
245 | lastLoadTerm: string = "";
246 | items: any[] = [];
247 |
248 | @ContentChild(AutocompleteDropdownTemplate)
249 | dropdownTemplate: AutocompleteDropdownTemplate;
250 |
251 | // -------------------------------------------------------------------------
252 | // Private Properties
253 | // -------------------------------------------------------------------------
254 |
255 | private originalModel = false;
256 | private initialized: boolean = false;
257 | private loadDenounce: Function;
258 |
259 | // -------------------------------------------------------------------------
260 | // Constructor
261 | // -------------------------------------------------------------------------
262 |
263 | constructor(public valueAccessor: SelectValueAccessor,
264 | private validator: SelectValidator,
265 | private utils: Utils) {
266 | this.valueAccessor.modelWrites.subscribe((model: any) => {
267 | if (model)
268 | this.originalModel = true;
269 | if (this.initialized)
270 | this.term = this.getItemLabel(model);
271 | });
272 | }
273 |
274 | // -------------------------------------------------------------------------
275 | // Lifecycle callbacks
276 | // -------------------------------------------------------------------------
277 |
278 | ngOnInit() {
279 | this.initialized = true;
280 | this.term = this.getItemLabel(this.valueAccessor.model);
281 |
282 | this.loadDenounce = this.utils.debounce(() => {
283 | if (!this.originalModel && typeof this.term === "string" && this.term.trim().length >= this.minQueryLength) {
284 | this.load();
285 | }
286 | }, this.debounceTime);
287 | }
288 |
289 | // -------------------------------------------------------------------------
290 | // Public Methods
291 | // -------------------------------------------------------------------------
292 |
293 | get dropdownItems() {
294 | return this.items;
295 | }
296 |
297 | onTermChange(term: string) {
298 |
299 | // if persist mode is set then create a new object
300 | if (!this.isMultiple()) {
301 | if (this.persist && term && (!this.valueAccessor.model || this.getItemLabel(this.valueAccessor.model) !== term)) {
302 | const value = this.itemConstructor ? this.itemConstructor(term) : { [this.labelBy as string]: term };
303 | this.valueAccessor.set(value);
304 | this.originalModel = false;
305 | }
306 |
307 | // if term is empty then clean the model
308 | if (this.valueAccessor.model && term === "") {
309 | this.valueAccessor.set(undefined);
310 | }
311 | } else {
312 | this.originalModel = false;
313 | }
314 |
315 | this.loadDenounce();
316 | }
317 |
318 | load(): Subscription {
319 | if (!this.loader || this.originalModel || !this.term || this.term.length < this.minQueryLength || this.term === this.lastLoadTerm)
320 | return;
321 |
322 | return this
323 | .loader(this.term)
324 | .subscribe(items => {
325 | this.lastLoadTerm = this.term;
326 | this.items = items;
327 | });
328 | }
329 |
330 | onModelChange(model: any) {
331 | this.valueAccessor.set(model);
332 | if (!this.isMultiple() && model) {
333 | this.term = this.getItemLabel(model);
334 | this.lastLoadTerm = this.term;
335 | this.items = [];
336 | } else {
337 | this.lastLoadTerm = "";
338 | this.term = "";
339 | this.items = [];
340 | }
341 | }
342 |
343 | addTerm() {
344 | if (!this.term || !this.persist || !this.isMultiple()) return;
345 |
346 | // if (!this.valueAccessor.model)
347 | // this.valueAccessor.set([]);
348 |
349 | const newModel = this.itemConstructor ? this.itemConstructor(this.term) : { [this.labelBy as string]: this.term };
350 | this.valueAccessor.add(newModel);
351 | this.lastLoadTerm = "";
352 | this.term = "";
353 | this.items = [];
354 | }
355 |
356 | isMultiple() {
357 | if (this.multiple !== undefined)
358 | return this.multiple;
359 |
360 | return this.valueAccessor.model instanceof Array;
361 | }
362 |
363 | isDisabled() {
364 | if (this.maxModelSize > 0 &&
365 | this.isMultiple() &&
366 | this.valueAccessor.model.length >= this.maxModelSize)
367 | return true;
368 |
369 | return this.disabled;
370 | }
371 |
372 | getItemLabel(item: any) {
373 | if (!item) return "";
374 | const labelBy = this.valueBy ? this.itemLabelBy : (this.itemLabelBy || this.labelBy);
375 |
376 | if (labelBy) {
377 | if (typeof labelBy === "string") {
378 | return item[labelBy as string];
379 |
380 | } else if (typeof labelBy === "function") {
381 | return (labelBy as any)(item);
382 | }
383 | }
384 |
385 | return item;
386 | }
387 |
388 |
389 | }
--------------------------------------------------------------------------------
/src/Checkbox.ts:
--------------------------------------------------------------------------------
1 | import {Input, Host, Directive, HostBinding, Optional, HostListener, Provider, forwardRef, Inject} from "@angular/core";
2 | import {NG_VALUE_ACCESSOR, NG_VALIDATORS} from "@angular/forms";
3 | import {SelectValueAccessor} from "./SelectValueAccessor";
4 | import {SelectValidator} from "./SelectValidator";
5 | import {CheckboxGroup} from "./CheckboxGroup";
6 |
7 | @Directive({
8 | selector: "input[type=checkbox]",
9 | providers: [
10 | SelectValueAccessor,
11 | SelectValidator,
12 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
13 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
14 | ],
15 | })
16 | export class Checkbox {
17 |
18 | // -------------------------------------------------------------------------
19 | // Inputs
20 | // -------------------------------------------------------------------------
21 |
22 | @Input()
23 | value: any = true;
24 |
25 | @Input()
26 | uncheckedValue: any = false;
27 |
28 | // -------------------------------------------------------------------------
29 | // Input accessors
30 | // -------------------------------------------------------------------------
31 |
32 | @Input()
33 | set required(required: boolean) {
34 | this.validator.options.required = required;
35 | }
36 |
37 | get required() {
38 | return this.validator.options.required;
39 | }
40 |
41 | // -------------------------------------------------------------------------
42 | // Constructor
43 | // -------------------------------------------------------------------------
44 |
45 | constructor(@Optional() @Host() @Inject(forwardRef(() => CheckboxGroup)) private checkboxGroup: CheckboxGroup,
46 | private validator: SelectValidator,
47 | private valueAccessor: SelectValueAccessor) {
48 | }
49 |
50 | // -------------------------------------------------------------------------
51 | // Bindings
52 | // -------------------------------------------------------------------------
53 |
54 | @HostBinding("checked")
55 | get checked() {
56 | const valueAccessor = this.checkboxGroup ? this.checkboxGroup.valueAccessor : this.valueAccessor;
57 | return valueAccessor.has(this.value);
58 | }
59 |
60 | @HostListener("click")
61 | check() {
62 | const valueAccessor = this.checkboxGroup ? this.checkboxGroup.valueAccessor : this.valueAccessor;
63 | if (valueAccessor.model instanceof Array) {
64 | valueAccessor.addOrRemove(this.value);
65 | } else {
66 | if (valueAccessor.has(this.value)) {
67 | valueAccessor.set(this.uncheckedValue);
68 | } else {
69 | valueAccessor.set(this.value);
70 | }
71 | }
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/src/CheckboxGroup.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, Provider, forwardRef, ViewEncapsulation, ContentChildren} from "@angular/core";
2 | import {NG_VALUE_ACCESSOR, NG_VALIDATORS} from "@angular/forms";
3 | import {SelectValueAccessor} from "./SelectValueAccessor";
4 | import {SelectValidator} from "./SelectValidator";
5 | import {CheckboxItem} from "./CheckboxItem";
6 |
7 | @Component({
8 | selector: "checkbox-group",
9 | template: ` `,
10 | encapsulation: ViewEncapsulation.None,
11 | providers: [
12 | SelectValueAccessor,
13 | SelectValidator,
14 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
15 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
16 | ]
17 | })
18 | export class CheckboxGroup {
19 |
20 | // -------------------------------------------------------------------------
21 | // Inputs
22 | // -------------------------------------------------------------------------
23 |
24 | @Input()
25 | disabled: boolean = false;
26 |
27 | @Input()
28 | readonly: boolean = false;
29 |
30 | @Input()
31 | customToggleLogic: (options: { event: MouseEvent, valueAccessor: SelectValueAccessor, value: any }) => void;
32 |
33 | // -------------------------------------------------------------------------
34 | // Input accessors
35 | // -------------------------------------------------------------------------
36 |
37 | @Input()
38 | set trackBy(trackBy: string|((item: any) => string)) {
39 | this.valueAccessor.trackBy = trackBy;
40 | }
41 |
42 | get trackBy() {
43 | return this.valueAccessor.trackBy;
44 | }
45 |
46 | @Input()
47 | set required(required: boolean) {
48 | this.validator.options.required = required;
49 | }
50 |
51 | get required() {
52 | return this.validator.options.required;
53 | }
54 |
55 | // -------------------------------------------------------------------------
56 | // Public Properties
57 | // -------------------------------------------------------------------------
58 |
59 | @ContentChildren(forwardRef(() => CheckboxItem))
60 | checkboxItems: CheckboxItem[];
61 |
62 | // -------------------------------------------------------------------------
63 | // Constructor
64 | // -------------------------------------------------------------------------
65 |
66 | constructor(public valueAccessor: SelectValueAccessor,
67 | private validator: SelectValidator) {
68 | }
69 | // -------------------------------------------------------------------------
70 | // Public Methods
71 | // -------------------------------------------------------------------------
72 |
73 | get values() {
74 | if (!this.checkboxItems) return [];
75 | return this.checkboxItems.map(checkboxItem => checkboxItem.value);
76 | }
77 |
78 | }
--------------------------------------------------------------------------------
/src/CheckboxItem.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, Host, forwardRef, Inject, ViewEncapsulation, Output, EventEmitter} from "@angular/core";
2 | import {CheckboxGroup} from "./CheckboxGroup";
3 |
4 | @Component({
5 | selector: "checkbox-item",
6 | template: `
7 |
11 |
12 |
`,
13 | encapsulation: ViewEncapsulation.None,
14 | styles: [`
15 | .checkbox-item {
16 | cursor: pointer;
17 | }
18 | .checkbox-item.disabled {
19 | cursor: not-allowed;
20 | }
21 | .checkbox-item.readonly {
22 | cursor: default;
23 | }
24 | `]
25 | })
26 | export class CheckboxItem {
27 |
28 | // -------------------------------------------------------------------------
29 | // Inputs
30 | // -------------------------------------------------------------------------
31 |
32 | @Input()
33 | value: any;
34 |
35 | @Input()
36 | disabled: boolean = false;
37 |
38 | @Input()
39 | readonly: boolean = false;
40 |
41 | @Output()
42 | onSelect = new EventEmitter<{ event: Event }>();
43 |
44 | // -------------------------------------------------------------------------
45 | // Constructor
46 | // -------------------------------------------------------------------------
47 |
48 | constructor(@Host() @Inject(forwardRef(() => CheckboxGroup)) private checkboxGroup: CheckboxGroup) {
49 | }
50 |
51 | // -------------------------------------------------------------------------
52 | // Public Methods
53 | // -------------------------------------------------------------------------
54 |
55 | toggleCheck(event: MouseEvent) {
56 | if (this.isReadonly() || this.isDisabled()) return;
57 | if (this.checkboxGroup.customToggleLogic) {
58 | this.checkboxGroup.customToggleLogic({
59 | event: event,
60 | valueAccessor: this.checkboxGroup.valueAccessor,
61 | value: this.value
62 | });
63 |
64 | } else {
65 | this.checkboxGroup.valueAccessor.addOrRemove(this.value);
66 | }
67 |
68 | this.onSelect.emit({ event: event });
69 | }
70 |
71 | isChecked() {
72 | return this.checkboxGroup.valueAccessor.has(this.value);
73 | }
74 |
75 | isDisabled() {
76 | return this.disabled === true || this.checkboxGroup.disabled;
77 | }
78 |
79 | isReadonly() {
80 | return this.readonly || this.checkboxGroup.readonly;
81 | }
82 |
83 | }
--------------------------------------------------------------------------------
/src/ItemTemplate.ts:
--------------------------------------------------------------------------------
1 | import {Input, TemplateRef, ViewContainerRef, Directive, QueryList} from "@angular/core";
2 |
3 | @Directive({
4 | selector: "[itemTemplate]"
5 | })
6 | export class ItemTemplate {
7 |
8 | @Input("itemTemplate")
9 | item: any;
10 |
11 | constructor(public templateRef: TemplateRef) {
12 | }
13 |
14 | }
15 |
16 | @Directive({
17 | selector: "[itemTemplateTransclude]"
18 | })
19 | export class ItemTemplateTransclude {
20 |
21 | private itemTemplates: QueryList;
22 |
23 | @Input()
24 | item: any;
25 |
26 | @Input()
27 | set itemTemplateTransclude(itemTemplates: QueryList) {
28 | this.itemTemplates = itemTemplates;
29 | if (itemTemplates) {
30 | const itemTemplate = itemTemplates.toArray().find(itemTemplate => itemTemplate.item === this.item);
31 | if (itemTemplate && this.viewContainer)
32 | this.viewContainer.createEmbeddedView(itemTemplate.templateRef);
33 | }
34 | };
35 |
36 | get itemTemplateTransclude() {
37 | return this.itemTemplates;
38 | }
39 |
40 | constructor(private viewContainer: ViewContainerRef) {
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/src/RadioBox.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Input,
3 | Host,
4 | Directive,
5 | ElementRef,
6 | HostBinding,
7 | HostListener,
8 | Optional,
9 | Provider,
10 | forwardRef
11 | } from "@angular/core";
12 | import {NG_VALUE_ACCESSOR, NG_VALIDATORS} from "@angular/forms";
13 | import {SelectValueAccessor} from "./SelectValueAccessor";
14 | import {SelectValidator} from "./SelectValidator";
15 | import {RadioGroup} from "./RadioGroup";
16 |
17 | @Directive({
18 | selector: "input[type=radio]",
19 | providers: [
20 | SelectValueAccessor,
21 | SelectValidator,
22 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
23 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
24 | ]
25 | })
26 | export class RadioBox {
27 |
28 | // -------------------------------------------------------------------------
29 | // Input accessors
30 | // -------------------------------------------------------------------------
31 |
32 | @Input()
33 | set required(required: boolean) {
34 | this.validator.options.required = required;
35 | }
36 |
37 | get required() {
38 | return this.validator.options.required;
39 | }
40 |
41 | // -------------------------------------------------------------------------
42 | // Constructor
43 | // -------------------------------------------------------------------------
44 |
45 | constructor(private element: ElementRef, @Optional() @Host() private radioGroup: RadioGroup,
46 | private valueAccessor: SelectValueAccessor,
47 | private validator: SelectValidator) {
48 | }
49 |
50 | // -------------------------------------------------------------------------
51 | // Bindings
52 | // -------------------------------------------------------------------------
53 |
54 | @HostBinding("checked")
55 | get checked() {
56 | const element: HTMLInputElement = this.element.nativeElement;
57 | const valueAccessor = this.radioGroup ? this.radioGroup.valueAccessor : this.valueAccessor;
58 | return valueAccessor.model === element.value;
59 | }
60 |
61 | @HostListener("click")
62 | check() {
63 | const element: HTMLInputElement = this.element.nativeElement;
64 | const valueAccessor = this.radioGroup ? this.radioGroup.valueAccessor : this.valueAccessor;
65 | valueAccessor.set(element.value);
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/src/RadioGroup.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, Provider, ViewEncapsulation} from "@angular/core";
2 | import {NG_VALUE_ACCESSOR, NG_VALIDATORS} from "@angular/forms";
3 | import {SelectValueAccessor} from "./SelectValueAccessor";
4 | import {SelectValidator} from "./SelectValidator";
5 |
6 | @Component({
7 | selector: "radio-group",
8 | template: `
`,
9 | encapsulation: ViewEncapsulation.None,
10 | providers: [
11 | SelectValueAccessor,
12 | SelectValidator,
13 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
14 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
15 | ]
16 | })
17 | export class RadioGroup {
18 |
19 | // -------------------------------------------------------------------------
20 | // Inputs
21 | // -------------------------------------------------------------------------
22 |
23 | @Input()
24 | disabled: boolean = false;
25 |
26 | @Input()
27 | readonly: boolean = false;
28 |
29 | // -------------------------------------------------------------------------
30 | // Input accessors
31 | // -------------------------------------------------------------------------
32 |
33 | @Input()
34 | set trackBy(trackBy: string|((item: any) => string)) {
35 | this.valueAccessor.trackBy = trackBy;
36 | }
37 |
38 | get trackBy() {
39 | return this.valueAccessor.trackBy;
40 | }
41 |
42 | @Input()
43 | set required(required: boolean) {
44 | this.validator.options.required = required;
45 | }
46 |
47 | get required() {
48 | return this.validator.options.required;
49 | }
50 |
51 | // -------------------------------------------------------------------------
52 | // Constructor
53 | // -------------------------------------------------------------------------
54 |
55 | constructor(public valueAccessor: SelectValueAccessor,
56 | private validator: SelectValidator) {
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/src/RadioItem.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, Host, forwardRef, Inject, ViewEncapsulation, Output} from "@angular/core";
2 | import {RadioGroup} from "./RadioGroup";
3 | import {EventEmitter} from "@angular/platform-browser-dynamic/src/facade/async";
4 |
5 | @Component({
6 | selector: "radio-item",
7 | template: `
8 |
12 |
13 |
`,
14 | styles: [`
15 | .radio-item {
16 | cursor: pointer;
17 | }
18 | .radio-item.disabled {
19 | cursor: not-allowed;
20 | }
21 | .radio-item.readonly {
22 | cursor: default;
23 | }
24 | `],
25 | encapsulation: ViewEncapsulation.None
26 | })
27 | export class RadioItem {
28 |
29 | // -------------------------------------------------------------------------
30 | // Inputs
31 | // -------------------------------------------------------------------------
32 |
33 | @Input()
34 | value: any;
35 |
36 | @Input()
37 | disabled: boolean = false;
38 |
39 | @Input()
40 | readonly: boolean = false;
41 |
42 | @Output()
43 | onSelect = new EventEmitter<{ event: Event }>();
44 |
45 | // -------------------------------------------------------------------------
46 | // Constructor
47 | // -------------------------------------------------------------------------
48 |
49 | constructor(@Host() @Inject(forwardRef(() => RadioGroup)) private radioGroup: RadioGroup) {
50 | }
51 |
52 | // -------------------------------------------------------------------------
53 | // Public Methods
54 | // -------------------------------------------------------------------------
55 |
56 | check(event: MouseEvent) {
57 | if (this.isReadonly() || this.isDisabled()) return;
58 | this.radioGroup.valueAccessor.set(this.value);
59 | this.onSelect.emit({ event: event });
60 | }
61 |
62 | isChecked() {
63 | return this.radioGroup.valueAccessor.has(this.value);
64 | }
65 |
66 | isDisabled() {
67 | return this.disabled === true || this.radioGroup.disabled;
68 | }
69 |
70 | isReadonly() {
71 | return this.readonly || this.radioGroup.readonly;
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/src/SelectControlsOptions.ts:
--------------------------------------------------------------------------------
1 | import "rxjs/Rx";
2 | import {
3 | Component,
4 | Input,
5 | ViewEncapsulation,
6 | OnInit,
7 | Directive,
8 | ContentChildren,
9 | QueryList,
10 | ContentChild, Injectable
11 | } from "@angular/core";
12 | import {NG_VALIDATORS, NG_VALUE_ACCESSOR} from "@angular/forms";
13 | import {SelectItems} from "./SelectItems";
14 | import {DROPDOWN_DIRECTIVES} from "ng2-dropdown";
15 | import {Observable, Subscription} from "rxjs/Rx";
16 | import {SelectValueAccessor} from "./SelectValueAccessor";
17 | import {SelectValidator} from "./SelectValidator";
18 | import {Utils} from "./Utils";
19 | import {ItemTemplate} from "./ItemTemplate";
20 |
21 | @Injectable()
22 | export class SelectControlsOptions {
23 |
24 | searchLabel: string;
25 | selectAllLabel: string;
26 | noSelectionLabel: string;
27 |
28 | }
--------------------------------------------------------------------------------
/src/SelectDropdown.ts:
--------------------------------------------------------------------------------
1 | import "rxjs/Rx";
2 | import {Component, Input, Provider, ViewEncapsulation, ViewChild} from "@angular/core";
3 | import {NG_VALIDATORS, NG_VALUE_ACCESSOR} from "@angular/forms";
4 | import {SelectItems} from "./SelectItems";
5 | import {DROPDOWN_DIRECTIVES, Dropdown} from "ng2-dropdown";
6 | import {SelectValidator} from "./SelectValidator";
7 | import {SelectValueAccessor} from "./SelectValueAccessor";
8 |
9 | @Component({
10 | selector: "select-dropdown",
11 | template: `
12 |
13 |
14 |
16 |
17 | 0">
18 | {{ readonly ? (readonlyLabel || label) : label }}
19 |
20 |
27 |
28 |
29 |
30 | {{ readonly ? (readonlyLabel || label) : label }}
31 |
32 |
33 | {{ getItemLabel(model) }}
34 |
35 |
36 |
37 |
38 |
39 |
63 |
64 |
65 |
`,
66 | styles: [`
67 | .select-dropdown .hidden {
68 | display: none !important;
69 | }
70 | .select-dropdown .select-dropdown-box {
71 | outline: none;
72 | }
73 | .select-dropdown .select-dropdown-box .select-items-item {
74 | display: inline-block;
75 | }
76 | .select-dropdown .btn-plus {
77 | border-left: none;
78 | }
79 | .select-dropdown .select-dropdown-dropdown {
80 | position: relative;
81 | }
82 | .select-dropdown .select-dropdown-dropdown-menu {
83 | position: absolute;
84 | top: 100%;
85 | left: 0;
86 | z-index: 1000;
87 | display: none;
88 | float: left;
89 | min-width: 160px;
90 | padding: 5px 0;
91 | margin: 2px 0 0;
92 | font-size: 14px;
93 | text-align: left;
94 | list-style: none;
95 | background-color: #fff;
96 | -webkit-background-clip: padding-box;
97 | background-clip: padding-box;
98 | border: 1px solid #ccc;
99 | border: 1px solid rgba(0, 0, 0, .15);
100 | }
101 | .select-dropdown .dropdown.open .dropdown-menu {
102 | display: block;
103 | }
104 | .select-dropdown .select-dropdown-box .single-selected,
105 | .select-dropdown .select-dropdown-box .select-items .select-items-item .select-items-label,
106 | .select-dropdown .select-dropdown-box .no-selection {
107 | color: #337ab7;
108 | border-bottom: 1px dashed #337ab7;
109 | cursor: pointer;
110 | }
111 | .select-dropdown .dropdown-menu .select-items .select-items-item.selected {
112 | text-decoration: none;
113 | color: #fff;
114 | background-color: #0095cc;
115 | }
116 | .select-dropdown .dropdown-menu .select-items .select-items-item.active.selected {
117 | background-color: #469FE0;
118 | }
119 | .select-dropdown .select-dropdown-box .single-selected.disabled,
120 | .select-dropdown .select-dropdown-box .no-selection.disabled {
121 | color: #CCC;
122 | border-bottom: 1px dashed #CCC;
123 | cursor: not-allowed;
124 | }
125 | .select-dropdown .select-dropdown-box .single-selected.readonly,
126 | .select-dropdown .select-dropdown-box .no-selection.readonly {
127 | color: #333;
128 | border: none;
129 | cursor: default;
130 | }
131 | .select-dropdown .select-dropdown-box .select-items .select-items-label {
132 | padding-right: 0;
133 | }
134 | .select-dropdown .select-dropdown-box .select-items .select-items-item .separator {
135 | padding-right: 2px;
136 | }
137 | .select-dropdown .select-dropdown-box .select-items .select-items-item .separator:before {
138 | content: ",";
139 | }
140 | .select-dropdown .select-dropdown-dropdown-menu .select-items .no-selection,
141 | .select-dropdown .select-dropdown-dropdown-menu .select-items .select-all,
142 | .select-dropdown .select-dropdown-dropdown-menu .select-items .checkbox-item,
143 | .select-dropdown .select-dropdown-dropdown-menu .select-items .radio-item {
144 | padding: 3px 10px;
145 | clear: both;
146 | font-weight: normal;
147 | line-height: 1.42857;
148 | white-space: nowrap;
149 | display: block;
150 | }
151 | .select-dropdown .select-dropdown-dropdown-menu .select-items .no-selection:hover,
152 | .select-dropdown .select-dropdown-dropdown-menu .select-items .select-all:hover,
153 | .select-dropdown .select-dropdown-dropdown-menu .select-items .checkbox-item:hover,
154 | .select-dropdown .select-dropdown-dropdown-menu .select-items .radio-item:hover {
155 | text-decoration: none;
156 | color: #fff;
157 | background-color: #0095cc;
158 | cursor: pointer;
159 | }
160 | .select-dropdown .select-dropdown-dropdown-menu .select-items .checkbox-item.disabled:hover,
161 | .select-dropdown .select-dropdown-dropdown-menu .select-items .radio-item.disabled:hover {
162 | color: #333;
163 | background-color: #eeeeee;
164 | cursor: not-allowed;
165 | }
166 | .select-dropdown .select-dropdown-dropdown-menu .select-items .select-items-search {
167 | margin: 0 5px 5px 5px;
168 | }
169 | `],
170 | encapsulation: ViewEncapsulation.None,
171 | directives: [
172 | SelectItems,
173 | DROPDOWN_DIRECTIVES
174 | ],
175 | providers: [
176 | SelectValueAccessor,
177 | SelectValidator,
178 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
179 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
180 | ]
181 | })
182 | export class SelectDropdown {
183 |
184 | // -------------------------------------------------------------------------
185 | // Inputs
186 | // -------------------------------------------------------------------------
187 |
188 | @Input()
189 | items: any[];
190 |
191 | @Input()
192 | label: string = "click to select";
193 |
194 | @Input()
195 | readonly: boolean = false;
196 |
197 | @Input()
198 | readonlyLabel: string;
199 |
200 | @Input()
201 | multiple: boolean;
202 |
203 | @Input()
204 | listTrackBy: string|((item: any) => string);
205 |
206 | @Input()
207 | listLabelBy: string|((item: any) => string);
208 |
209 | @Input()
210 | labelBy: string|((item: any) => string);
211 |
212 | @Input()
213 | disableBy: string|((item: any) => string);
214 |
215 | @Input()
216 | searchBy: string|((item: any, keyword: string) => boolean);
217 |
218 | @Input()
219 | orderBy: string|((item1: any, item2: any) => number);
220 |
221 | @Input()
222 | orderDirection: "asc"|"desc";
223 |
224 | @Input()
225 | disabled: boolean = false;
226 |
227 | @Input()
228 | limit: number;
229 |
230 | @Input()
231 | noSelectionLabel: string;
232 |
233 | @Input()
234 | searchLabel: string;
235 |
236 | @Input()
237 | selectAllLabel: string;
238 |
239 | @Input()
240 | hideControls: boolean;
241 |
242 | @Input()
243 | maxModelSize: number;
244 |
245 | @Input()
246 | minModelSize: number;
247 |
248 | @Input()
249 | filter: (items: any[]) => any[];
250 |
251 | // -------------------------------------------------------------------------
252 | // Input accessors
253 | // -------------------------------------------------------------------------
254 |
255 | @Input()
256 | set valueBy(valueBy: string|((item: any) => string)) {
257 | this.valueAccessor.valueBy = valueBy;
258 | }
259 |
260 | get valueBy() {
261 | return this.valueAccessor.valueBy;
262 | }
263 |
264 | @Input()
265 | set trackBy(trackBy: string|((item: any) => string)) {
266 | this.valueAccessor.trackBy = trackBy;
267 | }
268 |
269 | get trackBy() {
270 | return this.valueAccessor.trackBy;
271 | }
272 |
273 | @Input()
274 | set required(required: boolean) {
275 | this.validator.options.required = required;
276 | }
277 |
278 | get required() {
279 | return this.validator.options.required;
280 | }
281 |
282 | // -------------------------------------------------------------------------
283 | // Private Properties
284 | // -------------------------------------------------------------------------
285 |
286 | @ViewChild(Dropdown)
287 | dropdown: Dropdown;
288 |
289 | @ViewChild("dropdownSelectItems")
290 | selectItems: SelectItems;
291 |
292 | // -------------------------------------------------------------------------
293 | // Constructor
294 | // -------------------------------------------------------------------------
295 |
296 | constructor(public valueAccessor: SelectValueAccessor,
297 | private validator: SelectValidator) {
298 | }
299 |
300 | // -------------------------------------------------------------------------
301 | // Public Methods
302 | // -------------------------------------------------------------------------
303 |
304 | isMultiple() {
305 | if (this.multiple !== undefined)
306 | return this.multiple;
307 |
308 | return this.valueAccessor.model instanceof Array;
309 | }
310 |
311 | getItemLabel(item: any) {// todo: duplication
312 | if (!item) return "";
313 | const labelBy = this.valueBy ? this.listLabelBy : (this.listLabelBy || this.labelBy);
314 |
315 | if (labelBy) {
316 | if (typeof labelBy === "string") {
317 | return item[labelBy as string];
318 |
319 | } else if (typeof labelBy === "function") {
320 | return (labelBy as any)(item);
321 | }
322 | }
323 |
324 | return item;
325 | }
326 |
327 | onModelChange() {
328 | if (!this.isMultiple()) {
329 | this.dropdown.close();
330 | }
331 | }
332 |
333 | /**
334 | * When user keydowns on component we need to capture esc/enter/arrows to make possible keyboard management.
335 | */
336 | onSelectTagsBoxKeydown(event: KeyboardEvent) {
337 | if (event.keyCode === 27 && this.dropdown.isOpened()) {
338 | this.dropdown.close();
339 |
340 | } else if (event.keyCode === 38) { // top
341 | this.selectItems.previousActive();
342 | event.preventDefault();
343 | event.stopPropagation();
344 |
345 | } else if (event.keyCode === 40) { // bottom
346 | this.selectItems.nextActive();
347 | event.preventDefault();
348 | event.stopPropagation();
349 |
350 | } else if (event.keyCode === 13 || event.keyCode === 32) { // enter or space
351 | this.selectItems.selectActive();
352 | event.preventDefault();
353 | event.stopPropagation();
354 | }
355 |
356 | }
357 |
358 | }
--------------------------------------------------------------------------------
/src/SelectItems.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | Input,
4 | ViewEncapsulation,
5 | ChangeDetectorRef,
6 | AfterViewInit,
7 | ElementRef,
8 | ViewChildren,
9 | QueryList,
10 | Output,
11 | EventEmitter,
12 | ContentChildren, Optional
13 | } from "@angular/core";
14 | import {NG_VALIDATORS, NG_VALUE_ACCESSOR} from "@angular/forms";
15 | import {CheckboxGroup} from "./CheckboxGroup";
16 | import {RadioGroup} from "./RadioGroup";
17 | import {RadioItem} from "./RadioItem";
18 | import {CheckboxItem} from "./CheckboxItem";
19 | import {SelectValueAccessor} from "./SelectValueAccessor";
20 | import {SelectValidator} from "./SelectValidator";
21 | import {ItemTemplate, ItemTemplateTransclude} from "./ItemTemplate";
22 | import {SelectControlsOptions} from "./SelectControlsOptions";
23 |
24 | @Component({
25 | selector: "select-items",
26 | template: `
27 |
28 |
29 |
30 |
31 |
32 |
38 |
39 | {{ getSelectAllLabel() }}
40 |
41 |
42 |
43 |
52 |
59 |
64 |
65 | {{ getItemLabel(item) }}
66 |
67 | ×
69 |
70 |
71 |
72 |
73 |
74 |
80 |
81 | {{ getNoSelectionLabel() }}
82 |
83 |
84 |
85 |
88 |
96 |
101 |
102 | {{ getItemLabel(item) }}
103 |
104 | ×
105 |
106 |
107 |
108 |
109 |
113 |
117 |
`,
118 | styles: [`
119 | .select-items .hidden {
120 | display: none !important;
121 | }
122 | .select-items input.select-items-search {
123 | padding: 6px 12px;
124 | margin-bottom: 5px;
125 |
126 | }
127 | .select-items .select-all, .select-items .no-selection {
128 | cursor: pointer;
129 | }
130 | .select-items .select-items-item.hide-controls.selected {
131 | background: #337ab7;
132 | color: #FFF;
133 | }
134 | .select-items .select-items-item.selected.active {
135 | background: #469FE0;
136 | color: #FFF;
137 | }
138 | .select-items .select-items-item.active {
139 | color: #495c68;
140 | background-color: #f5fafd;
141 | }
142 | .select-items .select-items-label {
143 | padding-left: 3px;
144 | padding-right: 3px;
145 | }
146 | .select-items .select-items-group-header {
147 | font-weight: bold;
148 | margin-top: 2px;
149 | }
150 | .select-items .select-items-group-header.select-all {
151 | cursor: pointer;
152 | }
153 | .select-items .select-items-group-header input[type=checkbox] {
154 | vertical-align: text-top;
155 | }
156 | .select-items .remove-button {
157 | font-size: 12px;
158 | font-weight: bold;
159 | color: #999;
160 | vertical-align: text-bottom;
161 | cursor: pointer;
162 | }
163 | .select-items .remove-button:hover {
164 | color: #333;
165 | }
166 | .select-items .checkbox-item, .select-items .radio-item {
167 | display: inline;
168 | }
169 | .select-items .select-items-item.hide-controls input[type=checkbox],
170 | .select-items .select-items-item.hide-controls input[type=radio] {
171 | display: none;
172 | }
173 | .select-items .more-button, .select-items .hide-button {
174 | color: #999;
175 | cursor: pointer;
176 | }
177 | .select-items .more-button a, .select-items .hide-button a {
178 | border-bottom: 1px dashed;
179 | }
180 | .select-items .more-button a:hover, .select-items .hide-button a:hover {
181 | text-decoration: none;
182 | }
183 | .select-items .caret-body {
184 | display: inline-block;
185 | width: 0;
186 | height: 0;
187 | margin-left: 2px;
188 | vertical-align: middle;
189 | border-right: 4px solid transparent;
190 | border-left: 4px solid transparent;
191 | }
192 | .select-items .caret-bottom {
193 | border-bottom: 4px dashed;
194 | }
195 | .select-items .caret-top {
196 | border-top: 4px dashed;
197 | }
198 | `],
199 | encapsulation: ViewEncapsulation.None,
200 | directives: [
201 | RadioGroup, RadioItem, CheckboxGroup, CheckboxItem, ItemTemplateTransclude
202 | ],
203 | providers: [
204 | SelectValueAccessor,
205 | SelectValidator,
206 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
207 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
208 | ]
209 | })
210 | export class SelectItems implements AfterViewInit {
211 |
212 | // -------------------------------------------------------------------------
213 | // Inputs / Outputs
214 | // -------------------------------------------------------------------------
215 |
216 | @Input()
217 | items: any[];
218 |
219 | @Output()
220 | itemsChange = new EventEmitter();
221 |
222 | @Input()
223 | multiple: boolean;
224 |
225 | @Input()
226 | disableBy: string|((item: any) => string);
227 |
228 | @Input()
229 | labelBy: string|((item: any) => string);
230 |
231 | @Input()
232 | searchBy: string|((item: any, keyword: string) => boolean);
233 |
234 | @Input()
235 | orderBy: string|((item1: any, item2: any) => number);
236 |
237 | @Input()
238 | groupBy: string|((item1: any, item2: any) => number);
239 |
240 | @Input()
241 | groupSelectAll: boolean = false;
242 |
243 | @Input()
244 | hideGroupSelectAllCheckbox: boolean = false;
245 |
246 | @Input()
247 | orderDirection: "asc"|"desc";
248 |
249 | @Input()
250 | limit: number;
251 |
252 | @Input()
253 | disabled: boolean = false;
254 |
255 | @Input()
256 | readonly: boolean = false;
257 |
258 | @Input()
259 | hideSelected: boolean = false;
260 |
261 | @Input()
262 | removeButton: boolean = false;
263 |
264 | @Input()
265 | hideControls: boolean = false;
266 |
267 | @Input()
268 | searchLabel: string;
269 |
270 | @Input()
271 | moreLabel: string;
272 |
273 | @Input()
274 | hideLabel: string;
275 |
276 | @Input()
277 | selectAll: boolean = false;
278 |
279 | @Input()
280 | selectAllLabel: string;
281 |
282 | @Input()
283 | noSelection: boolean = false;
284 |
285 | @Input()
286 | noSelectionLabel: string;
287 |
288 | @Input()
289 | maxModelSize: number;
290 |
291 | @Input()
292 | minModelSize: number;
293 |
294 | @Input()
295 | filter: (items: any[]) => any[];
296 |
297 | @Input()
298 | customToggleLogic: (options: { event: MouseEvent, valueAccessor: SelectValueAccessor }) => void;
299 |
300 | @Output()
301 | onSelect = new EventEmitter<{ event: Event }>();
302 |
303 | @Input()
304 | keyword: string;
305 |
306 | // -------------------------------------------------------------------------
307 | // Input accessors
308 | // -------------------------------------------------------------------------
309 |
310 | @Input()
311 | set valueBy(valueBy: string|((item: any) => string)) {
312 | this.valueAccessor.valueBy = valueBy;
313 | }
314 |
315 | get valueBy() {
316 | return this.valueAccessor.valueBy;
317 | }
318 |
319 | @Input()
320 | set trackBy(trackBy: string|((item: any) => string)) {
321 | this.valueAccessor.trackBy = trackBy;
322 | }
323 |
324 | get trackBy() {
325 | return this.valueAccessor.trackBy;
326 | }
327 |
328 | @Input()
329 | set required(required: boolean) {
330 | this.validator.options.required = required;
331 | }
332 |
333 | get required() {
334 | return this.validator.options.required;
335 | }
336 |
337 | // -------------------------------------------------------------------------
338 | // Public Properties
339 | // -------------------------------------------------------------------------
340 |
341 | active: number = -1;
342 | activeSelectAll: boolean = false;
343 | activeNoSelection: boolean = false;
344 | isMoreShown: boolean = false;
345 | isMaxLimitReached: boolean = false;
346 |
347 | @ViewChildren("itemElement")
348 | itemElements: QueryList;
349 |
350 | @ContentChildren(ItemTemplate)
351 | itemTemplates: QueryList;
352 |
353 | @Input()
354 | customItemTemplates: QueryList;
355 |
356 | // -------------------------------------------------------------------------
357 | // Constructor
358 | // -------------------------------------------------------------------------
359 |
360 | constructor(private cdr: ChangeDetectorRef,
361 | public valueAccessor: SelectValueAccessor,
362 | private validator: SelectValidator,
363 | @Optional() private defaultOptions: SelectControlsOptions) {
364 | }
365 |
366 | // -------------------------------------------------------------------------
367 | // Lifecycle callbacks
368 | // -------------------------------------------------------------------------
369 |
370 | ngAfterViewInit(): void {
371 | this.cdr.detectChanges();
372 | }
373 |
374 | // -------------------------------------------------------------------------
375 | // Public Methods
376 | // -------------------------------------------------------------------------
377 |
378 | get displayedItems() {
379 | return this.getItems();
380 | }
381 |
382 | getItemTemplates() {
383 | if (this.customItemTemplates)
384 | return this.customItemTemplates;
385 |
386 | return this.itemTemplates;
387 | }
388 |
389 | changeModel(model: any) {
390 | this.valueAccessor.set(model);
391 | this.checkActive();
392 | }
393 |
394 | isMultiple() {
395 | if (this.multiple === undefined)
396 | return this.valueAccessor.model instanceof Array;
397 |
398 | return this.multiple;
399 | }
400 |
401 | isItemDisabled(item: any) {
402 | if (this.disabled)
403 | return true;
404 |
405 | if (this.disableBy) {
406 | if (typeof this.disableBy === "string") {
407 | return !!item[this.disableBy as string];
408 |
409 | } else if (typeof this.disableBy === "function") {
410 | return !!(this.disableBy as any)(item);
411 | }
412 | }
413 |
414 | if (this.isMultiple()) {
415 | if (this.maxModelSize > 0 && this.valueAccessor.model.length >= this.maxModelSize) {
416 | return this.valueAccessor.has(item) ? false : true;
417 | }
418 | if (this.minModelSize > 0 && this.valueAccessor.model.length <= this.minModelSize) {
419 | return this.valueAccessor.has(item) ? true : false;
420 | }
421 |
422 | }
423 |
424 | return false;
425 | }
426 |
427 | getItemLabel(item: any) {
428 | if (this.labelBy) {
429 | if (typeof this.labelBy === "string") {
430 | return item[this.labelBy as string];
431 |
432 | } else if (typeof this.labelBy === "function") {
433 | return (this.labelBy as any)(item);
434 | }
435 | }
436 |
437 | return item;
438 | }
439 |
440 | getItemValue(item: any) {
441 | if (this.valueBy) {
442 | if (typeof this.valueBy === "string") {
443 | return item[this.valueBy as string];
444 |
445 | } else if (typeof this.valueBy === "function") {
446 | return (this.valueBy as any)(item);
447 | }
448 | }
449 |
450 | return item;
451 | }
452 |
453 | getGroups() {
454 | if (!this.groupBy)
455 | return [undefined];
456 |
457 | return this.getItems().map(item => {
458 | if (typeof this.groupBy === "string") {
459 | return item[this.groupBy as string];
460 | } else {
461 | return (this.groupBy as (item: any) => any)(item);
462 | }
463 | }).filter((item, index, items) => items.lastIndexOf(item) === index);
464 | }
465 |
466 | getItems(group?: any) {
467 | if (!this.items) return [];
468 |
469 | let items = this.items.map(item => item);
470 | if (this.searchBy && this.keyword) {
471 | items = items.filter(item => {
472 | if (typeof this.searchBy === "string") {
473 | if (typeof item[this.searchBy as string] === "string")
474 | return item[this.searchBy as string].toLowerCase().indexOf(this.keyword.toLowerCase()) !== -1;
475 |
476 | } else {
477 | return (this.searchBy as any)(item, this.keyword);
478 | }
479 | return false;
480 | });
481 | }
482 |
483 | if (this.orderBy) {
484 | if (typeof this.orderBy === "string") {
485 | items.sort((item1, item2) => {
486 | const a = item1[this.orderBy as string];
487 | const b = item2[this.orderBy as string];
488 |
489 | if (typeof a === "string" && typeof b === "string") { // order logic for strings
490 | const aLower = a.toLowerCase();
491 | const bLower = b.toLowerCase();
492 | if (aLower < bLower)
493 | return -1;
494 | if (aLower > bLower)
495 | return 1;
496 | return 0;
497 |
498 | } else if (typeof a === "number" && typeof b === "number") { // order logic for numbers
499 | return a - b;
500 | }
501 |
502 | return 0; // else simply don't order
503 | });
504 | } else {
505 | items.sort(this.orderBy as any);
506 | }
507 |
508 | if (this.orderDirection === "desc")
509 | items = items.reverse();
510 | }
511 |
512 | if (this.hideSelected) {
513 | items = items.filter(item => !this.valueAccessor.has(item));
514 | }
515 |
516 | this.isMaxLimitReached = false;
517 | if (this.limit) {
518 | const startFrom = items.length - this.limit;
519 | this.isMaxLimitReached = startFrom <= 0;
520 | if (startFrom > 0 && !this.isMoreShown)
521 | items.splice(startFrom * -1);
522 | }
523 |
524 | if (this.filter)
525 | this.filter(items);
526 |
527 | if (this.groupBy && group) {
528 | items = items.filter(item => {
529 | if (typeof this.groupBy === "string") {
530 | return item[this.groupBy as string] === group;
531 | } else {
532 | return (this.groupBy as (item: any) => any)(item) === group;
533 | }
534 | });
535 | }
536 |
537 | return items;
538 | }
539 |
540 | removeItem(item: any) {
541 | if (this.isItemDisabled(item)) return;
542 | this.items.splice(this.items.indexOf(item), 1);
543 | this.itemsChange.emit(this.items);
544 | this.checkActive();
545 | }
546 |
547 | showMore() {
548 | this.isMoreShown = true;
549 | }
550 |
551 | hideMore() {
552 | this.isMoreShown = false;
553 | }
554 |
555 | selectAllItems(items: any[]) {
556 | if (!this.isAllSelected(items)) {
557 | items.forEach(item => this.valueAccessor.add(this.getItemValue(item)));
558 | } else {
559 | items.forEach(item => this.valueAccessor.remove(this.getItemValue(item)));
560 | }
561 | }
562 |
563 | isAllSelected(items: any[]): boolean {
564 | let has = true;
565 | items.forEach(item => {
566 | if (has)
567 | has = this.valueAccessor.has(this.getItemValue(item));
568 | });
569 |
570 | return has;
571 | }
572 |
573 | resetModel() {
574 | this.valueAccessor.set(undefined);
575 | }
576 |
577 | previousActive() {
578 | const items = this.getItems();
579 | let newIndex = this.active - 1;
580 | if (newIndex === -1) {
581 | if (this.isMultiple() && this.selectAllLabel) {
582 | this.activeSelectAll = true;
583 | this.active = -1;
584 | } else if (!this.isMultiple() && this.noSelectionLabel) {
585 | this.activeNoSelection = true;
586 | this.active = -1;
587 | } else {
588 | newIndex = 0;
589 | }
590 | } else if (newIndex === -2 && !this.activeNoSelection && !this.activeSelectAll) {
591 | this.active = items.length - 1;
592 | }
593 |
594 | if (newIndex !== -1) {
595 | if (items[newIndex] !== null && items[newIndex] !== undefined) {
596 | this.active = newIndex;
597 | }
598 | }
599 | }
600 |
601 | nextActive(): void {
602 | const items = this.getItems();
603 | const newIndex = this.active + 1;
604 | if (this.activeNoSelection || this.activeSelectAll) {
605 | this.activeNoSelection = this.activeSelectAll = false;
606 | }
607 | if (items[newIndex] !== null && items[newIndex] !== undefined) {
608 | this.active = newIndex;
609 | }
610 | }
611 |
612 | resetActive(): void {
613 | this.active = -1;
614 | this.activeNoSelection = false;
615 | this.activeSelectAll = false;
616 | }
617 |
618 | selectActive() {
619 | const items = this.getItems();
620 | if (this.activeSelectAll) {
621 | this.selectAllItems(items);
622 |
623 | } else if (this.activeNoSelection) {
624 | this.resetModel();
625 |
626 | } else if (this.active > -1 && items[this.active]) {
627 | if (this.isMultiple()) {
628 | this.valueAccessor.addOrRemove(items[this.active]);
629 | } else {
630 | this.valueAccessor.set(items[this.active]);
631 | }
632 | this.checkActive();
633 | }
634 | }
635 |
636 | checkActive(): void {
637 | const items = this.getItems();
638 | if (this.active > -1 && this.active >= items.length) {
639 | this.active = items.length - 1;
640 | }
641 | }
642 |
643 | hasActive(): boolean {
644 | return !!this.activeNoSelection || !!this.activeSelectAll || this.active > -1;
645 | }
646 |
647 | getSearchLabel() {
648 | if (this.searchLabel !== undefined)
649 | return this.searchLabel;
650 |
651 | if (this.defaultOptions && this.defaultOptions.searchLabel !== undefined)
652 | return this.defaultOptions.searchLabel;
653 |
654 | return "";
655 | }
656 |
657 | getSelectAllLabel() {
658 | if (this.selectAllLabel !== undefined)
659 | return this.selectAllLabel;
660 |
661 | if (this.defaultOptions && this.defaultOptions.selectAllLabel !== undefined)
662 | return this.defaultOptions.selectAllLabel;
663 |
664 | return "select all";
665 | }
666 |
667 | getNoSelectionLabel() {
668 | if (this.noSelectionLabel !== undefined)
669 | return this.noSelectionLabel;
670 |
671 | if (this.defaultOptions && this.defaultOptions.noSelectionLabel !== undefined)
672 | return this.defaultOptions.noSelectionLabel;
673 |
674 | return "no selection";
675 | }
676 |
677 | }
--------------------------------------------------------------------------------
/src/SelectTags.ts:
--------------------------------------------------------------------------------
1 | import "rxjs/Rx";
2 | import {
3 | Component, Input, Provider, ViewEncapsulation, OnInit, ViewChild, ElementRef,
4 | ChangeDetectorRef, ContentChild, QueryList, ContentChildren, Directive
5 | } from "@angular/core";
6 | import {NG_VALIDATORS, NG_VALUE_ACCESSOR, AbstractControl} from "@angular/forms";
7 | import {SelectItems} from "./SelectItems";
8 | import {DROPDOWN_DIRECTIVES} from "ng2-dropdown";
9 | import {Observable, Subscription} from "rxjs/Rx";
10 | import {WidthCalculator} from "./WidthCalculator";
11 | import {SelectValidator} from "./SelectValidator";
12 | import {SelectValueAccessor} from "./SelectValueAccessor";
13 | import {Utils} from "./Utils";
14 | import {ItemTemplate} from "./ItemTemplate";
15 |
16 | @Directive({
17 | selector: "select-tags-dropdown-template"
18 | })
19 | export class SelectTagsDropdownTemplate {
20 |
21 | @ContentChildren(ItemTemplate)
22 | itemTemplates: QueryList;
23 |
24 | }
25 |
26 | @Directive({
27 | selector: "select-tags-box-template"
28 | })
29 | export class SelectTagsBoxTemplate {
30 |
31 | @ContentChildren(ItemTemplate)
32 | itemTemplates: QueryList;
33 |
34 | }
35 |
36 | @Component({
37 | selector: "select-tags",
38 | template: `
39 | `,
109 | styles: [`
110 | .select-tags {
111 | }
112 | .select-tags .hidden {
113 | display: none !important;
114 | }
115 | .select-tags-add-button {
116 | margin-top: 2px;
117 | float: right;
118 | font-size: 0.75em;
119 | color: #999;
120 | }
121 | .select-tags-non-unique {
122 | margin-top: 2px;
123 | float: left;
124 | font-size: 0.75em;
125 | color: #992914;
126 | }
127 | .select-tags-add-button a {
128 | border-bottom: 1px dotted;
129 | }
130 | .select-tags .select-tags-dropdown .select-tags-input {
131 | border: none;
132 | outline: none;
133 | box-shadow: none;
134 | height: 25px;
135 | width: 4px;
136 | display: inline-block;
137 | }
138 | .select-tags .select-tags-dropdown {
139 | position: relative;
140 | }
141 | .select-tags .select-tags-dropdown-menu {
142 | position: absolute;
143 | top: 100%;
144 | width: 100%;
145 | left: 0;
146 | z-index: 1000;
147 | display: none;
148 | margin: 2px 0px 2px -1px;
149 | min-width: 160px;
150 | padding: 5px 0 5px 0;
151 | font-size: 14px;
152 | text-align: left;
153 | list-style: none;
154 | background-color: #fff;
155 | -webkit-background-clip: padding-box;
156 | background-clip: padding-box;
157 | border: 1px solid #ccc;
158 | border: 1px solid rgba(0, 0, 0, .15);
159 | }
160 | .select-tags .select-tags-dropdown.open .dropdown-menu {
161 | display: block;
162 | }
163 | .select-tags .select-tags-box {
164 | padding: 5px 5px 3px 5px;
165 | outline: 1px solid #CCC;
166 | }
167 | .select-tags .select-tags-box .select-items,
168 | .select-tags .select-tags-box .select-items .select-items-group,
169 | .select-tags .select-tags-box .select-items .select-items-multiple,
170 | .select-tags .select-tags-box .select-items .select-items-single,
171 | .select-tags .select-tags-box .select-items .radio-group,
172 | .select-tags .select-tags-box .select-items .checkbox-group {
173 | display: inline;
174 | }
175 | .select-tags .select-tags-box .select-items .select-items-item .remove-button {
176 | position: absolute;
177 | top: 0;
178 | right: 0;
179 | bottom: 0;
180 | display: inline-block;
181 | width: 20px;
182 | padding: 2px 0 0 0;
183 | font-size: 12px;
184 | font-weight: bold;
185 | color: inherit;
186 | text-align: center;
187 | text-decoration: none;
188 | vertical-align: middle;
189 | border-left: 1px solid #0073bb;
190 | -webkit-border-radius: 0 2px 2px 0;
191 | -moz-border-radius: 0 2px 2px 0;
192 | border-radius: 0 2px 2px 0;
193 | -webkit-box-sizing: border-box;
194 | -moz-box-sizing: border-box;
195 | box-sizing: border-box;
196 | }
197 | .select-tags .select-tags-box .select-items .select-items-item .remove-button:hover {
198 | background: rgba(0, 0, 0, 0.05);
199 | }
200 | .select-tags .select-tags-box .select-items .select-items-item.with-remove-button {
201 | position: relative;
202 | padding-right: 24px !important;
203 | }
204 | .select-tags .select-tags-box .select-items .select-items-item {
205 | display: inline-block;
206 | padding: 2px 6px;
207 | margin: 0 3px 3px 0;
208 | color: #ffffff;
209 | cursor: pointer;
210 | background: #1da7ee;
211 | border: 1px solid #0073bb;
212 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 1px rgba(255, 255, 255, 0.03);
213 | border-radius: 3px;
214 | background-image: linear-gradient(to bottom, #1da7ee, #178ee9);
215 | background-repeat: repeat-x;
216 | text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3);
217 | background-color: #1b9dec;
218 | -moz-user-select: none;
219 | -khtml-user-select: none;
220 | -webkit-user-select: none;
221 | -ms-user-select: none;
222 | user-select: none;
223 | }
224 | .select-tags .select-tags-box .select-items .select-items-item.selected,
225 | .select-tags .select-tags-box .select-items .select-items-item.selected {
226 | background-color: #0085d4;
227 | background-image: -moz-linear-gradient(top, #008fd8, #0075cf);
228 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf));
229 | background-image: -webkit-linear-gradient(top, #008fd8, #0075cf);
230 | background-image: -o-linear-gradient(top, #008fd8, #0075cf);
231 | background-image: linear-gradient(to bottom, #008fd8, #0075cf);
232 | background-repeat: repeat-x;
233 | border: 1px solid #00578d;
234 | }
235 | .select-tags .select-tags-box .select-items .select-items-item.selected .remove-button ,
236 | .select-tags .select-tags-box .select-items .select-items-item.selected .remove-button {
237 | border-left-color: #00578d;
238 | }
239 | .select-tags .select-tags-dropdown-menu .select-items .no-selection,
240 | .select-tags .select-tags-dropdown-menu .select-items .select-all,
241 | .select-tags .select-tags-dropdown-menu .select-items .checkbox-item,
242 | .select-tags .select-tags-dropdown-menu .select-items .radio-item {
243 | padding: 3px 15px;
244 | clear: both;
245 | font-weight: normal;
246 | line-height: 1.42857;
247 | color: #333333;
248 | white-space: nowrap;
249 | display: block;
250 | }
251 | .select-tags .select-tags-dropdown-menu .select-items .no-selection:hover,
252 | .select-tags .select-tags-dropdown-menu .select-items .select-all:hover,
253 | .select-tags .select-tags-dropdown-menu .select-items .checkbox-item:hover,
254 | .select-tags .select-tags-dropdown-menu .select-items .radio-item:hover {
255 | text-decoration: none;
256 | color: #fff;
257 | background-color: #0095cc;
258 | cursor: pointer;
259 | }
260 | .select-tags .select-items .checkbox-item.disabled:hover,
261 | .select-tags .select-items .radio-item.disabled:hover {
262 | color: #333;
263 | background-color: #eeeeee;
264 | cursor: not-allowed;
265 | }
266 | .select-tags.disabled .select-items .checkbox-item.disabled:hover, .select-tags .select-items .radio-item.disabled:hover{
267 | color: #BBB;
268 | }
269 | .select-tags.disabled,
270 | .select-tags.disabled .select-tags-box .select-items .remove-button,
271 | .select-tags.disabled .select-tags-box .select-items .select-items-item,
272 | .select-tags.disabled .select-tags-box .select-items .no-selection,
273 | .select-tags.disabled .select-tags-box .select-items .select-all,
274 | .select-tags.disabled .select-tags-box .select-items .checkbox-item,
275 | .select-tags.disabled .select-tags-box .select-items .radio-item {
276 | cursor: not-allowed;
277 | }
278 | .select-tags.disabled .select-tags-box .select-items .select-items-item .remove-button:hover {
279 | background: #EEE;
280 | }
281 | .select-tags.disabled .select-tags-box .select-items .select-items-item .remove-button {
282 | border-left: 1px solid #CCC;
283 | }
284 | .select-tags.disabled .select-tags-box .select-items .select-items-item {
285 | background: #EEE;
286 | border: 1px solid #CCC;
287 | color: #BBB;
288 | box-shadow: none;
289 | text-shadow: none;
290 | }
291 | .select-tags.readonly .select-tags-input {
292 | display: none;
293 | }
294 | .select-tags.readonly .select-tags-box .select-items .select-items-item {
295 | cursor: default;
296 | }
297 | `],
298 | encapsulation: ViewEncapsulation.None,
299 | directives: [
300 | SelectItems,
301 | DROPDOWN_DIRECTIVES
302 | ],
303 | providers: [
304 | Utils,
305 | SelectValueAccessor,
306 | SelectValidator,
307 | WidthCalculator,
308 | { provide: NG_VALUE_ACCESSOR, useExisting: SelectValueAccessor, multi: true },
309 | { provide: NG_VALIDATORS, useExisting: SelectValidator, multi: true }
310 | ]
311 | })
312 | export class SelectTags implements OnInit {
313 |
314 | // -------------------------------------------------------------------------
315 | // Inputs
316 | // -------------------------------------------------------------------------
317 |
318 | @Input()
319 | name: string;
320 |
321 | @Input()
322 | debounceTime = 500;
323 |
324 | @Input()
325 | minQueryLength = 2;
326 |
327 | @Input()
328 | persist: boolean = false;
329 |
330 | @Input()
331 | listTrackBy: string|((item: any) => string);
332 |
333 | @Input()
334 | listLabelBy: string|((item: any) => string);
335 |
336 | @Input()
337 | labelBy: string|((item: any) => string);
338 |
339 | @Input()
340 | disableBy: string|((item: any) => string);
341 |
342 | @Input()
343 | orderBy: string|((item1: any, item2: any) => number);
344 |
345 | @Input()
346 | orderDirection: "asc"|"desc";
347 |
348 | @Input()
349 | readonly: boolean = false;
350 |
351 | @Input()
352 | disabled: boolean = false;
353 |
354 | @Input()
355 | limit: number;
356 |
357 | @Input()
358 | maxModelSize: number;
359 |
360 | @Input()
361 | loader: (term: string) => Observable;
362 |
363 | @Input()
364 | items: any[] = [];
365 |
366 | @Input()
367 | itemConstructor: ((term: string) => any);
368 |
369 | @Input()
370 | nonUniqueTermLabel: string = "item with such name already exist";
371 |
372 | @Input()
373 | addButtonLabel: string = "add";
374 |
375 | @Input()
376 | addButtonSecondaryLabel: string = "(or press enter)";
377 |
378 | @Input()
379 | removeButton: boolean = true;
380 |
381 | @Input()
382 | removeByKey: boolean = true;
383 |
384 | @Input()
385 | unique: boolean = false;
386 |
387 | @Input()
388 | selectAllLabel: string;
389 |
390 | /**
391 | * Additional filter to filter items displayed in the dropdown.
392 | */
393 | @Input()
394 | filter: (items: any[]) => any[];
395 |
396 | // -------------------------------------------------------------------------
397 | // Input accessors
398 | // -------------------------------------------------------------------------
399 |
400 | @Input()
401 | set placeholder(placeholder: string) {
402 | this._placeholder = placeholder;
403 | if (!this.term)
404 | this.recalculateInputWidth(undefined, placeholder);
405 | }
406 |
407 | get placeholder() {
408 | return this._placeholder;
409 | }
410 |
411 | @Input()
412 | set valueBy(valueBy: string|((item: any) => string)) {
413 | this.valueAccessor.valueBy = valueBy;
414 | }
415 |
416 | get valueBy() {
417 | return this.valueAccessor.valueBy;
418 | }
419 |
420 | @Input()
421 | set trackBy(trackBy: string|((item: any) => string)) {
422 | this.valueAccessor.trackBy = trackBy;
423 | }
424 |
425 | get trackBy() {
426 | return this.valueAccessor.trackBy;
427 | }
428 |
429 | @Input()
430 | set required(required: boolean) {
431 | this.validator.options.required = required;
432 | }
433 |
434 | get required() {
435 | return this.validator.options.required;
436 | }
437 |
438 | @Input()
439 | set minModelSize(minModelSize: number) {
440 | this.validator.options.minModelSize = minModelSize;
441 | }
442 |
443 | get minModelSize() {
444 | return this.validator.options.minModelSize;
445 | }
446 |
447 | // -------------------------------------------------------------------------
448 | // Public Properties
449 | // -------------------------------------------------------------------------
450 |
451 | @ViewChild("termControl")
452 | termControl: AbstractControl;
453 |
454 | term: string = "";
455 | lastLoadTerm: string = "";
456 | selectedItems: any[] = [];
457 |
458 | @ViewChild("dropdownSelectItems")
459 | dropdownSelectItems: SelectItems;
460 |
461 | @ViewChild("selectTagsBoxInput")
462 | selectTagsBoxInput: ElementRef;
463 |
464 | @ViewChild("tagSelectItems")
465 | tagSelectItems: SelectItems;
466 |
467 | @ViewChild("selectTagsBox")
468 | selectTagsBox: ElementRef;
469 |
470 | @ContentChild(SelectTagsDropdownTemplate)
471 | selectDropdownTemplate: SelectTagsDropdownTemplate;
472 |
473 | @ContentChild(SelectTagsBoxTemplate)
474 | selectTagsTemplate: SelectTagsBoxTemplate;
475 |
476 | selectItemsToggleLogic = (options: { event: MouseEvent, valueAccessor: SelectValueAccessor, value: any }) => {
477 | if (options.event.metaKey || options.event.shiftKey || options.event.ctrlKey) {
478 | options.valueAccessor.addOrRemove(options.value);
479 | } else {
480 | options.valueAccessor.clear();
481 | options.valueAccessor.add(options.value);
482 | }
483 | };
484 |
485 | // -------------------------------------------------------------------------
486 | // Private Properties
487 | // -------------------------------------------------------------------------
488 |
489 | private _placeholder: string = "";
490 | private cursorPosition: number = 0;
491 | private originalModel = false;
492 | private initialized: boolean = false;
493 | private itemsAreLoaded: boolean = false;
494 | private loadDenounce: Function;
495 |
496 | // -------------------------------------------------------------------------
497 | // Constructor
498 | // -------------------------------------------------------------------------
499 |
500 | constructor(private widthCalculator: WidthCalculator,
501 | public valueAccessor: SelectValueAccessor,
502 | private validator: SelectValidator,
503 | private utils: Utils) {
504 | this.valueAccessor.modelWrites.subscribe((model: any) => {
505 | if (model)
506 | this.originalModel = true;
507 | if (this.initialized) {
508 | this.cursorPosition = model.length;
509 | this.recalculateInputWidth(undefined, this.term);
510 | }
511 | });
512 | }
513 |
514 | // -------------------------------------------------------------------------
515 | // Lifecycle callbacks
516 | // -------------------------------------------------------------------------
517 |
518 | ngOnInit() {
519 | this.initialized = true;
520 |
521 | if (this.valueAccessor.model) {
522 | this.cursorPosition = this.valueAccessor.model.length;
523 | }
524 |
525 | this.loadDenounce = this.utils.debounce(() => {
526 | if (!this.originalModel && typeof this.term === "string" && this.term.trim().length >= this.minQueryLength) {
527 | this.load();
528 | }
529 | }, this.debounceTime);
530 | }
531 |
532 | // -------------------------------------------------------------------------
533 | // Public Methods
534 | // -------------------------------------------------------------------------
535 |
536 | onTermChange(term: string) {
537 | this.originalModel = false;
538 | this.dropdownSelectItems.resetActive();
539 | this.loadDenounce();
540 | }
541 |
542 | /**
543 | * Load items using loader.
544 | */
545 | load(): Subscription {
546 | if (this.readonly || !this.loader || this.originalModel || !this.term || this.term.length < this.minQueryLength || this.term === this.lastLoadTerm)
547 | return;
548 |
549 | return this
550 | .loader(this.term)
551 | .subscribe(items => {
552 | this.lastLoadTerm = this.term;
553 | this.items = items;
554 | this.itemsAreLoaded = true;
555 | });
556 | }
557 |
558 | /**
559 | * On model change outside.
560 | */
561 | onModelChange(model: any[]) {
562 | this.cursorPosition = model.length;
563 | this.valueAccessor.set(model);
564 | this.lastLoadTerm = "";
565 | this.term = "";
566 | this.recalculateInputWidth(undefined, this.term);
567 | this.focusTagsInput();
568 | if (this.itemsAreLoaded)
569 | this.items = [];
570 | }
571 |
572 | onItemsChange(model: any[]) {
573 | this.cursorPosition = model.length;
574 | this.valueAccessor.set(model);
575 | }
576 |
577 | /**
578 | * Adds new term to the tags input.
579 | */
580 | addTerm() {
581 | const term = this.term ? this.term.trim() : "";
582 | if (!term || !this.persist) return;
583 | if (this.dropdownSelectItems.hasActive()) return; // if dropdown has active then we select it, instead of adding a new term
584 | if (!this.isTermUnique()) return;
585 |
586 | const newModel = this.itemConstructor ? this.itemConstructor(term) : { [this.labelBy as string]: term };
587 | this.valueAccessor.addAt(newModel, this.cursorPosition);
588 | this.cursorPosition++;
589 | setTimeout(() => this.move()); // using timeout is monkey patch
590 | this.lastLoadTerm = "";
591 | this.term = "";
592 | if (this.itemsAreLoaded)
593 | this.items = [];
594 | this.recalculateInputWidth(undefined, term);
595 | }
596 |
597 | isTermUnique() {
598 | if (!this.term || !this.unique || !this.valueAccessor.model) return true;
599 | return !(this.valueAccessor.model as any[]).find(i => {
600 | return this.getItemLabel(i) === this.term;
601 | });
602 | }
603 |
604 | /**
605 | * Checks if this component is disabled.
606 | */
607 | isDisabled() {
608 | if (this.valueAccessor.model &&
609 | this.maxModelSize > 0 &&
610 | this.valueAccessor.model.length >= this.maxModelSize)
611 | return true;
612 | if (this.readonly)
613 | return true;
614 |
615 | return this.disabled;
616 | }
617 |
618 | /**
619 | * Gets item's label.
620 | */
621 | getItemLabel(item: any) { // todo: duplication
622 | if (!item) return "";
623 | const labelBy = this.valueBy ? this.listLabelBy : (this.listLabelBy || this.labelBy);
624 |
625 | if (labelBy) {
626 | if (typeof labelBy === "string") {
627 | return item[labelBy as string];
628 |
629 | } else if (typeof labelBy === "function") {
630 | return (labelBy as any)(item);
631 | }
632 | }
633 |
634 | return item;
635 | }
636 |
637 | /**
638 | * Recalculates input width. Input width always match input's contents.
639 | */
640 | recalculateInputWidth(event?: Event, value?: any) {
641 | this.widthCalculator.recalculateInputWidth(this.selectTagsBoxInput.nativeElement, event, value);
642 | }
643 |
644 | /**
645 | * When keydown in the tags input occurs we need to recalculate width of input.
646 | * Also we handle to some of them specifically:
647 | * - left, right to move input to the left or right
648 | * - backspace and delete to remove previous or next tag box
649 | */
650 | onInputKeydown(event: KeyboardEvent) {
651 | this.recalculateInputWidth(event);
652 |
653 | // actions with tags can be done when focus inside, but no text in the input box
654 | if (!this.term) {
655 | if (event.keyCode === 37) { // left
656 | this.moveLeft();
657 | event.preventDefault();
658 |
659 | } else if (event.keyCode === 39) { // right
660 | this.moveRight();
661 | event.preventDefault();
662 |
663 | } else if (event.keyCode === 8 && this.removeByKey) { // backspace
664 | this.valueAccessor.removeAt(this.cursorPosition - 1);
665 | setTimeout(() => this.moveLeft()); // using timeout is monkey patch
666 |
667 | } else if (event.keyCode === 46 && this.removeByKey) { // delete
668 | this.valueAccessor.removeAt(this.cursorPosition);
669 | setTimeout(() => this.move()); // using timeout is monkey patch
670 |
671 | }
672 | }
673 | }
674 |
675 | /**
676 | * Moves input to the left.
677 | */
678 | moveLeft() {
679 | if (this.cursorPosition === 0) return;
680 | --this.cursorPosition;
681 | this.move();
682 | }
683 |
684 | /**
685 | * Moves input to the right.
686 | */
687 | moveRight() {
688 | if (this.cursorPosition >= this.tagSelectItems.itemElements.toArray().length) return;
689 | ++this.cursorPosition;
690 | this.move();
691 | }
692 |
693 | /**
694 | * Sets the focus to tags input. Selected tag boxes should be unselected after this operation.
695 | */
696 | focusTagsInput() {
697 | this.selectedItems = [];
698 | this.selectTagsBoxInput.nativeElement.focus();
699 | }
700 |
701 | /**
702 | * When user keydowns on select-tags-input we need to capture backspace, delete, esc and Ctrl+A to make
703 | * operations with tag boxes.
704 | */
705 | onSelectTagsBoxKeydown(event: KeyboardEvent) {
706 | /*if (this.term) {
707 | this.focusTagsInput();
708 | return;
709 | }*/
710 |
711 | if (!this.term && (event.keyCode === 46 || event.keyCode === 8) && this.removeByKey && this.selectedItems.length) { // backspace or delete
712 | this.valueAccessor.removeMany(this.selectedItems);
713 | this.cursorPosition = this.valueAccessor.model.length;
714 | this.selectedItems = [];
715 | event.preventDefault();
716 | event.stopPropagation();
717 | this.focusTagsInput();
718 |
719 | } else if (!this.term && event.keyCode === 27) { // esc
720 | this.selectedItems = [];
721 |
722 | } else if (!this.term && event.keyCode === 65 && (event.metaKey || event.ctrlKey)) { // ctrl + A
723 | this.selectedItems = this.valueAccessor.model.map((i: any) => i);
724 | event.preventDefault();
725 | event.stopPropagation();
726 |
727 | } else if (event.keyCode === 38) { // top
728 | this.dropdownSelectItems.previousActive();
729 | event.preventDefault();
730 | event.stopPropagation();
731 |
732 | } else if (event.keyCode === 40) { // bottom
733 | this.dropdownSelectItems.nextActive();
734 | event.preventDefault();
735 | event.stopPropagation();
736 |
737 | } else if (event.keyCode === 13) { // enter
738 | this.dropdownSelectItems.selectActive();
739 | }
740 |
741 | }
742 |
743 | /**
744 | * When user clicks item in boxes a tag we need to stop the event propagation, because we don't want out input
745 | * to get a focus at this time.
746 | */
747 | onTagSelect(event: { event: MouseEvent }) {
748 | event.event.preventDefault();
749 | event.event.stopPropagation();
750 | }
751 |
752 | /**
753 | * Exposes items in the dropdown, so they can be customized.
754 | */
755 | get dropdownItems() {
756 | return this.items;
757 | }
758 |
759 | /**
760 | * Exposes items in the tags box, so they can be customized.
761 | */
762 | get tagsItems() {
763 | return this.valueAccessor.model;
764 | }
765 |
766 | // -------------------------------------------------------------------------
767 | // Private Methods
768 | // -------------------------------------------------------------------------
769 |
770 | /**
771 | * Moves input box into cursorPosition.
772 | */
773 | private move() {
774 | const items = this.tagSelectItems.itemElements.toArray();
775 | const input = this.selectTagsBoxInput.nativeElement;
776 | if (items[this.cursorPosition]) {
777 | const element = items[this.cursorPosition].nativeElement;
778 | element.parentElement.insertBefore(input, element);
779 | } else {
780 | if (this.cursorPosition === 0 || !items[this.cursorPosition - 1])
781 | return;
782 |
783 | const element = items[this.cursorPosition - 1].nativeElement;
784 | element.parentElement.insertBefore(input, element.nextSibling);
785 | }
786 | this.selectTagsBoxInput.nativeElement.focus();
787 | }
788 |
789 | }
--------------------------------------------------------------------------------
/src/SelectValidator.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 | import {Validator, AbstractControl} from "@angular/forms";
3 |
4 | @Injectable()
5 | export class SelectValidator implements Validator {
6 |
7 | // -------------------------------------------------------------------------
8 | // Public Properties
9 | // -------------------------------------------------------------------------
10 |
11 | options: {
12 | required?: boolean;
13 | minModelSize?: number;
14 | } = {};
15 |
16 | // -------------------------------------------------------------------------
17 | // Implemented from Validator
18 | // -------------------------------------------------------------------------
19 |
20 | validate(c: AbstractControl) {
21 | if (!this.options) return null;
22 |
23 | const errors: any = {};
24 | if (this.options.required && (!c.value || (c.value instanceof Array) && c.value.length === 0))
25 | errors.required = true;
26 | if (this.options.minModelSize && (!c.value || (c.value instanceof Array) && c.value.length < this.options.minModelSize))
27 | errors.minModelSize = true;
28 |
29 | return Object.keys(errors).length > 0 ? errors : null;
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/src/SelectValueAccessor.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, EventEmitter} from "@angular/core";
2 | import {ControlValueAccessor} from "@angular/forms";
3 |
4 | @Injectable()
5 | export class SelectValueAccessor implements ControlValueAccessor {
6 |
7 | // -------------------------------------------------------------------------
8 | // Public Properties
9 | // -------------------------------------------------------------------------
10 |
11 | modelWrites = new EventEmitter();
12 | trackBy: string|((item: any) => string);
13 | valueBy: string|((item: any) => string);
14 |
15 | // -------------------------------------------------------------------------
16 | // Private Properties
17 | // -------------------------------------------------------------------------
18 |
19 | private _model: any;
20 | private onChange: (m: any) => void;
21 | private onTouched: (m: any) => void;
22 |
23 | // -------------------------------------------------------------------------
24 | // Implemented from ControlValueAccessor
25 | // -------------------------------------------------------------------------
26 |
27 | writeValue(value: any): void {
28 | this._model = value;
29 | this.modelWrites.emit(value);
30 | }
31 |
32 | registerOnChange(fn: any): void {
33 | this.onChange = fn;
34 | }
35 |
36 | registerOnTouched(fn: any): void {
37 | this.onTouched = fn;
38 | }
39 |
40 | // -------------------------------------------------------------------------
41 | // Accessors
42 | // -------------------------------------------------------------------------
43 |
44 | get model() {
45 | return this._model;
46 | }
47 |
48 | // -------------------------------------------------------------------------
49 | // Public Methods
50 | // -------------------------------------------------------------------------
51 |
52 | set(value: any) {
53 | this._model = value;
54 | this.onChange(this._model);
55 | }
56 |
57 | add(value: any) {
58 | if (!this.has(value)) {
59 | if (this._model instanceof Array) {
60 | this._model.push(value);
61 | } else {
62 | this._model = [value];
63 | }
64 | this.onChange(this._model);
65 | }
66 | }
67 |
68 | remove(value: any) {
69 | // value = this.extractModelValue(value);
70 | if (this.trackBy) {
71 | const item = this._model.find((i: any) => {
72 | return this.extractValue(i, this.trackBy) === this.extractValue(value, this.trackBy);
73 | });
74 | this.removeAt(this._model.indexOf(item));
75 | } else {
76 | const item = this._model.find((i: any) => {
77 | return i === value;
78 | });
79 | this.removeAt(this._model.indexOf(item));
80 | }
81 | }
82 |
83 | removeAt(index: number): boolean {
84 | if (!this._model || index < 0 || (index > this._model.length - 1))
85 | return false;
86 |
87 | this._model.splice(index, 1);
88 | this.onChange(this._model);
89 | }
90 |
91 | clear() {
92 | if (this._model instanceof Array) {
93 | this._model.splice(0, this._model.length);
94 | } else {
95 | this._model = undefined;
96 | }
97 | }
98 |
99 | addAt(value: any, index: number): boolean {
100 | if (!this._model || index < 0)
101 | return false;
102 |
103 | this._model.splice(index, 0, value);
104 | this.onChange(this._model);
105 | }
106 |
107 | addOrRemove(value: any) {
108 | if (this.has(value)) {
109 | this.remove(value);
110 | } else {
111 | this.add(value);
112 | }
113 | }
114 |
115 | has(value: any): boolean {
116 | // value = this.extractModelValue(value);
117 | if (this._model instanceof Array) {
118 | if (this.trackBy) {
119 | return !!this._model.find((i: any) => {
120 | return this.extractValue(i, this.trackBy) === this.extractValue(value, this.trackBy);
121 | });
122 | } else {
123 | return !!this._model.find((i: any) => {
124 | return i === value;
125 | });
126 | }
127 |
128 | } else if (this._model !== null && this._model !== undefined) {
129 | if (this.trackBy) {
130 | return this.extractValue(this._model, this.trackBy) === this.extractValue(value, this.trackBy);
131 | } else {
132 | return this._model === value;
133 | }
134 | }
135 |
136 | return false;
137 | }
138 |
139 | addMany(values: any[]): void {
140 | if (!values || !values.length) return;
141 | values.forEach(value => this.add(value));
142 | }
143 |
144 | removeMany(values: any[]): void {
145 | if (!values || !values.length) return;
146 | values.forEach(value => this.remove(value));
147 | }
148 |
149 | hasMany(values: any[]): boolean {
150 | if (!values || !values.length) return false;
151 |
152 | let has = true;
153 | values.forEach(item => {
154 | if (has)
155 | has = this.has(item.value);
156 | });
157 | return has;
158 | }
159 |
160 | private extractModelValue(model: any) {
161 | if (this.valueBy) {
162 | return this.extractValue(model, this.valueBy);
163 | } else {
164 | return model;
165 | }
166 | }
167 |
168 | private extractValue(model: any, value: string|((item: any) => string)) {
169 | if (value instanceof Function) {
170 | return (value as (item: any) => any)(model);
171 | } else {
172 | return model[value as string];
173 | }
174 | }
175 |
176 | }
--------------------------------------------------------------------------------
/src/Utils.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 |
3 | @Injectable()
4 | export class Utils {
5 |
6 | debounce(func: Function, wait: number, immediate: boolean = false) {
7 | let timeout: any;
8 | return function() {
9 | let context = this, args = arguments;
10 | let later = function() {
11 | timeout = null;
12 | if (!immediate) func.apply(context, args);
13 | };
14 | let callNow = immediate && !timeout;
15 | clearTimeout(timeout);
16 | timeout = setTimeout(later, wait);
17 | if (callNow) func.apply(context, args);
18 | };
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/src/WidthCalculator.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 |
3 | @Injectable()
4 | export class WidthCalculator {
5 |
6 | /**
7 | * Recalculates input width when input's value changes.
8 | *
9 | * @param {HTMLInputElement} input
10 | * @param {Event} event
11 | */
12 | recalculateInputWidth(input: HTMLInputElement, event?: any, value?: any) {
13 | if (event && (event.metaKey || event.altKey || !event.target)) return;
14 |
15 | value = value !== undefined ? value : input.value;
16 | if (event && event.type && event.type.toLowerCase() === "keydown")
17 | value = this.processValueAfterPressedKey(value, this.getSelection(input), event.keyCode, event.shiftKey);
18 |
19 | // if there is NO value in the input, it means that in the input there can be
20 | // a placeholder value. and if placeholder is not empty then use its value to measure
21 | let placeholder = input.getAttribute("placeholder") || "";
22 | if (!value.length && placeholder.length > 0) {
23 | value = placeholder;
24 | }
25 |
26 | // finally measure input value's width and update input's width
27 | let measureContainer = this.createInputStringMeasureContainer(input);
28 | let width = this.measureString(value, measureContainer) + 10;
29 | input.style.setProperty("width", width + "px");
30 | }
31 |
32 | /**
33 | * Creates and returns a container in the DOM where string can be stored to measure its length.
34 | *
35 | * @param {HTMLInputElement} parentElement Element (input box) from where properties will be copied to match
36 | * the styles (font-size, font-family, etc.) to make a proper string measurement
37 | * @returns {object} Created element where string can be stored to measure string length
38 | */
39 | createInputStringMeasureContainer(parentElement: HTMLInputElement) {
40 | let body: any = document.querySelector("body");
41 | let stringMeasureElement = document.getElementById("tokenInputStringMeasure");
42 | if (!stringMeasureElement) {
43 | stringMeasureElement = document.createElement("div");
44 | stringMeasureElement.id = "tokenInputStringMeasure";
45 | const styles: any = {
46 | position: "absolute",
47 | top: "-99999px",
48 | left: "-99999px",
49 | width: "auto",
50 | padding: 0,
51 | whiteSpace: "pre"
52 | };
53 | Object.keys(styles).forEach(key => {
54 | stringMeasureElement.style.setProperty(key, styles[key]);
55 | });
56 | body.appendChild(stringMeasureElement);
57 | }
58 |
59 | this.transferStyles(parentElement, stringMeasureElement, [
60 | "letterSpacing",
61 | "fontSize",
62 | "fontFamily",
63 | "fontWeight",
64 | "textTransform"
65 | ]);
66 |
67 | return stringMeasureElement;
68 | }
69 |
70 | /**
71 | * Copies CSS properties from one element to another.
72 | *
73 | * @param {HTMLElement} from
74 | * @param {HTMLElement} to
75 | * @param {string[]|Array} properties
76 | */
77 | transferStyles(from: HTMLElement, to: HTMLElement, properties: string[]) {
78 | let i: number, n: number, styles: any = {};
79 | if (properties) {
80 | for (i = 0, n = properties.length; i < n; i++) {
81 | styles[properties[i]] = this.css(from/*[0]*/, properties[i]);
82 | }
83 | } else {
84 | const fromStyles: any = from.style;
85 | Object.keys(fromStyles).filter(styleName => fromStyles[styleName] !== "").forEach(styleName => {
86 | styles[styleName] = fromStyles[styleName];
87 | });
88 | }
89 | Object.keys(styles).forEach(key => {
90 | (to as any).style[key] = styles[key];
91 | });
92 | }
93 |
94 | /**
95 | * Gets the proper value of the given CSS property.
96 | *
97 | * @param {HTMLElement} element
98 | * @param {string} name
99 | * @returns {string|undefined}
100 | */
101 | css(element: any, name: string) {
102 | let val: string;
103 | if (typeof element.currentStyle !== "undefined") { // for old IE
104 | val = element.currentStyle[name];
105 | } else if (typeof window.getComputedStyle !== "undefined") { // for modern browsers
106 | val = element.ownerDocument.defaultView.getComputedStyle(element, null)[name];
107 | } else {
108 | val = element.style[name];
109 | }
110 | return (val === "") ? undefined : val;
111 | }
112 |
113 | /**
114 | * Measures the width of a string within a parent element (in pixels).
115 | *
116 | * @param {string} str String to be measured
117 | * @param {HTMLElement} measureContainer html element
118 | * @returns {int}
119 | */
120 | measureString(str: string, measureContainer: HTMLElement) {
121 | str = str.replace(new RegExp(" ", "g"), "_"); // replace spaces
122 | const textNode = document.createTextNode(str);
123 | measureContainer.appendChild(textNode);
124 | let width = measureContainer.offsetWidth;
125 | measureContainer.innerHTML = "";
126 | return width;
127 | }
128 |
129 | /**
130 | * Determines the current selection within a text input control.
131 | * Returns an object containing:
132 | * - start Where selection started
133 | * - length How many characters were selected
134 | *
135 | * @param {object} inputElement
136 | * @returns {{start: int, length: int}}
137 | */
138 | getSelection(inputElement: any) {
139 | let selection = { start: 0, length: 0 };
140 |
141 | if ("selectionStart" in inputElement) {
142 | selection.start = inputElement.selectionStart;
143 | selection.length = inputElement.selectionEnd - inputElement.selectionStart;
144 |
145 | } else if ((document as any).selection) {
146 | inputElement.focus();
147 | let sel = (document as any).selection.createRange();
148 | let selLen = (document as any).selection.createRange().text.length;
149 | sel.moveStart("character", inputElement.value.length * -1);
150 | selection.start = sel.text.length - selLen;
151 | selection.length = selLen;
152 | }
153 |
154 | return selection;
155 | }
156 |
157 | /**
158 | * Removes value based on the cursor position. If there is something selected then
159 | * this selected text will be removed, otherwise if no selection, but BACKSPACE key
160 | * has been pressed, then previous character will be removed, or if DELETE key has
161 | * been pressed when next character will be removed.
162 | *
163 | * @param {string} value The input value
164 | * @param {object} selection Current selection in the input
165 | * @param {int} pressedKeyCode Key that was pressed by a user
166 | * @returns {string}
167 | */
168 | removeValueByCursorPosition(value: string, selection: any, pressedKeyCode: number) {
169 |
170 | if (selection.length) {
171 | return value.substring(0, selection.start) + value.substring(selection.start + selection.length);
172 |
173 | } else if (pressedKeyCode === 8 /* "BACKSPACE" */ && selection.start) {
174 | return value.substring(0, selection.start - 1) + value.substring(selection.start + 1);
175 |
176 | } else if (pressedKeyCode === 46 /* "DELETE" */ && typeof selection.start !== "undefined") {
177 | return value.substring(0, selection.start) + value.substring(selection.start + 1);
178 | }
179 |
180 | return value;
181 | }
182 |
183 | /**
184 | * Checks if given key code is a-z, or A-Z, or 1-9 or space.
185 | *
186 | * @param {number} keyCode
187 | * @returns {boolean} True if key code in the [a-zA-Z0-9 ] range or not
188 | */
189 | isPrintableKey(keyCode: number) {
190 | return ((keyCode >= 97 && keyCode <= 122) || // a-z
191 | (keyCode >= 65 && keyCode <= 90) || // A-Z
192 | (keyCode >= 48 && keyCode <= 57) || // 0-9
193 | keyCode === 32 // space
194 | );
195 | }
196 |
197 | /**
198 | * Checks if given key code is "removing key" (e.g. backspace or delete).
199 | *
200 | * @param {number} keyCode
201 | * @returns {boolean}
202 | */
203 | isRemovingKey(keyCode: number) {
204 | return keyCode === 46 || keyCode === 8; // "DELETE" or "BACKSPACE"
205 | }
206 |
207 | /**
208 | * Processes a value after some key has been pressed by a user.
209 | *
210 | * @param {string} value
211 | * @param {{ start: number, length: number }} selection Position where user selected a text
212 | * @param {number} keyCode The code of the key that has been pressed
213 | * @param {boolean} shiftKey Indicates if shift key has been pressed or not
214 | * @returns {string}
215 | */
216 | processValueAfterPressedKey(value: string, selection: { start: number, length: number }, keyCode: number, shiftKey: boolean) {
217 |
218 | if (this.isRemovingKey(keyCode))
219 | return this.removeValueByCursorPosition(value, selection, keyCode);
220 |
221 | if (this.isPrintableKey(keyCode)) {
222 | let character = String.fromCharCode(keyCode);
223 | character = shiftKey ? character.toUpperCase() : character.toLowerCase();
224 | return value + character;
225 | }
226 |
227 | return value;
228 | }
229 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./CheckboxGroup";
2 | export * from "./Checkbox";
3 | export * from "./CheckboxItem";
4 | export * from "./RadioGroup";
5 | export * from "./RadioBox";
6 | export * from "./RadioItem";
7 | export * from "./SelectItems";
8 | export * from "./Autocomplete";
9 | export * from "./SelectDropdown";
10 | export * from "./SelectTags";
11 | export * from "./ItemTemplate";
12 |
13 | import {Checkbox} from "./Checkbox";
14 | import {CheckboxItem} from "./CheckboxItem";
15 | import {CheckboxGroup} from "./CheckboxGroup";
16 | import {RadioBox} from "./RadioBox";
17 | import {RadioItem} from "./RadioItem";
18 | import {RadioGroup} from "./RadioGroup";
19 | import {SelectItems} from "./SelectItems";
20 | import {Autocomplete, AutocompleteDropdownTemplate} from "./Autocomplete";
21 | import {SelectDropdown} from "./SelectDropdown";
22 | import {SelectTags, SelectTagsDropdownTemplate, SelectTagsBoxTemplate} from "./SelectTags";
23 | import {ItemTemplate} from "./ItemTemplate";
24 |
25 | export const SELECT_DIRECTIVES: [any] = [
26 | CheckboxGroup,
27 | CheckboxItem,
28 | RadioGroup,
29 | RadioItem,
30 | RadioBox,
31 | Checkbox,
32 | SelectItems,
33 | Autocomplete,
34 | SelectDropdown,
35 | SelectTags,
36 | ItemTemplate,
37 | SelectTagsDropdownTemplate,
38 | SelectTagsBoxTemplate,
39 | AutocompleteDropdownTemplate
40 | ];
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.8.0",
3 | "compilerOptions": {
4 | "outDir": "build/es5",
5 | "target": "es5",
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "sourceMap": true,
11 | "noImplicitAny": true,
12 | "declaration": true
13 | },
14 | "exclude": [
15 | "build",
16 | "node_modules",
17 | "typings/browser.d.ts",
18 | "typings/browser"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "class-name": true,
4 | "comment-format": [
5 | true,
6 | "check-space"
7 | ],
8 | "indent": [
9 | true,
10 | "spaces"
11 | ],
12 | "no-duplicate-variable": true,
13 | "no-eval": true,
14 | "no-internal-module": true,
15 | "no-var-keyword": true,
16 | "one-line": [
17 | true,
18 | "check-open-brace",
19 | "check-whitespace"
20 | ],
21 | "quotemark": [
22 | true,
23 | "double"
24 | ],
25 | "semicolon": true,
26 | "triple-equals": [
27 | true,
28 | "allow-null-check"
29 | ],
30 | "typedef-whitespace": [
31 | true,
32 | {
33 | "call-signature": "nospace",
34 | "index-signature": "nospace",
35 | "parameter": "nospace",
36 | "property-declaration": "nospace",
37 | "variable-declaration": "nospace"
38 | }
39 | ],
40 | "variable-name": [
41 | true,
42 | "ban-keywords"
43 | ],
44 | "whitespace": [
45 | true,
46 | "check-branch",
47 | "check-decl",
48 | "check-operator",
49 | "check-separator",
50 | "check-type"
51 | ]
52 | }
53 | }
--------------------------------------------------------------------------------
/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ambientDevDependencies": {
3 | "assertion-error": "github:DefinitelyTyped/DefinitelyTyped/assertion-error/assertion-error.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645",
4 | "chai": "github:DefinitelyTyped/DefinitelyTyped/chai/chai.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645",
5 | "mocha": "github:DefinitelyTyped/DefinitelyTyped/mocha/mocha.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645",
6 | "node": "registry:dt/node#4.0.0+20160505172921",
7 | "sinon": "github:DefinitelyTyped/DefinitelyTyped/sinon/sinon.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645"
8 | },
9 | "ambientDependencies": {
10 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------