├── .gitignore ├── LICENSE ├── README.md ├── angular-schema-form-dynamic-select.js ├── angular-schema-form-dynamic-select.min.js ├── app.css ├── app.js ├── bower.json ├── gulpfile.js ├── index.html ├── karma.conf.js ├── package.json ├── src ├── angular-schema-form-dynamic-select.js ├── strapmultiselect.html ├── strapselect.html ├── uiselect.html └── uiselectmultiple.html ├── test ├── testdata.json ├── testdata_mapped.json └── tests.js └── ui-sortable.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | bower_components 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | .DS_Store 31 | 32 | 33 | # IDE configuration files 34 | .idea 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 chengz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![bower version](https://img.shields.io/bower/v/angular-schema-form-dynamic-select.svg?style=flat-square)](#bower) 2 | [![npm version](https://img.shields.io/npm/v/angular-schema-form-dynamic-select.svg?style=flat-square)](https://www.npmjs.org/package/angular-schema-form-dynamic-select) 3 | [![Join the chat at https://gitter.im/OptimalBPM/angular-schema-form-dynamic-select](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/OptimalBPM/angular-schema-form-dynamic-select?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | 6 | # WARNING: This component is currently looking for maintainers! 7 | 8 | Angular Schema Form Dynamic Select (ASFDS) add-on 9 | ================================================= 10 | 11 | This add-on integrates the [angular-strap-select](https://github.com/mgcrea/angular-strap/tree/master/src/select) and the [angular-ui-select](https://github.com/angular-ui/ui-select) components 12 | to provide fully featured drop downs to [angular-schema-form](https://github.com/Textalk/angular-schema-form). 13 | 14 | It is drop-in compliant with angular-schema-forms existing selects, which makes using it a breeze, no adaptations are needed. 15 | 16 | All settings are kept in the form, separating validation and UI-configuration. 17 | 18 | *Note about UI-select: 19 | The ui-select support is quite new and while many things work, it is still somewhat partial, so WRT the features below, they apply to angular-strap-select. For that reason, ui-select has a [special section in the documentation](https://github.com/OptimalBPM/angular-schema-form-dynamic-select#ui-select).* 20 | 21 | # Features: 22 | 23 | * Static and dynamic lists 24 | * Single and multiple select 25 | * Convenient HTTP GET/POST and property mapping functionality 26 | * Filters 27 | * Sync and Async callbacks 28 | * All callbacks referenced either by name (string) or reference 29 | * [Angular schema form options](https://github.com/Textalk/angular-schema-form/blob/development/docs/index.md#standard-options) 30 | * Supported: 31 | * key, type, title, description, placeholder 32 | * enum 33 | * notitle, onChange, condition 34 | * htmlClass, labelHtmlClass and fieldHtmlClass 35 | * validationMessage 36 | * Not supported(will be added): 37 | * readonly, copyValueTo, 38 | * Not applicable(will not be added due to the nature of drop downs, [disagree?](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/issues)): 39 | * feedback 40 | 41 | 42 | 43 | # Example 44 | 45 | There is a live example at [http://demo.optimalbpm.se/angular-schema-form-dynamic-select/](http://demo.optimalbpm.se/angular-schema-form-dynamic-select/). 46 | 47 | The example code is in the repository, it's made up of the index.html, app.js, test/testdata.json and test/testdata_mapped.json files. 48 | 49 | To run it locally, simply clone the repository: 50 | 51 | git clone https://github.com/OptimalBPM/angular-schema-form-dynamic-select.git 52 | cd angular-schema-form-dynamic-select 53 | bower update 54 | 55 | ..and open index.html in a browser or serve using your favorite IDE. 56 | 57 | However, to make the *entire* example work properly, as it contains UI-select components, please install the [ui-select dependencies](https://github.com/OptimalBPM/angular-schema-form-dynamic-select#ui-select) as well. 58 | 59 | (you will need to have [bower installed](http://bower.io/#install-bower), of course) 60 | 61 | # Help 62 | 63 | What are my options if I feel I need help? 64 | 65 | ### I don't understand the documentation 66 | The prerequisite for understanding the ASFDS documentation [below](https://github.com/OptimalBPM/angular-schema-form-dynamic-select#installation-and-usage) is that you have a basic understanding of how to use [Angular Schema Form](https://github.com/Textalk/angular-schema-form#basic-usage).
67 | So if you understand that, and still cannot understand the documentation of ASFDS, it is probably not your fault. [Please create an issue](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/issues) in those cases.
68 | 69 | ### I have a question 70 | If you have a question and cannot find an answer for it in the documentation below, [please create an issue](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/issues).
71 | Questions and their answers have great value for the community. 72 | 73 | ### I have found a bug 74 | [Please create an issue](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/issues). 75 | Be sure to provide ample information, remember that any help won't be better than your explanation. 76 | 77 | Unless something is obviously wrong, you are likely to be asked to provide a [plunkr](http://plnkr.co/)-example, displaying the erroneous behaviour. 78 | 79 | While this might feel troublesome, a tip is to always make a plunkr that have the same external requirements as your project.
80 | It is great for troubleshooting those annoying problems where you don't know if the problem is at your end or the components'.
81 | And you can then easily fork and provide as an example.
82 | You will answers and resolutions way quicker, also, many other open source projects require it.
83 | 84 | ### I have a feature request 85 | [Good stuff! Please create an issue!](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/issues)
86 | (features are more likely to be added the more users they seem to benefit) 87 | 88 | ### I want to discuss ASFDS or reach out to the developers, or other ASFDS users 89 | [The gitter page](https://gitter.im/OptimalBPM/angular-schema-form-dynamic-select) is good for when you want to talk, but perhaps doesn't feel that the discussion has to be indexed for posterity. 90 | 91 | 92 | # Glossary 93 | 94 | * List items: the items that make up the selection list, for example the items in a drop down. 95 | * ASFDS: Angular-Schema-Form-Dynamic-Select 96 | 97 | # Installation and usage 98 | 99 | ASFDS is an add-on to the angular-schema-form. To use it (in production), follow these steps: 100 | 101 | ### Dependencies 102 | Easiest way is to install is with bower, this will also include dependencies: 103 | 104 | ```bash 105 | $ bower install angular-schema-form-dynamic-select 106 | ``` 107 | 108 | If you want to use the develop branch: 109 | 110 | ```bash 111 | $ bower install angular-schema-form-dynamic-select#develop 112 | ``` 113 | 114 | \#develop is not recommended for production, but perhaps you want to use stuff from the next version in development. 115 | 116 | You can also use npm for installation: 117 | 118 | ```bash 119 | $ npm i angular-schema-form-dynamic-select 120 | ``` 121 | 122 | ### HTML 123 | Usage is straightforward, simply include and reference: 124 | ```html 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | ``` 138 | Note: Make sure you load angular-schema-form-dynamic-select.js **after** loading angular schema form. 139 | 140 | ### Configuring your angular module 141 | 142 | When you create your module, be sure to make it depend on mgcrea.ngStrap as well: 143 | ```js 144 | angular.module('yourModule', ['schemaForm', 'mgcrea.ngStrap']); 145 | ``` 146 | Note: Se the [ui-select dependencies](https://github.com/OptimalBPM/angular-schema-form-dynamic-select#ui-select) section for ui-select instructions 147 | 148 | # Form 149 | 150 | ASFDS is configured using form settings. There are no ASFDS-specific settings in the schema. 151 | 152 | This is to keep the schemas clean from UI-specific settings and kept usable anywhere in the solution and/or organization. 153 | 154 | ## Form types 155 | 156 | The add-on contributes the following new form types, `strapselect`, `uiselect`, `uiselectmulti`. 157 | 158 | The strapselect implements angular-strap-selects and uiselect* implements angular-ui-select. 159 | 160 | Built-in select-controls gets the bootstrap look but retain their functionality. 161 | 162 | 163 | ## Form Definition 164 | All settings reside in the form definition. See the [app.js](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/blob/master/app.js) file for this example in use. 165 | ```js 166 | $scope.form = [ 167 | ``` 168 | 169 | ### Single select from static list 170 | The drop down items are defined by and array of value/name objects residing in the form 171 | ```js 172 | { 173 | "key": 'select', 174 | "type": 'strapselect', 175 | "titleMap": [ 176 | {"value": 'value1', "name": 'text1'}, 177 | {"value": 'value2', "name": 'text2'}, 178 | {"value": 'value3', "name": 'text3'} 179 | ] 180 | }, 181 | ``` 182 | ### Multiple select from static list 183 | Like the above, but allows multiple items to be selected. 184 | ```js 185 | { 186 | "key": 'multiselect', 187 | "type": 'strapselect', 188 | "options": { 189 | "multiple": "true" 190 | } 191 | "titleMap": [ 192 | {"value": 'value1', "name": 'text1'}, 193 | {"value": 'value2', "name": 'text2'}, 194 | {"value": 'value3', "name": 'long very very long label3'} 195 | ] 196 | }, 197 | ``` 198 | ### Single select from dynamically loaded list via synchronous callback function 199 | Callback must return an array of value/name objects (see static list above). 200 | The "options" structure is passed to it as a parameter. 201 | ```js 202 | { 203 | "key": "selectDynamic", 204 | "type": 'strapselect', 205 | "options": { 206 | "callback": $scope.callBackSD 207 | } 208 | }, 209 | ``` 210 | For examples of how the different kinds of callbacks are implemented, please look at the [relevant code in app.js](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/blob/master/app.js#L18), 211 | 212 | ### Multiple select from dynamically loaded list via synchronous callback function 213 | Like strapselectdynamic above, but allowed multiple items to be selected. 214 | 215 | ```js 216 | { 217 | "key": "multiselectDynamic", 218 | "type": 'strapmultiselect', 219 | "options": { 220 | "multiple": "true" 221 | "callback": $scope.callBackMSD 222 | } 223 | }, 224 | ``` 225 | ### Multiple select from asynchronous callback 226 | 227 | The asyncCallback must return a *http-style promise* and the data the promise provides must be a JSON array of value/name objects. 228 | 229 | 230 | ```js 231 | { 232 | "key": "multiselectDynamicAsync", 233 | "type": 'strapselect', 234 | "options": { 235 | "multiple": "true" 236 | "asyncCallback": "callBackMSDAsync" 237 | } 238 | } 239 | }, 240 | ``` 241 | Note that in this example, the reference to the callback is a string, meaning a callback in the using controller scope. 242 | Also note, again, because this is a common misunderstanding, that asyncCallback should *not* return the array of items, but a http-promise, like the one $http.get()/$http.post() or [jquery's deferred.promise](https://api.jquery.com/deferred.promise/). 243 | Returning the array would be a synchronous operation, see "callback" above. 244 | 245 | ### Multiple select from dynamically loaded list via http get 246 | Convenience function, makes a get request, no need for callback. 247 | Expects the server to return a JSON array of value/name objects. 248 | ```js 249 | { 250 | "key": "multiselectDynamicHttpGet", 251 | "type": 'strapselect', 252 | "options": { 253 | "multiple": "true" 254 | "httpGet": { 255 | "url" : "test/testdata.json" 256 | } 257 | } 258 | }, 259 | ``` 260 | ### Multiple select from dynamically loaded list via http post with an options callback 261 | Like the get variant above function, but makes a JSON POST request passing the "parameter" as JSON.
262 | This example makes use of the optionsCallback property. 263 | It is a callback that like the others, gets the options structure 264 | as a parameter, but allows its content to be modified and returned for use in the call. 265 | Here, the otherwise mandatory httpPost.url is not set in the options but in the callback. 266 | 267 | See the [stringOptionsCallback function in app.js](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/blob/master/app.js#L46) for an example. 268 | The options-instance that is passed to the parameter is a *copy* of the instance in the form, 269 | so the form instance is not affected by any modifications by the callback. 270 | ```js 271 | { 272 | "key": "multiselectDynamicHttpPost", 273 | "type": 'strapselect', 274 | "options": { 275 | "multiple": "true" 276 | "httpPost": { 277 | "optionsCallback" : "stringOptionsCallback", 278 | "parameter": { "myparam" : "Hello"} 279 | } 280 | } 281 | }, 282 | ``` 283 | 284 | ### Property mapping 285 | The angular-schema-form titleMap naming standard is value/name, but that is sometimes difficult to get from a server, 286 | it might not support it. 287 | Therefore, a "map"-property is provided.
288 | The property in valueProperty says in what property to look for the value, and nameProperty the name. 289 | In this case: 290 | ```js 291 | {"nodeId" : 1, "nodeName": "Test", "nodeType": "99"} 292 | ``` 293 | which cannot be used, is converted into: 294 | ```js 295 | {"value" : 1, "name": "Test", "nodeId" : 1, nodeName: "Test", "nodeType": "99"} 296 | ``` 297 | which is the native format with the old options retained to not destroy auxiliary information. 298 | For example, a field like "nodeType" might be used for filtering(see Filters section, below). 299 | The options for that mapping look like this: 300 | ```js 301 | { 302 | "key": "multiselectdynamic_http_get", 303 | "type": "strapselect", 304 | "options": { 305 | "multiple": "true" 306 | "httpGet": { 307 | "url": "test/testdata_mapped.json" 308 | }, 309 | "map" : {"valueProperty": "nodeId", nameProperty: "nodeName"} 310 | } 311 | }, 312 | ``` 313 | The nameProperty can also be an array, in which case ASFDS looks for the first value. 314 | For example, in this case, one wants to first show the caption, and if that is not available, the name: 315 | 316 | ```js 317 | "map" : {"valueProperty": "nodeId", nameProperty: ["nodeCaption", "nodeName"]} 318 | ``` 319 | 320 | *For more complicated mappings, and situations where the source data is 321 | in a completely different format, the callback and asyncCallback options can be used instead.* 322 | 323 | ## Filters 324 | 325 | Filters, like [conditions](https://github.com/Textalk/angular-schema-form/blob/development/docs/index.md#standard-options), 326 | handle visibility, but for each item in the options list. 327 | 328 | It works by evaluating the filter expression for each row, if it evaluates to true, the option remains in the list. 329 | One could compare it with an SQL join. 330 | 331 | The options are: 332 | 333 | * filter : An expression, evaluated in the user scope, with the "item" local variable injected. "item" is the current list item, `"model.select==item.category"` 334 | * filterTrigger : An array of expressions triggering the filtering, `"model.select"` 335 | 336 | Example: 337 | ```js 338 | { 339 | "key": 'multiselect', 340 | "type": 'strapselect', 341 | options: { 342 | "multiple": "true" 343 | "filterTriggers": ["model.select"], 344 | "filter" : "model.select==item.category" 345 | }, 346 | "titleMap": [ 347 | {"value": 'value1', "name": 'text1', "category": "value1"}, 348 | {"value": 'value2', "name": 'text2', "category": "value1"}, 349 | {"value": 'value3', "name": 'long very very long label3'} 350 | ] 351 | }, 352 | ``` 353 | Note on filterTrigger and why not having a watch on the entire expression: 354 | 355 | * The expression is actually a one-to-many join, and mixes two scopes in the evaluation. This might not always be handled the same by $eval. 356 | * Adding watches for the expression would mean having to add one watch for each list item, long lists would mean a huge overhead. 357 | * Also, there might be use cases where triggering should be triggered by other conditions. Or not be triggered for some other reason. 358 | 359 | ## The ASFDS controller scope 360 | 361 | One usable property that is set by ASFDS is the options.scope-attribute. 362 | 363 | Its value is the scope of the controller, which provides far-reaching control over ASFDS behavior. 364 | 365 | In the [example](https://github.com/OptimalBPM/angular-schema-form-dynamic-select/blob/master/app.js), the multiselectDynamicAsync's 366 | onChange event is implemented so that another ASFDS controller is told to repopulate its select list items when the value is changed. 367 | This is valuable, for example, when there is too much data or for some other reason, filters are inappropriate. 368 | 369 | ## Defaults and enum 370 | If a there is a form item that only has type "string" defined, but has an enum value, then a single select will be shown for that value. 371 | ```js 372 | { 373 | "key": 'select' 374 | }, 375 | ``` 376 | The schema declaration(the enum values will be both value and name for the options): 377 | ```js 378 | select: { 379 | title: 'Single Select Static', 380 | type: 'string', 381 | enum: ["value1", "value2", "value3"], 382 | description: 'Only single item is allowed. Based on schema enum and form default.(change here and observe how the select list below is filtered)' 383 | }, 384 | ``` 385 | ## inlineMaxLength and inlineMaxLengthHtml angularStrap parameters. 386 | These settings affects only [strapselect](http://mgcrea.github.io/angular-strap/#/selects-usage) and controls the number of items that are shown in the selected list of items. 387 | If that list is full, the number of list items + the test in inlineMaxLengthHtml is shown. 388 | If, for example, inlineMaxLength is set to 2 and the number of selected items is 4, the text shown will be: 389 | 390 | `4 items are too many items to show....` 391 | 392 | Example(the same as in the example file): 393 | ```js 394 | "key": 'multiselect_overflow', 395 | "type": 'strapselect', 396 | "placeholder": "Please select some items.", 397 | "options": { 398 | "multiple": "true", 399 | "inlineMaxLength": "2", 400 | "inlineMaxLengthHtml": " items are too many items to show...." 401 | }, 402 | "titleMap": [ 403 | {"value": 'value1', "name": 'text1'}, 404 | {"value": 'value2', "name": 'text2'}, 405 | {"value": 'value3', "name": 'text3'}, 406 | {"value": 'value4', "name": 'text4'}, 407 | ] 408 | 409 | ``` 410 | ## Positioning the angularStrap select. 411 | The placement option can be used to position a [strapselect](http://mgcrea.github.io/angular-strap/#/selects). Possible values for placement are top, bottom, left, right, auto or any combination (e.g. bottom-right). 412 | Further details can be found in the [angularStrap select documentation](http://mgcrea.github.io/angular-strap/#/selects-usage). 413 | 414 | Example (the same as in the example file): 415 | ```js 416 | "key": "select_placement", 417 | "placeholder": "Please select from the right.", 418 | "options": { 419 | "placement": "right" 420 | } 421 | 422 | ``` 423 | ### And then a submit button. 424 | Not needed, of course, but is commonly used. 425 | ```js 426 | { 427 | type: "submit", 428 | style: "btn-info", 429 | title: "OK" 430 | } 431 | ``` 432 | And ending the form element array: 433 | ```js 434 | ]; 435 | ``` 436 | 437 | # Populating the list items 438 | 439 | The form.titleMap property in a form holds the list items(also in the dynamic variants). 440 | The name titleMap is the same as the built-in angular-schema-form select. 441 | 442 | ## Dynamically fetching list items 443 | These types are dynamic and fetches their data from different back ends. 444 | 445 | #### Callbacks in general 446 | Callbacks can be defined either by name(`"loadGroups"`) or absolute reference (`$scope.loadGroups`). 447 | 448 | The name is actually is an expression evaluated in the user scope that must return a function reference. 449 | This means that it *can* be `"getLoadFunctions('Groups')"`, as long as that returns a function reference. 450 | 451 | But the main reason for supporting referring to functions both by name and reference is that forms 452 | are often stored in a database and passed from the server to the client in [pure JSON format](http://stackoverflow.com/questions/2904131/what-is-the-difference-between-json-and-object-literal-notation), 453 | and there, `callback: $scope.loadGroups` is not allowed. 454 | 455 | #### Callback results 456 | The results of all callbacks can be remapped using the "map" property described above. 457 | All callbacks(also optionsCallback) has two parameters: 458 | * the options of the form, 459 | * if it is a UI-selects, the entered search value (see the UI-select example). 460 | 461 | The two kinds of callback mechanisms are: 462 | 463 | ### callback and asyncCallback 464 | 465 | * list items are fetched by a user-specified callback. The user implements the calling mechanism. 466 | * the callback receive the form options as a parameter and returns an array of list items(see the static strapselect) 467 | * asyncCallback implementations returns the data through a HttpPromise. NOT an array if items. 468 | 469 | *TIP: in an asyncCallback, you need to intercept and change an async server response before passing it on to the add-on, use the [transformResponse function](https://docs.angularjs.org/api/ng/service/$http#transforming-requests-and-responses).* 470 | 471 | ### httpGet and httpPost 472 | 473 | * list items are fetched using a built in async http mechanism, so that the user doesn't have to implement that. 474 | * the url property defines the URL to use. 475 | * the optional optionsCallback can be used to add to or change the options with information known in runtime. 476 | * httpPost-options has a "parameter"-property, that contains the JSON that will be POST:ed to the server. 477 | 478 | ### Handling errors from asynchronous callbacks 479 | For asyncCallback, httpGet and httpPost, there is an option, `onPopulationError`. 480 | 481 | If set to a callback function, and in case of a http error, the callback is called. 482 | Its parameters are: the form of the field(where they key and options are), the data and the status. 483 | 484 | See app.js for an example of its usage. Try and rename test/testdata.js and you'll see it being called. 485 | 486 | ## Statically setting the list items 487 | 488 | This is done by either using the JSON-schema enum-property, or by manually setting form.titleMap. 489 | 490 | # UI-Select 491 | The support for angular-ui-select was added in the 0.9.0-version, and is currently partial, but getting there. 492 | 493 | The currently supported UI-select specific/native options are: 494 | * Single: tagging, taggingTokens, taggingLabel, refreshDelay, searchDescription, uiClass 495 | * Multiple select: refreshDelay, uiClass, groupBy(only multiple) 496 | 497 | ## Installation 498 | 499 | UI-select is not installed by default in ASFDS, even though it is featured in the demo and example, here is how to make it work: 500 | 501 | ### Dependencies 502 | 503 | Its dependencies aren't included in the package.json, and will hence have to be installed manually, here is a script: 504 | 505 | ```bash 506 | $ bower install angular-ui-select angular-underscore underscore angular-ui-utils angular-translate angular-ui-select angular-ui-utils angular-sanitize 507 | ``` 508 | ### HTML 509 | Include all relevant files: 510 | ```html 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | ``` 520 | ### Angular module configuration 521 | 522 | UI-select have several additional dependencies that need to be added to your module configuration: 523 | 524 | ```bash 525 | angular.module('yourModule', ['schemaForm', 'mgcrea.ngStrap', 'mgcrea.ngStrap.modal', 'pascalprecht.translate', 'ui.select', 'ui.highlight','mgcrea.ngStrap.select']); 526 | ``` 527 | 528 | 529 | ### Forms 530 | It is used as strapselect, but by including the form types uiselect and uiselectmultiple instead. 531 | ```js 532 | { 533 | "key": 'uiselectmultiple', 534 | "type": 'uiselectmultiple', 535 | "titleMap": [ 536 | { value: 'one', name: 'option one'}, 537 | { value: 'two', name: 'option two'}, 538 | { value: 'three', name: 'option three'} 539 | ] 540 | }, 541 | ``` 542 | It supports dynamically fetching items from a backend using callbacks and http-methods, but works a little bit different from AngularStrap internally, so filters, for example, aren't implemented yet. 543 | 544 | See the example app in the source for more details on how to use it. 545 | 546 | # Recommendations 547 | 548 | * Choose httpGet and httpPost over the callback and asyncCallback methods if your don't specifically need the full freedom 549 | of callback and asyncCallback. There is no reason clutter client code with http-request handling unless you have to. 550 | * Given the asynchronous nature of javascript development, try use asynchronous alternatives before synchronous that block the UI. 551 | * The way the plug-ins works, they register themselves as defaults for all matching types.
552 | As long this is the case, all relevant fields must specify the "type"-property.
553 | If not, they will get the wrong editor. Either way, it is recommended to define the type. 554 | 555 | 556 | # Building 557 | 558 | Building and minifying is done using [gulp](http://gulpjs.com/) 559 | 560 | ### Installing gulp and requrements 561 | To install gulp, you need npm to be installer, however, we want a local bower install: 562 | ```bash 563 | sudo npm install bower 564 | node_modules/bower/bin/bower install 565 | ``` 566 | And then install the rest of the depencies 567 | ```bash 568 | sudo npm install 569 | ``` 570 | *The instructions are for Linux, to install under windows, the same commands adjusted for windows should work* 571 | 572 | ### Running the build 573 | 574 | In the project root folder, run: 575 | 576 | ```bash 577 | $ gulp default 578 | ``` 579 | 580 | # Contributing 581 | 582 | Pull requests are always very welcome. Try to make one for each thing you add, don't do [like this author(me) did](https://github.com/chengz/schema-form-strapselect/pull/2). 583 | 584 | Remember that the next version is in the develop branch, so if you want to add new features, do that there.
585 | If you want to fix a bug, do that against the master branch and it will be merged into the develop branch later. 586 | 587 | 588 | 589 | # Testing 590 | 591 | Unit testing is done using [Karma and Jasmine](http://karma-runner.github.io/0.12/intro/installation.html). 592 | The main configuration file for running tests is karma.conf.js, and test/tests.js holds the test code. 593 | First, make sure the relevant development dependencies are installed: 594 | 595 | ```bash 596 | $ npm update 597 | ``` 598 | 599 | To run the tests: 600 | 601 | 602 | ```bash 603 | $ node_modules/karma/bin/karma start karma.conf.js 604 | ``` 605 | # Breaking change history 606 | 607 | Important: Over the early minor versions, there has been considerable changes: 608 | 609 | * 0.3.0: all dynamic-select-related settings moved to the form. 610 | * 0.3.3: value/name-pairs for drop down data is deprecated.
611 | The correct way, and how the HTML select element actually works, is value/text.(note: Reverted in 0.8.0)
612 | The the add-on still supports both variants, but value/name will be removed.
613 | * 0.4.0: use the options.map functionality instead.
614 | * 0.5.0: Breaking changes: 615 | * http_post and http_get are renamed to httpPost and httpGet. 616 | * async.callback is removed and asyncCallback is used instead. 617 | * 0.6.0: earlier deprecated support for value/name-pairs is now removed 618 | * 0.7.0: meant a forced update of dependencies and some rewriting, since: 619 | * 2.2.1 of angular-strap has breaking changes making it impossible to keep backwards compatibility. 620 | * 0.8.0 of angular-schema-form, which also has breaking changes had to be updated to stay compatible with angular-straps' dependencies. 621 | * 0.8.0: Harmonization with angular-schema-form to be a drop-in replacement 622 | * Breaking change: The items array is now renamed to titleMap, as in ASF. 623 | * Value/name-pairs for drop-down data is now reintroduced (value/text is still supported) 624 | * 0.9.0: Breaking changes: strapselectdynamic, strapmultiselect and strapmultiselect was merged into strapselect. 625 | 626 | Note: no further API changes are planned. 627 | 628 | 629 | # History 630 | 631 | 1. This component was originally created by [chengz](https://github.com/chengz/). 632 | 633 | 2. [stevehu](https://github.com/stevehu) then added functionality to his project to connect to his [light 634 | framework](https://github.com/networknt/light). 635 | 636 | 3. This inspired [nicklasb](https://github.com/nicklasb) to merge stevehu:s code and rewrite the plugin in order to: 637 | 638 | * harmonize it with the current lookup handling in angular-schema-form 639 | * generalize it for it to be able to connect to any backend. 640 | 641 | The rest is extremely recent history(i.e. > 0.3.0). 642 | -------------------------------------------------------------------------------- /angular-schema-form-dynamic-select.js: -------------------------------------------------------------------------------- 1 | ;(function(root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['angular-schema-form'], factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(require('angular-schema-form')); 6 | } else { 7 | root.angularSchemaFormDynamicSelect = factory(root.schemaForm); 8 | } 9 | }(this, function(schemaForm) { 10 | angular.module("schemaForm").run(["$templateCache", function($templateCache) {$templateCache.put("directives/decorators/bootstrap/strap/strapmultiselect.html","
\n \n\n
\n \n {{ (hasError() && errorMessage(schemaError())) || form.description}}\n
\n
\n"); 11 | $templateCache.put("directives/decorators/bootstrap/strap/strapselect.html","
\n \n\n
\n \n \n {{ (hasError() && errorMessage(schemaError())) || form.description}} \n
\n
\n\n");}]); 12 | angular.module("schemaForm").run(["$templateCache", function($templateCache) {$templateCache.put("directives/decorators/bootstrap/uiselect/uiselect.html","
\n \n\n
\n \n \n {{select_model.selected.name}}\n \n \n
\n
\n \' + (\'\'+item.description | highlight: (form.options.searchDescriptions===true ? $select.search : \'NOTSEARCHINGFORTHIS\'))+ \'\'\">\n
\n
\n
\n \n \n {{select_model.selected.name}} \n {{(select_model.selected.isTag===true ? form.options.taggingLabel : \'\')}}\n \n \n \n
\' + (item.name | highlight: $select.search) + \' \' + form.options.taggingLabel + \'
\'\">
\n
\n
\n \' + (\'\'+item.description | highlight: (form.options.searchDescriptions===true ? $select.search : \'NOTSEARCHINGFORTHIS\')) + \'\'\">\n
\n \n \n\n \n\n \n \n {{select_model.selected.name}} \n {{(select_model.selected.isTag===true ? form.options.taggingLabel : \'\')}}\n \n \n
\' + (item.name | highlight: $select.search) + \' \' + form.options.taggingLabel + \'
\'\">
\n
\n
\n \' + (\'\'+item.description | highlight: (form.options.searchDescriptions===true ? $select.search : \'NOTSEARCHINGFORTHIS\')) + \'\'\">\n
\n \n \n\n \n\n \n\n
\n\n \n\n"); 13 | $templateCache.put("directives/decorators/bootstrap/uiselect/uiselectmultiple.html","\n
\n \n
\n \n {{$item.name}}\n \n
\n
\n
\n \n
\n
\n
\n");}]); 14 | angular.module('schemaForm').config( 15 | ['schemaFormProvider', 'schemaFormDecoratorsProvider', 'sfPathProvider', 16 | function (schemaFormProvider, schemaFormDecoratorsProvider, sfPathProvider) { 17 | 18 | var select = function (name, schema, options) { 19 | if ((schema.type === 'string') && ("enum" in schema)) { 20 | var f = schemaFormProvider.stdFormObj(name, schema, options); 21 | f.key = options.path; 22 | f.type = 'strapselect'; 23 | options.lookup[sfPathProvider.stringify(options.path)] = f; 24 | return f; 25 | } 26 | }; 27 | 28 | schemaFormProvider.defaults.string.unshift(select); 29 | 30 | //Add to the bootstrap directive 31 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapselect', 32 | 'directives/decorators/bootstrap/strap/strapselect.html'); 33 | 34 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapmultiselect', 35 | 'directives/decorators/bootstrap/strap/strapmultiselect.html'); 36 | 37 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapselectdynamic', 38 | 'directives/decorators/bootstrap/strap/strapselect.html'); 39 | 40 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapmultiselectdynamic', 41 | 'directives/decorators/bootstrap/strap/strapmultiselect.html'); 42 | 43 | 44 | // UI SELECT 45 | //Add to the bootstrap directive 46 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'uiselect', 47 | 'directives/decorators/bootstrap/uiselect/uiselect.html'); 48 | 49 | 50 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'uiselectmultiple', 51 | 'directives/decorators/bootstrap/uiselect/uiselectmultiple.html'); 52 | 53 | 54 | }]) 55 | .directive("toggleSingleModel", function() { 56 | // some how we get this to work ... 57 | return { 58 | require: 'ngModel', 59 | restrict: "A", 60 | scope: {}, 61 | replace: true, 62 | controller: ['$scope', function($scope) { 63 | $scope.$parent.$watch('select_model.selected',function(){ 64 | if($scope.$parent.select_model.selected != undefined) { 65 | $scope.$parent.insideModel = $scope.$parent.select_model.selected.value; 66 | $scope.$parent.ngModel.$setViewValue($scope.$parent.select_model.selected.value); 67 | } 68 | }); 69 | }] 70 | }; 71 | }) 72 | 73 | .directive('multipleOn', function() { 74 | return { 75 | link: function($scope, $element, $attrs) { 76 | $scope.$watch( 77 | function () { return $element.attr('multiple-on'); }, 78 | function (newVal) { 79 | 80 | if(newVal == "true") { 81 | var select_scope = angular.element($element).scope().$$childTail; 82 | select_scope.$isMultiple = true; 83 | select_scope.options.multiple = true; 84 | select_scope.$select.$element.addClass('select-multiple'); 85 | } 86 | else { 87 | angular.element($element).scope().$$childTail.$isMultiple = false; 88 | } 89 | } 90 | ); 91 | } 92 | }; 93 | }) 94 | .filter('whereMulti', function() { 95 | return function(items, key, values) { 96 | var out = []; 97 | 98 | if (angular.isArray(values) && items !== undefined) { 99 | values.forEach(function (value) { 100 | for (var i = 0; i < items.length; i++) { 101 | if (value == items[i][key]) { 102 | out.push(items[i]); 103 | break; 104 | } 105 | } 106 | }); 107 | } else { 108 | // Let the output be the input untouched 109 | out = items; 110 | } 111 | 112 | return out; 113 | }; 114 | }) 115 | .filter('propsFilter', function() { 116 | return function (items, props) { 117 | var out = []; 118 | 119 | if (angular.isArray(items)) { 120 | items.forEach(function (item) { 121 | var itemMatches = false; 122 | 123 | var keys = Object.keys(props); 124 | for (var i = 0; i < keys.length; i++) { 125 | var prop = keys[i]; 126 | if (item.hasOwnProperty(prop)) { 127 | //only match if this property is actually in the item to avoid 128 | var text = props[prop].toLowerCase(); 129 | //search for either a space before the text or the textg at the start of the string so that the middle of words are not matched 130 | if (item[prop].toString().toLowerCase().indexOf(text) === 0 || ( item[prop].toString()).toLowerCase().indexOf(' ' + text) !== -1) { 131 | itemMatches = true; 132 | break; 133 | } 134 | } 135 | } 136 | 137 | if (itemMatches) { 138 | out.push(item); 139 | } 140 | }); 141 | } else { 142 | // Let the output be the input untouched 143 | out = items; 144 | } 145 | 146 | return out; 147 | }; 148 | }); 149 | 150 | angular.module('schemaForm').controller('dynamicSelectController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { 151 | 152 | if (!$scope.form.options) { 153 | $scope.form.options = {}; 154 | } 155 | 156 | $scope.select_model = {}; 157 | 158 | console.log("Setting options." + $scope.form.options.toString()); 159 | $scope.form.options.scope = $scope; 160 | 161 | $scope.triggerTitleMap = function () { 162 | console.log("listener triggered"); 163 | // Ugly workaround to trigger titleMap expression re-evaluation so that the selectFilter it reapplied. 164 | $scope.form.titleMap.push({"value": "345890u340598u3405u9", "name": "34095u3p4ouij"}) 165 | $timeout(function () { $scope.form.titleMap.pop() }) 166 | 167 | }; 168 | 169 | $scope.initFiltering = function (localModel) { 170 | if ($scope.form.options.filterTriggers) { 171 | $scope.form.options.filterTriggers.forEach(function (trigger) { 172 | $scope.$parent.$watch(trigger, $scope.triggerTitleMap) 173 | 174 | }); 175 | } 176 | // This is set here, as the model value may become unitialized and typeless if validation fails. 177 | $scope.localModelType = Object.prototype.toString.call(localModel); 178 | $scope.filteringInitialized = true; 179 | }; 180 | 181 | 182 | $scope.finalizeTitleMap = function (form, data, newOptions) { 183 | // Remap the data 184 | 185 | form.titleMap = []; 186 | 187 | if (newOptions && "map" in newOptions && newOptions .map) { 188 | var current_row = null, 189 | final = newOptions.map.nameProperty.length - 1, 190 | separator = newOptions.map.separatorValue ? newOptions.map.separatorValue : ' - '; 191 | data.forEach(function (current_row) { 192 | current_row["value"] = current_row[newOptions .map.valueProperty]; 193 | //check if the value passed is a string or not 194 | if(typeof newOptions.map.nameProperty != 'string'){ 195 | //loop through the object/array 196 | var newName = ""; 197 | for (var i in newOptions.map.nameProperty) { 198 | newName += current_row[newOptions .map.nameProperty[i]]; 199 | if(i != final){newName += separator}; 200 | } 201 | current_row["name"] = newName; //init the 'name' property 202 | } 203 | else{ 204 | //if it is a string 205 | current_row["name"] = current_row[newOptions .map.nameProperty]; 206 | } 207 | form.titleMap.push(current_row); 208 | }); 209 | 210 | } 211 | else { 212 | data.forEach(function (item) { 213 | if ("text" in item) { 214 | item.name = item.text 215 | } 216 | } 217 | ); 218 | form.titleMap = data; 219 | } 220 | 221 | if ($scope.insideModel && $scope.select_model.selected === undefined) { 222 | $scope.select_model.selected = $scope.find_in_titleMap($scope.insideModel).item; 223 | } 224 | 225 | // The ui-selects needs to be reinitialized (UI select sets the internalModel and externalModel. 226 | if ($scope.internalModel) { 227 | console.log("Call uiMultiSelectInitInternalModel"); 228 | $scope.uiMultiSelectInitInternalModel($scope.externalModel); 229 | } 230 | }; 231 | 232 | $scope.clone = function (obj) { 233 | // Clone an object (except references to this scope) 234 | if (null == obj || "object" != typeof(obj)) return obj; 235 | 236 | var copy = obj.constructor(); 237 | for (var attr in obj) { 238 | // Do not clone if it is this scope 239 | if (obj[attr] != $scope) { 240 | if (obj.hasOwnProperty(attr)) copy[attr] = $scope.clone(obj[attr]); 241 | } 242 | } 243 | return copy; 244 | }; 245 | 246 | 247 | $scope.getCallback = function (callback) { 248 | if (typeof(callback) == "string") { 249 | var _result = $scope.$parent.evalExpr(callback); 250 | if (typeof(_result) == "function") { 251 | return _result; 252 | } 253 | else { 254 | throw("A callback string must match name of a function in the parent scope") 255 | } 256 | 257 | } 258 | else if (typeof(callback) == "function") { 259 | return callback; 260 | } 261 | else { 262 | throw("A callback must either be a string matching the name of a function in the parent scope or a " + 263 | "direct function reference") 264 | 265 | } 266 | }; 267 | 268 | $scope.getOptions = function (options, search) { 269 | // If defined, let the a callback function manipulate the options 270 | if (options.httpPost && options.httpPost.optionsCallback) { 271 | newOptionInstance = $scope.clone(options); 272 | return $scope.getCallback(options.httpPost.optionsCallback)(newOptionInstance, search); 273 | } 274 | if (options.httpGet && options.httpGet.optionsCallback) { 275 | newOptionInstance = $scope.clone(options); 276 | return $scope.getCallback(options.httpGet.optionsCallback)(newOptionInstance, search); 277 | } 278 | else { 279 | return options; 280 | } 281 | }; 282 | 283 | $scope.test = function (form) { 284 | form.titleMap.pop(); 285 | }; 286 | 287 | 288 | $scope.populateTitleMap = function (form, search) { 289 | 290 | if (form.schema && "enum" in form.schema) { 291 | form.titleMap = []; 292 | form.schema.enum.forEach(function (item) { 293 | form.titleMap.push({"value": item, "name": item}) 294 | } 295 | ); 296 | 297 | } 298 | else if (!form.options) { 299 | 300 | console.log("dynamicSelectController.populateTitleMap(key:" + form.key + ") : No options set, needed for dynamic selects"); 301 | } 302 | else if (form.options.callback) { 303 | form.titleMap = $scope.getCallback(form.options.callback)(form.options, search); 304 | $scope.finalizeTitleMap(form,form.titleMap, form.options); 305 | console.log("callback items: ", form.titleMap); 306 | } 307 | else if (form.options.asyncCallback) { 308 | return $scope.getCallback(form.options.asyncCallback)(form.options, search).then( 309 | function (_data) { 310 | // In order to work with both $http and generic promises 311 | _data = _data.data || _data; 312 | $scope.finalizeTitleMap(form, _data, form.options); 313 | console.log('asyncCallback items', form.titleMap); 314 | }, 315 | function (data, status) { 316 | if (form.options.onPopulationError) { 317 | $scope.getCallback(form.options.onPopulationError)(form, data, status); 318 | } 319 | else { 320 | alert("Loading select items failed(Options: '" + String(form.options) + 321 | "\nError: " + status); 322 | } 323 | }); 324 | } 325 | else if (form.options.httpPost) { 326 | var finalOptions = $scope.getOptions(form.options, search); 327 | 328 | return $http.post(finalOptions.httpPost.url, finalOptions.httpPost.parameter).then( 329 | function (_data) { 330 | 331 | $scope.finalizeTitleMap(form, _data.data, finalOptions); 332 | console.log('httpPost items', form.titleMap); 333 | }, 334 | function (data, status) { 335 | if (form.options.onPopulationError) { 336 | $scope.getCallback(form.options.onPopulationError)(form, data, status); 337 | } 338 | else { 339 | alert("Loading select items failed (URL: '" + String(finalOptions.httpPost.url) + 340 | "' Parameter: " + String(finalOptions.httpPost.parameter) + "\nError: " + status); 341 | } 342 | }); 343 | } 344 | else if (form.options.httpGet) { 345 | var finalOptions = $scope.getOptions(form.options, search); 346 | return $http.get(finalOptions.httpGet.url, finalOptions.httpGet.parameter).then( 347 | function (data) { 348 | $scope.finalizeTitleMap(form, data.data, finalOptions); 349 | console.log('httpGet items', form.titleMap); 350 | }, 351 | function (data, status) { 352 | if (form.options.onPopulationError) { 353 | $scope.getCallback(form.options.onPopulationError)(form, data, status); 354 | } 355 | else { 356 | alert("Loading select items failed (URL: '" + String(finalOptions.httpGet.url) + 357 | "\nError: " + status); 358 | } 359 | }); 360 | } 361 | else { 362 | if ($scope.insideModel && $scope.select_model.selected === undefined) { 363 | $scope.select_model.selected = $scope.find_in_titleMap($scope.insideModel); 364 | } 365 | } 366 | }; 367 | 368 | 369 | $scope.find_in_titleMap = function (value) { 370 | for (i = 0; i < $scope.form.titleMap.length; i++) { 371 | if ($scope.form.titleMap[i].value == value) { 372 | return {"item": $scope.form.titleMap[i], "index": i}; 373 | } 374 | } 375 | 376 | }; 377 | 378 | $scope.uiMultiSelectInitInternalModel = function(supplied_model) 379 | { 380 | 381 | 382 | console.log("$scope.externalModel: Key: " +$scope.form.key.toString() + " Model: " + supplied_model.toString()); 383 | $scope.externalModel = supplied_model; 384 | $scope.internalModel = []; 385 | if ($scope.form.titleMap) { 386 | if (supplied_model !== undefined && angular.isArray(supplied_model)){ 387 | supplied_model.forEach(function (value) { 388 | titleMap_item = $scope.find_in_titleMap(value); 389 | $scope.internalModel.push(titleMap_item.item); 390 | $scope.form.titleMap.splice(titleMap_item.index, 1); 391 | } 392 | ) 393 | } 394 | } 395 | }; 396 | 397 | }]); 398 | 399 | angular.module('schemaForm').filter('selectFilter', [function ($filter) { 400 | return function (inputArray, controller, localModel, strLocalModel) { 401 | // As the controllers' .model is the global and its form is the local, we need to get the local model as well. 402 | // We also need tp be able to set it if is undefined after a validation failure,so for that we need 403 | // its string representation as well as we do not know its name. A typical value if strLocalModel is model['groups'] 404 | // This is very ugly, though. TODO: Find out why the model is set to undefined after validation failure. 405 | 406 | if (!angular.isDefined(inputArray) || !angular.isDefined(controller.form.options) || 407 | !angular.isDefined(controller.form.options.filter) || controller.form.options.filter == '') { 408 | return inputArray; 409 | } 410 | 411 | 412 | 413 | console.log("----- In filtering for " + controller.form.key + "(" + controller.form.title +"), model value: " + JSON.stringify( localModel) + "----"); 414 | console.log("Filter:" + controller.form.options.filter); 415 | if (!controller.filteringInitialized) { 416 | console.log("Initialize filter"); 417 | controller.initFiltering(localModel); 418 | } 419 | 420 | 421 | var data = []; 422 | 423 | 424 | angular.forEach(inputArray, function (curr_item) { 425 | //console.log("Compare: curr_item: " + JSON.stringify(curr_item) + 426 | //"with : " + JSON.stringify( controller.$eval(controller.form.options.filterTriggers[0]))); 427 | if (controller.$eval(controller.form.options.filter, {item: curr_item})) { 428 | data.push(curr_item); 429 | } 430 | else if (localModel) { 431 | // If not in list, also remove the set value 432 | 433 | if (controller.localModelType == "[object Array]" && localModel.indexOf(curr_item.value) > -1) { 434 | localModel.splice(localModel.indexOf(curr_item.value), 1); 435 | } 436 | else if (localModel == curr_item.value) { 437 | console.log("Setting model of type " + controller.localModelType + "to null."); 438 | localModel = null; 439 | } 440 | } 441 | }); 442 | 443 | if (controller.localModelType == "[object Array]" && !localModel) { 444 | // An undefined local model seems to mess up bootstrap select's indicators 445 | console.log("Resetting model of type " + controller.localModelType + " to []."); 446 | 447 | controller.$eval(strLocalModel + "=[]"); 448 | } 449 | 450 | //console.log("Input: " + JSON.stringify(inputArray)); 451 | //console.log("Output: " + JSON.stringify(data)); 452 | //console.log("Model value out : " + JSON.stringify(localModel)); 453 | console.log("----- Exiting filter for " + controller.form.title + "-----"); 454 | 455 | return data; 456 | }; 457 | }]); 458 | 459 | })); 460 | -------------------------------------------------------------------------------- /angular-schema-form-dynamic-select.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"function"==typeof define&&define.amd?define(["angular-schema-form"],t):"object"==typeof exports?module.exports=t(require("angular-schema-form")):e.angularSchemaFormDynamicSelect=t(e.schemaForm)}(this,function(e){return angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/strap/strapmultiselect.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
'),e.put("directives/decorators/bootstrap/strap/strapselect.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
'),e.put("directives/decorators/bootstrap/strap/uiselect.html",'
{{select_model.selected.name}}
{{select_model.selected.name}}  {{(select_model.selected.isTag===true ? form.options.taggingLabel : \'\')}}
{{select_model.selected.name}}  {{(select_model.selected.isTag===true ? form.options.taggingLabel : \'\')}}
'),e.put("directives/decorators/bootstrap/strap/uiselectmultiple.html",'
{{$item.name}}
')}]),angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/strap/strapmultiselect.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
'),e.put("directives/decorators/bootstrap/strap/strapselect.html",'
{{ (hasError() && errorMessage(schemaError())) || form.description}}
')}]),angular.module("schemaForm").run(["$templateCache",function(e){e.put("directives/decorators/bootstrap/uiselect/uiselect.html",'
{{select_model.selected.name}}
{{select_model.selected.name}}  {{(select_model.selected.isTag===true ? form.options.taggingLabel : \'\')}}
{{select_model.selected.name}}  {{(select_model.selected.isTag===true ? form.options.taggingLabel : \'\')}}
'),e.put("directives/decorators/bootstrap/uiselect/uiselectmultiple.html",'
{{$item.name}}
')}]),angular.module("schemaForm").config(["schemaFormProvider","schemaFormDecoratorsProvider","sfPathProvider",function(e,t,o){var l=function(t,l,s){if("string"===l.type&&"enum"in l){var a=e.stdFormObj(t,l,s);return a.key=s.path,a.type="strapselect",s.lookup[o.stringify(s.path)]=a,a}};e.defaults.string.unshift(l),t.addMapping("bootstrapDecorator","strapselect","directives/decorators/bootstrap/strap/strapselect.html"),t.addMapping("bootstrapDecorator","strapmultiselect","directives/decorators/bootstrap/strap/strapmultiselect.html"),t.addMapping("bootstrapDecorator","strapselectdynamic","directives/decorators/bootstrap/strap/strapselect.html"),t.addMapping("bootstrapDecorator","strapmultiselectdynamic","directives/decorators/bootstrap/strap/strapmultiselect.html"),t.addMapping("bootstrapDecorator","uiselect","directives/decorators/bootstrap/uiselect/uiselect.html"),t.addMapping("bootstrapDecorator","uiselectmultiple","directives/decorators/bootstrap/uiselect/uiselectmultiple.html")}]).directive("toggleSingleModel",function(){return{require:"ngModel",restrict:"A",scope:{},replace:!0,controller:["$scope",function(e){e.$parent.$watch("select_model.selected",function(){void 0!=e.$parent.select_model.selected&&(e.$parent.insideModel=e.$parent.select_model.selected.value,e.$parent.ngModel.$setViewValue(e.$parent.select_model.selected.value))})}]}}).directive("multipleOn",function(){return{link:function(e,t,o){e.$watch(function(){return t.attr("multiple-on")},function(e){if("true"==e){var o=angular.element(t).scope().$$childTail;o.$isMultiple=!0,o.options.multiple=!0,o.$select.$element.addClass("select-multiple")}else angular.element(t).scope().$$childTail.$isMultiple=!1})}}}).filter("whereMulti",function(){return function(e,t,o){var l=[];return angular.isArray(o)&&void 0!==e?o.forEach(function(o){for(var s=0;s-1?o.splice(o.indexOf(e.value),1):o==e.value&&(console.log("Setting model of type "+t.localModelType+"to null."),o=null))}),"[object Array]"!=t.localModelType||o||(console.log("Resetting model of type "+t.localModelType+" to []."),t.$eval(l+"=[]")),console.log("----- Exiting filter for "+t.form.title+"-----"),s}}]),angularSchemaFormDynamicSelect}); -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | .tilted { 2 | 3 | /* Safari, Chrome */ 4 | -webkit-transform: rotate(-13deg); 5 | 6 | /* Firefox */ 7 | -moz-transform: rotate(-1deg); 8 | 9 | /* IE */ 10 | -ms-transform: rotate(-1deg); 11 | 12 | /* Opera */ 13 | -o-transform: rotate(-1deg); 14 | 15 | /* Older versions of IE */ 16 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0.012); 17 | 18 | /* CSS3 standard as defined here: http://www.w3.org/TR/css3-transforms/ */ 19 | transform: rotate(-1.2deg); 20 | 21 | padding-bottom: 1em; 22 | 23 | } 24 | 25 | .bigger { 26 | font-size: larger; 27 | 28 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | "use strict"; 3 | 4 | /** 5 | * The main app module 6 | * @name testApp 7 | * @type {angular.Module} 8 | */ 9 | 10 | var testApp = angular.module("testApp", ["schemaForm", "mgcrea.ngStrap", "mgcrea.ngStrap.modal", 11 | "pascalprecht.translate", "ui.select", "mgcrea.ngStrap.select" 12 | 13 | ]); 14 | 15 | testApp.controller("appController", ["$scope", "$http", function ($scope, $http) { 16 | 17 | $scope.callBackSD = function (options, search) { 18 | if (search) { 19 | console.log("Here the select lis could be narrowed using the search value: " + search.toString()); 20 | return [ 21 | {value: "value1", name: "text1"}, 22 | {value: "value2", name: "text2"}, 23 | {value: "value3", name: "Select dynamic!"} 24 | ].filter(function (item) { 25 | return (item.name.search(search) > -1) 26 | }); 27 | } 28 | else { 29 | return [ 30 | {value: "value1", name: "text1"}, 31 | {value: "value2", name: "text2"}, 32 | {value: "value3", name: "Select dynamic!"} 33 | ]; 34 | 35 | } 36 | // Note: Options is a reference to the original instance, if you change a value, 37 | // that change will persist when you use this form instance again. 38 | }; 39 | $scope.callBackUI = function (options) { 40 | return [ 41 | {"value": "value1", "name": "text1", "category": "value1"}, 42 | {"value": "value2", "name": "text2", "category": "value1"}, 43 | {"value": "value3", "name": "So this is the next item", "category": "value2"}, 44 | {"value": "value4", "name": "The last item", "category": "value1"} 45 | ]; 46 | // Note: Options is a reference to the original instance, if you change a value, 47 | // that change will persist when you use this form instance again. 48 | }; 49 | $scope.callBackMSD = function (options) { 50 | return [ 51 | {value: "value1", name: "text1"}, 52 | {value: "value2", name: "text2"}, 53 | {value: "value3", name: "Multiple select dynamic!"} 54 | ]; 55 | // Note: Options is a reference to the original instance, if you change a value, 56 | // that change will persist when you use this form instance again. 57 | }; 58 | 59 | $scope.callBackMSDAsync = function (options) { 60 | // Note that we got the url from the options. Not necessary, but then the same callback function can be used 61 | // by different selects with different parameters. 62 | 63 | // The asynchronous function must always return a httpPromise 64 | return $http.get(options.urlOrWhateverOptionIWant); 65 | }; 66 | 67 | $scope.stringOptionsCallback = function (options) { 68 | // Here you can manipulate the form options used in a http_post or http_get 69 | // For example, you can use variables to build the URL or set the parameters, here we just set the url. 70 | options.httpPost.url = "test/testdata.json"; 71 | // Note: This is a copy of the form options, edits here will not persist but are only used in this request. 72 | return options; 73 | }; 74 | 75 | $scope.onPopulationError = function (form, data, status) { 76 | console.log("An error occurred when the " + form.key + "-fields drop down was to be populated! \n") 77 | console.log("The data: " + data.data.toString()); 78 | console.log("The status: " + status); 79 | }; 80 | 81 | $scope.schema = { 82 | type: "object", 83 | title: "Select", 84 | properties: { 85 | select: { 86 | title: "Single select strap-select", 87 | type: "string", 88 | enum: ["value1", "value2", "value3"], 89 | description: "Only single item is allowed. Based on schema enum and form default. Change here and observe how the select list below is filtered." 90 | }, 91 | multiselect: { 92 | title: "Multi select strap-select", 93 | type: "array", 94 | items: {type: "string"}, 95 | maxItems: 2, 96 | description: "Multiple items are allowed, select three for maxItems validation error. Each item belongs to a \"category\", so the list is filtered depending on what you have selected in the \"Single select strap-select\" above." 97 | }, 98 | uiselect: { 99 | title: "Single select for UI-select", 100 | type: "string", 101 | description: "This one is using UI-select, single selection. Fetches lookup values(titleMap) from a callback." 102 | }, 103 | uiselectmultiple: { 104 | title: "Multi select for UI-select", 105 | type: "array", 106 | items: {type: "integer"}, 107 | description: "This one is using UI-select, allows multiple selection. From a callback." 108 | }, 109 | selectDynamic: { 110 | title: "Single Select Dynamic", 111 | type: "string", 112 | description: "This titleMap is loaded from the $scope.callBackSD function. (and laid out using css-options)" 113 | }, 114 | multiselectDynamic: { 115 | title: "Multi Select Dynamic", 116 | type: "array", 117 | items: {type: "string"}, 118 | description: "This titleMap is loaded from the $scope.callBackMSD function. (referenced by name)" 119 | }, 120 | multiselectDynamicHttpPost: { 121 | title: "Multi Select Dynamic HTTP Post", 122 | type: "array", 123 | items: {type: "string"}, 124 | description: "This titleMap is asynchronously loaded using a HTTP post. " + 125 | "(specifies parameter in form, options.url in a named callback)" 126 | }, 127 | multiselectDynamicHttpGet: { 128 | title: "Multi Select Dynamic HTTP Get", 129 | type: "array", 130 | items: {type: "string"}, 131 | description: "This titleMap is asynchronously loaded using a HTTP get. " + 132 | "(Set the URL at options.url)" 133 | }, 134 | multiselectDynamicHttpGetMapped: { 135 | title: "Multi Select Dynamic HTTP Get Mapped data", 136 | type: "array", 137 | items: {type: "string"}, 138 | description: "This titleMap is as above, but remapped from a nodeId/nodeName array of objects. " + 139 | "(See app.js: \"map\" : {valueProperty: \"nodeId\", textProperty: \"nodeName\"})" 140 | }, 141 | multiselectDynamicHttpGetMappedArray: { 142 | title: "Multi Select Dynamic HTTP Get Mapped data using array", 143 | type: "array", 144 | items: {type: "string"}, 145 | description: "This titleMap is as above, but remapped from a nodeId/nodeName/category array of objects" + 146 | " with an optional separator and using the first existing value for the nameProperty (similar to COALESCE in sql server)." + 147 | "(See app.js: \"map\" : {valueProperty: \"nodeId\", nameProperty: [\"nodeName\",\"category\"], separatorValue: \" | \"})" 148 | }, 149 | multiselectDynamicAsync: { 150 | title: "Multi Select Dynamic Async", 151 | type: "array", 152 | items: {type: "string"}, 153 | description: "This titleMap is asynchrously loaded using options.async.call and implements the onChange event. " 154 | }, 155 | multiselect_overflow: { 156 | title: "Strap select with overflow", 157 | type: "array", 158 | items: {type: "string"}, 159 | description: "If you select more than two items here, it will only show the first two and " 160 | }, 161 | select_placement: { 162 | title: "Strap select with position set to right", 163 | type: "string", 164 | enum: ["value1", "value2", "value3"], 165 | description: "Position of the select set using the placement option." 166 | }, 167 | "priorities": { 168 | "type": "object", 169 | "properties": { 170 | "priority": { 171 | "type": "array", 172 | "items": { 173 | "type": "object", 174 | "properties": { 175 | "value": { 176 | "type": "string", 177 | "enum": ["DOG", "CAT", "FISH"] 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | }, 186 | required: ["select", "multiselect"] 187 | }; 188 | 189 | $scope.form = [ 190 | 191 | { 192 | "key": "select" 193 | }, 194 | { 195 | "key": "multiselect", 196 | "type": "strapselect", 197 | "placeholder": "My items feel unselected. Or you selected text3 in the selector above me.", 198 | "options": { 199 | "multiple": "true", 200 | "filterTriggers": ["model.select"], 201 | "filter": "item.category.indexOf(model.select) > -1" 202 | }, 203 | "validationMessage": "Hey, you can only select three items or you'll see this!", 204 | "titleMap": [ 205 | {"value": "value1", "name": "text1 (belongs to the value1-category)", "category": "value1"}, 206 | {"value": "value2", "name": "text2 (belongs to the value1-category)", "category": "value1"}, 207 | { 208 | "value": "value3", 209 | "name": "long very very long label3 (belongs to the value2-category)", 210 | "category": "value2" 211 | }, 212 | { 213 | "value": "value4", 214 | "name": "Select three and get a validation error! (belongs to the value1-category)", 215 | "category": "value1" 216 | } 217 | ] 218 | }, 219 | { 220 | "key": "uiselect", 221 | "type": "uiselect", 222 | "placeholder": "not set yet..", 223 | "options": { 224 | "callback": "callBackSD" 225 | } 226 | }, 227 | 228 | { 229 | "key": "uiselectmultiple", 230 | "type": "uiselectmultiple", 231 | "placeholder": "not set yet..", 232 | "options": { 233 | "callback": "callBackUI" 234 | } 235 | }, 236 | { 237 | "key": "selectDynamic", 238 | "type": "strapselect", 239 | "htmlClass": "col-lg-3 col-md-3", 240 | "labelHtmlClass": "bigger", 241 | "fieldHtmlClass": "tilted", 242 | "options": { 243 | "callback": $scope.callBackSD 244 | } 245 | }, 246 | { 247 | "key": "multiselectDynamic", 248 | "type": "strapselect", 249 | placeholder: "not set yet(this text is defined using the placeholder option)", 250 | "options": { 251 | "multiple": "true", 252 | "callback": "callBackMSD" 253 | } 254 | }, 255 | { 256 | "key": "multiselectDynamicHttpPost", 257 | "type": "strapselect", 258 | "title": "Multi Select Dynamic HTTP Post (title is from form.options, overriding the schema.title)", 259 | "options": { 260 | "multiple": "true", 261 | "httpPost": { 262 | "optionsCallback": "stringOptionsCallback", 263 | "parameter": {"myparam": "Hello"} 264 | } 265 | } 266 | }, 267 | { 268 | "key": "multiselectDynamicHttpGet", 269 | "type": "strapselect", 270 | "placeholder": "None selected here neither.", 271 | "options": { 272 | "multiple": "true", 273 | "httpGet": { 274 | "url": "test/testdata.json" 275 | } 276 | } 277 | }, 278 | { 279 | "key": "multiselectDynamicHttpGetMapped", 280 | "type": "strapselect", 281 | "placeholder": "And even less here...", 282 | "options": { 283 | "multiple": "true", 284 | "httpGet": { 285 | "url": "test/testdata_mapped.json" 286 | }, 287 | "map": {valueProperty: "nodeId", nameProperty: "nodeName"} 288 | } 289 | }, 290 | { 291 | "key": "multiselectDynamicHttpGetMappedArray", 292 | "type": "strapselect", 293 | "placeholder": "And even less here...", 294 | "options": { 295 | "multiple": "true", 296 | "httpGet": { 297 | "url": "test/testdata_mapped.json" 298 | }, 299 | "map": {valueProperty: "nodeId", nameProperty: ["nodeName","category"], separatorValue: " | "} 300 | } 301 | }, 302 | { 303 | "key": "multiselectDynamicAsync", 304 | "type": "strapselect", 305 | "onChange": function (modelValue, form) { 306 | $scope.form.forEach(function (item) { 307 | if (item.key == "multiselectDynamicHttpGet") { 308 | item.options.scope.populateTitleMap(item); 309 | } 310 | }); 311 | alert("onChange happened!\nYou changed this value into " + modelValue + " !\nThen code in this event cause the multiselectDynamicHttpGet to reload. \nSee the ASF onChange event for info."); 312 | 313 | 314 | }, 315 | "options": { 316 | "multiple": "true", 317 | "asyncCallback": $scope.callBackMSDAsync, 318 | "onPopulationError": "onPopulationError", 319 | "urlOrWhateverOptionIWant": "test/testdata.json" 320 | } 321 | }, 322 | { 323 | "key": "multiselect_overflow", 324 | "type": "strapselect", 325 | "placeholder": "Please select some items.", 326 | "options": { 327 | "multiple": "true", 328 | "inlineMaxLength": "2", 329 | "inlineMaxLengthHtml": "Too many items to show...." 330 | }, 331 | "titleMap": [ 332 | {"value": "value1", "name": "text1"}, 333 | {"value": "value2", "name": "text2"}, 334 | {"value": "value3", "name": "text3"}, 335 | {"value": "value4", "name": "text4"} 336 | ] 337 | }, 338 | { 339 | "key": "select_placement", 340 | "placeholder": "Please select from the right.", 341 | "options": { 342 | "placement": "right" 343 | } 344 | }, 345 | { 346 | "key": "priorities.priority", 347 | "title": "Array inside an object, defaults ASF select only", 348 | "description": "This is an example of how to use this in a complex structure. Note that the title and description is in the form, ASF only looks in the form for that.", 349 | "type": "array", 350 | "items": [ 351 | { 352 | "key": "priorities.priority[].value", 353 | "type": "strapselect" 354 | } 355 | ] 356 | }, 357 | { 358 | type: "submit", 359 | style: "btn-info", 360 | title: "OK" 361 | } 362 | 363 | ]; 364 | $scope.model = {}; 365 | $scope.model.select = "value1"; 366 | $scope.model.multiselect = ["value2", "value1"]; 367 | $scope.model.uiselect = "value1"; 368 | $scope.model.uiselectmultiple = ["value1", "value2"]; 369 | 370 | 371 | $scope.model.priorities = { 372 | "priority": [ 373 | { 374 | "value": "DOG" 375 | }, 376 | { 377 | "value": "DOG" 378 | }, 379 | { 380 | "value": "FISH" 381 | } 382 | ] 383 | }; 384 | 385 | $scope.submitted = function (form) { 386 | $scope.$broadcast("schemaFormValidate"); 387 | console.log($scope.model); 388 | }; 389 | }]) 390 | ; 391 | 392 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-schema-form-dynamic-select", 3 | "description": "Dynamic select add-on for angular schema form.", 4 | "main": [ 5 | "angular-schema-form-dynamic-select.min.js" 6 | ], 7 | "version": "0.15.0", 8 | "authors": [ 9 | "Cheng Zhu", 10 | "Stevehu", 11 | "Nicklas Börjesson" 12 | ], 13 | "moduleType": [ 14 | "globals" 15 | ], 16 | "keywords": [ 17 | "angular-schema-form", 18 | "angular-schema-form-add-on", 19 | "schema-form", 20 | "form", 21 | "json", 22 | "json-schema", 23 | "schema" 24 | ], 25 | "license": "MIT", 26 | "ignore": [ 27 | "**/.*", 28 | "node_modules", 29 | "bower_components", 30 | "test", 31 | "coverage" 32 | ], 33 | "dependencies": { 34 | "angular-schema-form": "^0", 35 | "angular-schema-form-bootstrap": "^0", 36 | "angular-strap": "^2", 37 | "bootstrap": "^3", 38 | "angular-ui-select": "^0" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/OptimalBPM/angular-schema-form-dynamic-select.git" 43 | }, 44 | "devDependencies": { 45 | "angular-translate": ">= 2.4.0", 46 | "angular-mocks": ">= 1.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | 3 | var gulp = require('gulp'); 4 | var webserver = require('gulp-webserver'); 5 | 6 | var templateCache = require('gulp-angular-templatecache'); 7 | var minifyHtml = require('gulp-minify-html'); 8 | var concat = require('gulp-concat'); 9 | var uglify = require('gulp-uglify'); 10 | var streamqueue = require('streamqueue'); 11 | var jscs = require('gulp-jscs'); 12 | var umd = require('gulp-umd'); 13 | 14 | gulp.task('minify', function() { 15 | var stream = streamqueue({objectMode: true}); 16 | stream.queue( 17 | gulp.src('./src/*.html') 18 | .pipe(minifyHtml({ 19 | empty: true, 20 | spare: true, 21 | quotes: true 22 | })) 23 | .pipe(templateCache({ 24 | module: 'schemaForm', 25 | root: 'directives/decorators/bootstrap/strap/' 26 | })) 27 | ); 28 | 29 | stream.queue( 30 | gulp.src('./src/strap*.html') 31 | .pipe(minifyHtml({ 32 | empty: true, 33 | spare: true, 34 | quotes: true 35 | })) 36 | .pipe(templateCache({ 37 | module: 'schemaForm', 38 | root: 'directives/decorators/bootstrap/strap/' 39 | })) 40 | ); 41 | stream.queue( 42 | gulp.src('./src/ui*.html') 43 | .pipe(minifyHtml({ 44 | empty: true, 45 | spare: true, 46 | quotes: true 47 | })) 48 | .pipe(templateCache({ 49 | module: 'schemaForm', 50 | root: 'directives/decorators/bootstrap/uiselect/' 51 | })) 52 | ); 53 | stream.queue(gulp.src('./src/*.js')); 54 | 55 | stream.done() 56 | .pipe(concat('angular-schema-form-dynamic-select.min.js')) 57 | .pipe(umd({ 58 | dependencies: function() { 59 | return [ 60 | {name: 'schemaForm', 61 | amd:"angular-schema-form", 62 | cjs: 'angular-schema-form'}, 63 | ]; 64 | }, 65 | exports: function() {return 'angularSchemaFormDynamicSelect';}, 66 | namespace: function() {return 'angularSchemaFormDynamicSelect';} 67 | })) 68 | .pipe(uglify()) 69 | .pipe(gulp.dest('.')); 70 | 71 | }); 72 | 73 | gulp.task('non-minified-dist', function() { 74 | var stream = streamqueue({objectMode: true}); 75 | stream.queue( 76 | gulp.src('./src/strap*.html') 77 | .pipe(templateCache({ 78 | module: 'schemaForm', 79 | root: 'directives/decorators/bootstrap/strap/' 80 | })) 81 | ); 82 | stream.queue( 83 | gulp.src('./src/ui*.html') 84 | .pipe(templateCache({ 85 | module: 'schemaForm', 86 | root: 'directives/decorators/bootstrap/uiselect/' 87 | })) 88 | ); 89 | stream.queue(gulp.src('./src/*.js')); 90 | 91 | stream.done() 92 | .pipe(concat('angular-schema-form-dynamic-select.js')) 93 | .pipe(umd({ 94 | dependencies: function() { 95 | return [ 96 | {name: 'schemaForm', 97 | amd:"angular-schema-form", 98 | cjs: 'angular-schema-form'}, 99 | ]; 100 | }, 101 | exports: function() {return 'angularSchemaFormDynamicSelect';}, 102 | namespace: function() {return 'angularSchemaFormDynamicSelect';} 103 | })) 104 | .pipe(gulp.dest('.')); 105 | 106 | }); 107 | 108 | gulp.task('jscs', function() { 109 | gulp.src('./src/**/*.js') 110 | .pipe(jscs()); 111 | }); 112 | 113 | gulp.task('default', [ 114 | 'minify', 115 | 'non-minified-dist' 116 | ]); 117 | 118 | gulp.task('watch', function() { 119 | gulp.watch('./src/**/*', ['default']); 120 | }); 121 | 122 | gulp.task('webserver', function() { 123 | gulp.src('.') 124 | .pipe(webserver({ 125 | livereload: true, 126 | port: 8001, 127 | open: true 128 | })); 129 | }); 130 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Angular schema form dynamic select example 32 | 33 | 34 |

Example

35 | 36 |

This is an example of the features of Angular 37 | schema form dynamic select

38 | 39 |

Please report any issues or bugs 40 | you may find.

41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 4 | // base path, that will be used to resolve files and exclude 5 | basePath: '.', 6 | 7 | // frameworks to use 8 | frameworks: ['mocha', 'chai-sinon'], 9 | 10 | // list of files / patterns to load in the browser 11 | files: [ 12 | 'bower_components/jquery/dist/jquery.min.js', 13 | 'bower_components/angular/angular.js', 14 | 'bower_components/angular-mocks/angular-mocks.js', 15 | 'bower_components/angular-sanitize/angular-sanitize.min.js', 16 | 'bower_components/angular-translate/angular-translate.min.js', 17 | 'bower_components/angular-strap/dist/angular-strap.min.js', 18 | 'bower_components/angular-strap/dist/angular-strap.tpl.min.js', 19 | 'bower_components/tv4/tv4.js', 20 | 'bower_components/objectpath/lib/ObjectPath.js', 21 | 'bower_components/angular-schema-form/dist/schema-form.js', 22 | 'bower_components/angular-schema-form/dist/bootstrap-decorator.min.js', 23 | 'src/*.js', 24 | 'src/**/*.html', 25 | 'test/tests.js' 26 | ], 27 | 28 | // list of files to exclude 29 | exclude: [ 30 | 31 | ], 32 | 33 | // test results reporter to use 34 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 35 | reporters: ['progress', 'coverage', 'growler'], 36 | 37 | preprocessors: { 38 | 'src/**/*.js': ['coverage'], 39 | 'src/**/*.html': ['ng-html2js'] 40 | }, 41 | 42 | // optionally, configure the reporter 43 | coverageReporter: { 44 | type : 'lcov', 45 | dir : 'coverage/' 46 | }, 47 | 48 | ngHtml2JsPreprocessor: { 49 | cacheIdFromPath: function(filepath) { 50 | return 'directives/decorators/bootstrap/strap/' + filepath.substr(4); 51 | }, 52 | moduleName: 'templates' 53 | }, 54 | 55 | // web server port 56 | port: 9876, 57 | 58 | // enable / disable colors in the output (reporters and logs) 59 | colors: true, 60 | 61 | // level of logging 62 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 63 | logLevel: config.LOG_INFO, 64 | 65 | 66 | // enable / disable watching file and executing tests whenever any file changes 67 | autoWatch: true, 68 | 69 | 70 | // Start these browsers, currently available: 71 | // - Chrome 72 | // - ChromeCanary 73 | // - Firefox 74 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 75 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 76 | // - PhantomJS 77 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 78 | browsers: ['PhantomJS'], 79 | 80 | 81 | // If browser does not capture in given timeout [ms], kill it 82 | captureTimeout: 60000, 83 | 84 | 85 | // Continuous Integration mode 86 | // if true, it capture browsers, run tests and exit 87 | singleRun: false 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-schema-form-dynamic-select", 3 | "version": "0.14.5", 4 | "description": "Dynamic select add-on for angular schema form", 5 | "scripts": { 6 | "build": "gulp default", 7 | "test": "rm -fr coverage && ./node_modules/karma/bin/karma start --single-run --browsers PhantomJS karma.conf.js" 8 | }, 9 | "author": "Nicklas Borjesson", 10 | "registry": "jspm", 11 | "contributors": [ 12 | { 13 | "name": "Cheng Zhu", 14 | "email": "cheng@aqmin.com" 15 | }, 16 | { 17 | "name": "stevehu", 18 | "email": "unknown" 19 | }, 20 | { 21 | "name": "Nicklas Börjesson", 22 | "email": "nicklas@optimalbpm.se" 23 | } 24 | ], 25 | "license": "MIT", 26 | "dependencies": { 27 | "angular-schema-form": "~0", 28 | "angular-schema-form-bootstrap": "~0", 29 | "angular-strap": "~2", 30 | "bootstrap": "~3", 31 | "angular-ui-select": "~0" 32 | }, 33 | "main": "angular-schema-form-dynamic-select.js", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/OptimalBPM/angular-schema-form-dynamic-select.git" 37 | }, 38 | "devDependencies": { 39 | "angular-mocks": ">= 1.2", 40 | "angular-translate": ">= 2.4.0", 41 | "chai": "^1.9.0", 42 | "coveralls": "^2.11.0", 43 | "gulp": "^3.5.6", 44 | "gulp-angular-templatecache": "^1.2.1", 45 | "gulp-concat": "^2.2.0", 46 | "gulp-jscs": "^1.1.0", 47 | "gulp-minify-html": "^0.1.1", 48 | "gulp-uglify": "^0.2.1", 49 | "gulp-webserver": "^0.9.1", 50 | "gulp-umd": "", 51 | "jasmine-core": "^2.2.0", 52 | "karma": "^0.12.31", 53 | "karma-chai-sinon": "^0.1.3", 54 | "karma-chrome-launcher": "^0.1.7", 55 | "karma-coverage": "^0.2.1", 56 | "karma-growler-reporter": "0.0.1", 57 | "karma-jasmine": "^0.3.5", 58 | "karma-mocha": "^0.1.3", 59 | "karma-ng-html2js-preprocessor": "^0.1.0", 60 | "karma-phantomjs-launcher": "^0.1.4", 61 | "mocha": "^1.18.0", 62 | "mocha-lcov-reporter": "0.0.1", 63 | "sinon": "^1.9.0", 64 | "sinon-chai": "^2.5.0", 65 | "streamqueue": "0.0.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/angular-schema-form-dynamic-select.js: -------------------------------------------------------------------------------- 1 | angular.module('schemaForm').config( 2 | ['schemaFormProvider', 'schemaFormDecoratorsProvider', 'sfPathProvider', 3 | function (schemaFormProvider, schemaFormDecoratorsProvider, sfPathProvider) { 4 | 5 | var select = function (name, schema, options) { 6 | if ((schema.type === 'string') && ("enum" in schema)) { 7 | var f = schemaFormProvider.stdFormObj(name, schema, options); 8 | f.key = options.path; 9 | f.type = 'strapselect'; 10 | options.lookup[sfPathProvider.stringify(options.path)] = f; 11 | return f; 12 | } 13 | }; 14 | 15 | schemaFormProvider.defaults.string.unshift(select); 16 | 17 | //Add to the bootstrap directive 18 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapselect', 19 | 'directives/decorators/bootstrap/strap/strapselect.html'); 20 | 21 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapmultiselect', 22 | 'directives/decorators/bootstrap/strap/strapmultiselect.html'); 23 | 24 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapselectdynamic', 25 | 'directives/decorators/bootstrap/strap/strapselect.html'); 26 | 27 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'strapmultiselectdynamic', 28 | 'directives/decorators/bootstrap/strap/strapmultiselect.html'); 29 | 30 | 31 | // UI SELECT 32 | //Add to the bootstrap directive 33 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'uiselect', 34 | 'directives/decorators/bootstrap/uiselect/uiselect.html'); 35 | 36 | 37 | schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'uiselectmultiple', 38 | 'directives/decorators/bootstrap/uiselect/uiselectmultiple.html'); 39 | 40 | 41 | }]) 42 | .directive("toggleSingleModel", function() { 43 | // some how we get this to work ... 44 | return { 45 | require: 'ngModel', 46 | restrict: "A", 47 | scope: {}, 48 | replace: true, 49 | controller: ['$scope', function($scope) { 50 | $scope.$parent.$watch('select_model.selected',function(){ 51 | if($scope.$parent.select_model.selected != undefined) { 52 | $scope.$parent.insideModel = $scope.$parent.select_model.selected.value; 53 | $scope.$parent.ngModel.$setViewValue($scope.$parent.select_model.selected.value); 54 | } 55 | }); 56 | }] 57 | }; 58 | }) 59 | 60 | .directive('multipleOn', function() { 61 | return { 62 | link: function($scope, $element, $attrs) { 63 | $scope.$watch( 64 | function () { return $element.attr('multiple-on'); }, 65 | function (newVal) { 66 | 67 | if(newVal == "true") { 68 | var select_scope = angular.element($element).scope().$$childTail; 69 | select_scope.$isMultiple = true; 70 | select_scope.options.multiple = true; 71 | select_scope.$select.$element.addClass('select-multiple'); 72 | } 73 | else { 74 | angular.element($element).scope().$$childTail.$isMultiple = false; 75 | } 76 | } 77 | ); 78 | } 79 | }; 80 | }) 81 | .filter('whereMulti', function() { 82 | return function(items, key, values) { 83 | var out = []; 84 | 85 | if (angular.isArray(values) && items !== undefined) { 86 | values.forEach(function (value) { 87 | for (var i = 0; i < items.length; i++) { 88 | if (value == items[i][key]) { 89 | out.push(items[i]); 90 | break; 91 | } 92 | } 93 | }); 94 | } else { 95 | // Let the output be the input untouched 96 | out = items; 97 | } 98 | 99 | return out; 100 | }; 101 | }) 102 | .filter('propsFilter', function() { 103 | return function (items, props) { 104 | var out = []; 105 | 106 | if (angular.isArray(items)) { 107 | items.forEach(function (item) { 108 | var itemMatches = false; 109 | 110 | var keys = Object.keys(props); 111 | for (var i = 0; i < keys.length; i++) { 112 | var prop = keys[i]; 113 | if (item.hasOwnProperty(prop)) { 114 | //only match if this property is actually in the item to avoid 115 | var text = props[prop].toLowerCase(); 116 | //search for either a space before the text or the textg at the start of the string so that the middle of words are not matched 117 | if (item[prop].toString().toLowerCase().indexOf(text) === 0 || ( item[prop].toString()).toLowerCase().indexOf(' ' + text) !== -1) { 118 | itemMatches = true; 119 | break; 120 | } 121 | } 122 | } 123 | 124 | if (itemMatches) { 125 | out.push(item); 126 | } 127 | }); 128 | } else { 129 | // Let the output be the input untouched 130 | out = items; 131 | } 132 | 133 | return out; 134 | }; 135 | }); 136 | 137 | angular.module('schemaForm').controller('dynamicSelectController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { 138 | 139 | if (!$scope.form.options) { 140 | $scope.form.options = {}; 141 | } 142 | 143 | $scope.select_model = {}; 144 | 145 | console.log("Setting options." + $scope.form.options.toString()); 146 | $scope.form.options.scope = $scope; 147 | 148 | $scope.triggerTitleMap = function () { 149 | console.log("listener triggered"); 150 | // Ugly workaround to trigger titleMap expression re-evaluation so that the selectFilter it reapplied. 151 | $scope.form.titleMap.push({"value": "345890u340598u3405u9", "name": "34095u3p4ouij"}) 152 | $timeout(function () { $scope.form.titleMap.pop() }) 153 | 154 | }; 155 | 156 | $scope.initFiltering = function (localModel) { 157 | if ($scope.form.options.filterTriggers) { 158 | $scope.form.options.filterTriggers.forEach(function (trigger) { 159 | $scope.$parent.$watch(trigger, $scope.triggerTitleMap) 160 | 161 | }); 162 | } 163 | // This is set here, as the model value may become unitialized and typeless if validation fails. 164 | $scope.localModelType = Object.prototype.toString.call(localModel); 165 | $scope.filteringInitialized = true; 166 | }; 167 | 168 | 169 | $scope.finalizeTitleMap = function (form, data, newOptions) { 170 | // Remap the data 171 | 172 | form.titleMap = []; 173 | 174 | if (newOptions && "map" in newOptions && newOptions .map) { 175 | var current_row = null, 176 | final = newOptions.map.nameProperty.length - 1, 177 | separator = newOptions.map.separatorValue ? newOptions.map.separatorValue : ' - '; 178 | data.forEach(function (current_row) { 179 | current_row["value"] = current_row[newOptions .map.valueProperty]; 180 | //check if the value passed is a string or not 181 | if(typeof newOptions.map.nameProperty != 'string'){ 182 | //loop through the object/array 183 | var newName = ""; 184 | for (var i in newOptions.map.nameProperty) { 185 | newName += current_row[newOptions .map.nameProperty[i]]; 186 | if(i != final){newName += separator}; 187 | } 188 | current_row["name"] = newName; //init the 'name' property 189 | } 190 | else{ 191 | //if it is a string 192 | current_row["name"] = current_row[newOptions .map.nameProperty]; 193 | } 194 | form.titleMap.push(current_row); 195 | }); 196 | 197 | } 198 | else { 199 | data.forEach(function (item) { 200 | if ("text" in item) { 201 | item.name = item.text 202 | } 203 | } 204 | ); 205 | form.titleMap = data; 206 | } 207 | 208 | if ($scope.insideModel && $scope.select_model.selected === undefined) { 209 | $scope.select_model.selected = $scope.find_in_titleMap($scope.insideModel).item; 210 | } 211 | 212 | // The ui-selects needs to be reinitialized (UI select sets the internalModel and externalModel. 213 | if ($scope.internalModel) { 214 | console.log("Call uiMultiSelectInitInternalModel"); 215 | $scope.uiMultiSelectInitInternalModel($scope.externalModel); 216 | } 217 | }; 218 | 219 | $scope.clone = function (obj) { 220 | // Clone an object (except references to this scope) 221 | if (null == obj || "object" != typeof(obj)) return obj; 222 | 223 | var copy = obj.constructor(); 224 | for (var attr in obj) { 225 | // Do not clone if it is this scope 226 | if (obj[attr] != $scope) { 227 | if (obj.hasOwnProperty(attr)) copy[attr] = $scope.clone(obj[attr]); 228 | } 229 | } 230 | return copy; 231 | }; 232 | 233 | 234 | $scope.getCallback = function (callback) { 235 | if (typeof(callback) == "string") { 236 | var _result = $scope.$parent.evalExpr(callback); 237 | if (typeof(_result) == "function") { 238 | return _result; 239 | } 240 | else { 241 | throw("A callback string must match name of a function in the parent scope") 242 | } 243 | 244 | } 245 | else if (typeof(callback) == "function") { 246 | return callback; 247 | } 248 | else { 249 | throw("A callback must either be a string matching the name of a function in the parent scope or a " + 250 | "direct function reference") 251 | 252 | } 253 | }; 254 | 255 | $scope.getOptions = function (options, search) { 256 | // If defined, let the a callback function manipulate the options 257 | if (options.httpPost && options.httpPost.optionsCallback) { 258 | newOptionInstance = $scope.clone(options); 259 | return $scope.getCallback(options.httpPost.optionsCallback)(newOptionInstance, search); 260 | } 261 | if (options.httpGet && options.httpGet.optionsCallback) { 262 | newOptionInstance = $scope.clone(options); 263 | return $scope.getCallback(options.httpGet.optionsCallback)(newOptionInstance, search); 264 | } 265 | else { 266 | return options; 267 | } 268 | }; 269 | 270 | $scope.test = function (form) { 271 | form.titleMap.pop(); 272 | }; 273 | 274 | 275 | $scope.populateTitleMap = function (form, search) { 276 | 277 | if (form.schema && "enum" in form.schema) { 278 | form.titleMap = []; 279 | form.schema.enum.forEach(function (item) { 280 | form.titleMap.push({"value": item, "name": item}) 281 | } 282 | ); 283 | 284 | } 285 | else if (!form.options) { 286 | 287 | console.log("dynamicSelectController.populateTitleMap(key:" + form.key + ") : No options set, needed for dynamic selects"); 288 | } 289 | else if (form.options.callback) { 290 | form.titleMap = $scope.getCallback(form.options.callback)(form.options, search); 291 | $scope.finalizeTitleMap(form,form.titleMap, form.options); 292 | console.log("callback items: ", form.titleMap); 293 | } 294 | else if (form.options.asyncCallback) { 295 | return $scope.getCallback(form.options.asyncCallback)(form.options, search).then( 296 | function (_data) { 297 | // In order to work with both $http and generic promises 298 | _data = _data.data || _data; 299 | $scope.finalizeTitleMap(form, _data, form.options); 300 | console.log('asyncCallback items', form.titleMap); 301 | }, 302 | function (data, status) { 303 | if (form.options.onPopulationError) { 304 | $scope.getCallback(form.options.onPopulationError)(form, data, status); 305 | } 306 | else { 307 | alert("Loading select items failed(Options: '" + String(form.options) + 308 | "\nError: " + status); 309 | } 310 | }); 311 | } 312 | else if (form.options.httpPost) { 313 | var finalOptions = $scope.getOptions(form.options, search); 314 | 315 | return $http.post(finalOptions.httpPost.url, finalOptions.httpPost.parameter).then( 316 | function (_data) { 317 | 318 | $scope.finalizeTitleMap(form, _data.data, finalOptions); 319 | console.log('httpPost items', form.titleMap); 320 | }, 321 | function (data, status) { 322 | if (form.options.onPopulationError) { 323 | $scope.getCallback(form.options.onPopulationError)(form, data, status); 324 | } 325 | else { 326 | alert("Loading select items failed (URL: '" + String(finalOptions.httpPost.url) + 327 | "' Parameter: " + String(finalOptions.httpPost.parameter) + "\nError: " + status); 328 | } 329 | }); 330 | } 331 | else if (form.options.httpGet) { 332 | var finalOptions = $scope.getOptions(form.options, search); 333 | return $http.get(finalOptions.httpGet.url, finalOptions.httpGet.parameter).then( 334 | function (data) { 335 | $scope.finalizeTitleMap(form, data.data, finalOptions); 336 | console.log('httpGet items', form.titleMap); 337 | }, 338 | function (data, status) { 339 | if (form.options.onPopulationError) { 340 | $scope.getCallback(form.options.onPopulationError)(form, data, status); 341 | } 342 | else { 343 | alert("Loading select items failed (URL: '" + String(finalOptions.httpGet.url) + 344 | "\nError: " + status); 345 | } 346 | }); 347 | } 348 | else { 349 | if ($scope.insideModel && $scope.select_model.selected === undefined) { 350 | $scope.select_model.selected = $scope.find_in_titleMap($scope.insideModel); 351 | } 352 | } 353 | }; 354 | 355 | 356 | $scope.find_in_titleMap = function (value) { 357 | for (i = 0; i < $scope.form.titleMap.length; i++) { 358 | if ($scope.form.titleMap[i].value == value) { 359 | return {"item": $scope.form.titleMap[i], "index": i}; 360 | } 361 | } 362 | 363 | }; 364 | 365 | $scope.uiMultiSelectInitInternalModel = function(supplied_model) 366 | { 367 | 368 | 369 | console.log("$scope.externalModel: Key: " +$scope.form.key.toString() + " Model: " + supplied_model.toString()); 370 | $scope.externalModel = supplied_model; 371 | $scope.internalModel = []; 372 | if ($scope.form.titleMap) { 373 | if (supplied_model !== undefined && angular.isArray(supplied_model)){ 374 | supplied_model.forEach(function (value) { 375 | titleMap_item = $scope.find_in_titleMap(value); 376 | $scope.internalModel.push(titleMap_item.item); 377 | $scope.form.titleMap.splice(titleMap_item.index, 1); 378 | } 379 | ) 380 | } 381 | } 382 | }; 383 | 384 | }]); 385 | 386 | angular.module('schemaForm').filter('selectFilter', [function ($filter) { 387 | return function (inputArray, controller, localModel, strLocalModel) { 388 | // As the controllers' .model is the global and its form is the local, we need to get the local model as well. 389 | // We also need tp be able to set it if is undefined after a validation failure,so for that we need 390 | // its string representation as well as we do not know its name. A typical value if strLocalModel is model['groups'] 391 | // This is very ugly, though. TODO: Find out why the model is set to undefined after validation failure. 392 | 393 | if (!angular.isDefined(inputArray) || !angular.isDefined(controller.form.options) || 394 | !angular.isDefined(controller.form.options.filter) || controller.form.options.filter == '') { 395 | return inputArray; 396 | } 397 | 398 | 399 | 400 | console.log("----- In filtering for " + controller.form.key + "(" + controller.form.title +"), model value: " + JSON.stringify( localModel) + "----"); 401 | console.log("Filter:" + controller.form.options.filter); 402 | if (!controller.filteringInitialized) { 403 | console.log("Initialize filter"); 404 | controller.initFiltering(localModel); 405 | } 406 | 407 | 408 | var data = []; 409 | 410 | 411 | angular.forEach(inputArray, function (curr_item) { 412 | //console.log("Compare: curr_item: " + JSON.stringify(curr_item) + 413 | //"with : " + JSON.stringify( controller.$eval(controller.form.options.filterTriggers[0]))); 414 | if (controller.$eval(controller.form.options.filter, {item: curr_item})) { 415 | data.push(curr_item); 416 | } 417 | else if (localModel) { 418 | // If not in list, also remove the set value 419 | 420 | if (controller.localModelType == "[object Array]" && localModel.indexOf(curr_item.value) > -1) { 421 | localModel.splice(localModel.indexOf(curr_item.value), 1); 422 | } 423 | else if (localModel == curr_item.value) { 424 | console.log("Setting model of type " + controller.localModelType + "to null."); 425 | localModel = null; 426 | } 427 | } 428 | }); 429 | 430 | if (controller.localModelType == "[object Array]" && !localModel) { 431 | // An undefined local model seems to mess up bootstrap select's indicators 432 | console.log("Resetting model of type " + controller.localModelType + " to []."); 433 | 434 | controller.$eval(strLocalModel + "=[]"); 435 | } 436 | 437 | //console.log("Input: " + JSON.stringify(inputArray)); 438 | //console.log("Output: " + JSON.stringify(data)); 439 | //console.log("Model value out : " + JSON.stringify(localModel)); 440 | console.log("----- Exiting filter for " + controller.form.title + "-----"); 441 | 442 | return data; 443 | }; 444 | }]); 445 | -------------------------------------------------------------------------------- /src/strapmultiselect.html: -------------------------------------------------------------------------------- 1 |
3 | 4 | 5 |
6 | 16 | {{ (hasError() && errorMessage(schemaError())) || form.description}} 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/strapselect.html: -------------------------------------------------------------------------------- 1 |
3 | 4 | 5 |
6 | 15 | 23 | {{ (hasError() && errorMessage(schemaError())) || form.description}} 24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /src/uiselect.html: -------------------------------------------------------------------------------- 1 |
4 | 5 | 6 |
7 | 10 | 12 | {{select_model.selected.name}} 13 | 14 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | 29 | 31 | {{select_model.selected.name}}  32 | {{(select_model.selected.isTag===true ? form.options.taggingLabel : '')}} 33 | 34 | 35 | 38 |
40 |
41 |
42 | 43 |
44 |
45 |
46 | 47 | 48 | 49 | 55 | 57 | {{select_model.selected.name}}  58 | {{(select_model.selected.isTag===true ? form.options.taggingLabel : '')}} 59 | 60 | 64 |
66 |
67 |
68 | 69 |
70 |
71 |
72 | 73 | 80 | 81 | 85 | 86 |
87 | 88 |
89 |
90 | -------------------------------------------------------------------------------- /src/uiselectmultiple.html: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 |
6 | 9 | {{$item.name}} 10 | 14 |
15 |
16 |
17 | 20 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /test/testdata.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"value": "json-value1", "name": "json-name1"}, 3 | {"value": "json-value2", "name": "json-name2"}, 4 | {"value": "json-value3", "name": "json-name3"} 5 | ] 6 | -------------------------------------------------------------------------------- /test/testdata_mapped.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"nodeId": "1", "nodeName": "Node 1", "category": "value1"}, 3 | {"nodeId": "2", "nodeName": "Node 2", "category": "value2"}, 4 | {"nodeId": "3", "nodeName": "Node 3", "category": "value3"} 5 | ] -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | /* jshint expr: true */ 2 | chai.should(); 3 | 4 | describe('Schema form', function () { 5 | 6 | describe('directive', function () { 7 | beforeEach(module('templates')); 8 | beforeEach(module('schemaForm')); 9 | beforeEach(module('mgcrea.ngStrap')); 10 | beforeEach( 11 | // We don't need no sanitation. We don't need no thought control. 12 | // (nicklasb: I don't totally understand this Floyd reference, so I better leave it in there :-) 13 | module(function ($sceProvider) { 14 | $sceProvider.enabled(false); 15 | }) 16 | ); 17 | 18 | // Populate a given scope with test data. 19 | var assignToScope = function (scope, http) { 20 | 21 | scope.callBackSD = function (options) { 22 | return [ 23 | {value: 'value1', name: 'text1'}, 24 | {value: 'value2', name: 'text2'}, 25 | {value: 'value3', name: 'Select dynamic!'} 26 | ]; 27 | // Note: Options is a reference to the original instance, if you change a value, 28 | // that change will persist when you use this form instance again. 29 | }; 30 | 31 | scope.callBackMSD = function (options) { 32 | return [ 33 | {value: 'value1', name: 'text1'}, 34 | {value: 'value2', name: 'text2'}, 35 | {value: 'value3', name: 'Multiple select dynamic!'} 36 | ]; 37 | // Note: Options is a reference to the original instance, if you change a value, 38 | // that change will persist when you use this form instance again. 39 | }; 40 | 41 | scope.callBackMSDAsync = function (options) { 42 | // Node that we got the url from the options. Not necessary, but then the same callback function can be used 43 | // by different selects with different parameters. 44 | return http.get(options.urlOrWhateverOptionIWant); 45 | }; 46 | 47 | scope.stringOptionsCallback = function (options) { 48 | // Here you can manipulate the form options used in a http_post or http_get 49 | // For example, you can use variables to build the URL or set the parameters, here we just set the url. 50 | options.httpPost.url = "test/testdata.json"; 51 | // Note: This is a copy of the form options, edits here will not persist but are only used in this request. 52 | return options; 53 | }; 54 | 55 | 56 | scope.schema = { 57 | type: 'object', 58 | title: 'Select', 59 | properties: { 60 | select: { 61 | title: 'Single Select Static', 62 | type: 'string', 63 | description: 'Only single item is allowed' 64 | }, 65 | multiselect: { 66 | title: 'Multi Select Static', 67 | type: 'array', 68 | items: { 69 | type: "string" 70 | }, 71 | maxItems: 2, 72 | description: 'Multi single items are allowed. (select three for maxItems error)' 73 | }, 74 | selectDynamic: { 75 | title: 'Single Select Dynamic', 76 | type: 'string', 77 | items: { 78 | type: "string" 79 | }, 80 | description: 'This data is loaded from the $scope.callBackSD function. (and laid out using css-options)' 81 | }, 82 | multiselectDynamic: { 83 | title: 'Multi Select Dynamic', 84 | type: 'array', 85 | items: { 86 | type: "string" 87 | }, 88 | description: 'This data is loaded from the $scope.callBackMSD function. (referenced by name)' 89 | }, 90 | multiselectDynamicHttpPost: { 91 | title: 'Multi Select Dynamic HTTP Post', 92 | type: 'array', 93 | items: { 94 | type: "string" 95 | }, 96 | description: 'This data is asynchronously loaded using a HTTP post. ' + 97 | '(specifies parameter in form, options.url in a named callback)' 98 | }, 99 | multiselectDynamicHttpGet: { 100 | title: 'Multi Select Dynamic HTTP Get', 101 | type: 'array', 102 | items: { 103 | type: "string" 104 | }, 105 | description: 'This data is asynchronously loaded using a HTTP get. ' + 106 | '(Set the URL at options.url)' 107 | }, 108 | multiselectDynamicHttpGetMapped: { 109 | title: 'Multi Select Dynamic HTTP Get Mapped data', 110 | type: 'array', 111 | items: { 112 | type: "string" 113 | }, 114 | description: 'This data is as above, but remapped from a nodeId/nodeName array of objects. ' + 115 | '(See app.js: "map" : {valueProperty: "nodeId", textProperty: "nodeName"})' 116 | }, 117 | multiselectDynamicAsync: { 118 | title: 'Multi Select Dynamic Async', 119 | type: 'array', 120 | items: { 121 | type: "string" 122 | }, 123 | description: 'This data is asynchrously loaded using a async call. ' + 124 | '(specify options.async.call)' 125 | } 126 | }, 127 | required: ['select', 'multiselect'] 128 | }; 129 | 130 | scope.testResponse = [ 131 | {value: "json-value1", name: "json-name1"}, 132 | {value: "json-value2", name: "json-name2"}, 133 | {value: "json-value3", name: "json-name3"} 134 | ]; 135 | scope.testResponseMapped = [ 136 | {"nodeId": "1", "nodeName": "Node 1"}, 137 | {"nodeId": "2", "nodeName": "Node 2"}, 138 | {"nodeId": "3", "nodeName": "Node 3"} 139 | ]; 140 | scope.testResponseMappedCmp = [ 141 | {"nodeId": "1", "nodeName": "Node 1", "value": "1", "name": "Node 1"}, 142 | {"nodeId": "2", "nodeName": "Node 2", "value": "2", "name": "Node 2"}, 143 | {"nodeId": "3", "nodeName": "Node 3", "value": "3", "name": "Node 3"} 144 | ]; 145 | scope.form = [ 146 | { 147 | "key": 'select', 148 | "type": 'strapselect', 149 | "titleMap": [ 150 | {"value": 'value1', "name": 'text1'}, 151 | {"value": 'value2', "name": 'text2'}, 152 | {"value": 'value3', "name": 'text3'} 153 | ] 154 | }, 155 | { 156 | "key": 'multiselect', 157 | "type": 'strapmultiselect', 158 | "titleMap": [ 159 | {"value": 'value1', "name": 'text1'}, 160 | {"value": 'value2', "name": 'text2'}, 161 | {"value": 'value3', "name": 'long very very long label3'} 162 | ] 163 | }, 164 | { 165 | "key": "selectDynamic", 166 | "type": 'strapselectdynamic', 167 | "htmlClass": "col-lg-3 col-md-3", 168 | "labelHtmlClass": "bigger", 169 | "fieldHtmlClass": "tilted", 170 | "options": { 171 | "callback": scope.callBackSD 172 | } 173 | }, 174 | { 175 | "key": "multiselectDynamic", 176 | "type": 'strapmultiselectdynamic', 177 | placeholder: "not set yet(this text is defined using the placeholder option)", 178 | "options": { 179 | "callback": "callBackMSD" 180 | } 181 | }, 182 | { 183 | "key": "multiselectDynamicHttpPost", 184 | "type": 'strapmultiselectdynamic', 185 | "title": 'Multi Select Dynamic HTTP Post (title is from form.options, overriding the schema.title)', 186 | "options": { 187 | "httpPost": { 188 | "optionsCallback": "stringOptionsCallback", 189 | "parameter": {"myparam": "Hello"} 190 | } 191 | } 192 | }, 193 | { 194 | "key": "multiselectDynamicHttpGet", 195 | "type": 'strapmultiselectdynamic', 196 | "options": { 197 | "httpGet": { 198 | "url": "test/testdata.json" 199 | } 200 | } 201 | }, 202 | { 203 | "key": "multiselectDynamicHttpGetMapped", 204 | "type": 'strapmultiselectdynamic', 205 | "options": { 206 | "httpGet": { 207 | "url": "test/testdata_mapped.json" 208 | }, 209 | "map": {valueProperty: "nodeId", nameProperty: "nodeName"} 210 | } 211 | }, 212 | { 213 | "key": "multiselectDynamicAsync", 214 | "type": 'strapmultiselectdynamic', 215 | "onChange": function () { 216 | alert("You changed this value! (this was the onChange event in action)"); 217 | }, 218 | "options": { 219 | "asyncCallback": scope.callBackMSDAsync, 220 | "urlOrWhateverOptionIWant": "test/testdata.json" 221 | } 222 | } 223 | ]; 224 | 225 | scope.model = {}; 226 | scope.model.select = 'value1'; 227 | scope.model.multiselect = ['value2', 'value1']; 228 | scope.model.multiselectDynamicHttpPost = null; 229 | }; 230 | 231 | it('should load the correct items into each type of select', function () { 232 | inject(function ($compile, $rootScope, schemaForm, $http, $httpBackend, $timeout, $document) { 233 | var scope = $rootScope.$new(); 234 | // Load example data 235 | assignToScope(scope, $http); 236 | 237 | // Create a template 238 | var tmpl = angular.element('
'); 239 | 240 | // Add http mocks 241 | $httpBackend.whenGET("test/testdata.json").respond(200, scope.testResponse); 242 | $httpBackend.whenGET("test/testdata_mapped.json").respond(200, scope.testResponseMapped); 243 | $httpBackend.whenPOST("test/testdata.json", {"myparam": "Hello"}).respond(200, scope.testResponse); 244 | 245 | // Compile the template 246 | $compile(tmpl)(scope); 247 | 248 | // Do an update, this triggers all items to be loaded 249 | $rootScope.$apply(); 250 | 251 | // Attempt to click one of the selects, doesn't work as Karma seem to not work that way 252 | tmpl.children().eq(7).children().eq(0).children().eq(1).click(); 253 | 254 | // Tell the mock to respond to requests 255 | $httpBackend.flush(); 256 | 257 | // Wait for all getting done before checking. 258 | $timeout(function () { 259 | 260 | // Find HTML elements in the response, find its scope, and then deep compare with known results. 261 | 262 | // Single Select Dynamic 263 | expect(JSON.stringify(angular.element(tmpl.children().eq(2).children().eq(0).children().eq(1)).scope().form.titleMap)). 264 | to.equal(JSON.stringify(scope.callBackSD()), "Single Select Dynamic test failed."); 265 | // Multi Select Dynamic 266 | expect(JSON.stringify(angular.element(tmpl.children().eq(3).children().eq(0).children().eq(1)).scope().form.titleMap)). 267 | to.equal(JSON.stringify(scope.callBackMSD()), "Multi Select Dynamic test failed."); 268 | // Multi Select Dynamic HTTP Post 269 | expect(JSON.stringify(angular.element(tmpl.children().eq(4).children().eq(0).children().eq(1)).scope().form.titleMap)). 270 | to.equal(JSON.stringify(scope.testResponse), "Multi Select Dynamic HTTP Post test failed."); 271 | // Multi Select Dynamic HTTP Get 272 | expect(JSON.stringify(angular.element(tmpl.children().eq(5).children().eq(0).children().eq(1)).scope().form.titleMap)). 273 | to.equal(JSON.stringify(scope.testResponse), "Multi Select Dynamic HTTP Get test failed."); 274 | // Multi Select Dynamic HTTP Get Mapped 275 | expect(JSON.stringify(angular.element(tmpl.children().eq(6).children().eq(0).children().eq(1)).scope().form.titleMap)). 276 | to.equal(JSON.stringify(scope.testResponseMappedCmp), "Multi Select Dynamic HTTP Get Mapped test failed."); 277 | // Multi Select Dynamic Async 278 | expect(JSON.stringify(angular.element(tmpl.children().eq(7).children().eq(0).children().eq(1)).scope().form.titleMap)). 279 | to.equal(JSON.stringify(scope.testResponse), "Multi Select Dynamic Async test failed."); 280 | 281 | } 282 | ); 283 | 284 | // Angular doesn't like async unit tests, tell it to call the checks above 285 | $timeout.flush() 286 | 287 | }); 288 | }); 289 | 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /ui-sortable.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery UI Sortable plugin wrapper 3 | @param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config 4 | */ 5 | angular.module('ui.sortable', []) 6 | .value('uiSortableConfig',{}) 7 | .directive('uiSortable', [ 8 | 'uiSortableConfig', '$timeout', '$log', 9 | function(uiSortableConfig, $timeout, $log) { 10 | return { 11 | require: '?ngModel', 12 | link: function(scope, element, attrs, ngModel) { 13 | var savedNodes; 14 | 15 | function combineCallbacks(first,second){ 16 | if(second && (typeof second === 'function')) { 17 | return function(e, ui) { 18 | first(e, ui); 19 | second(e, ui); 20 | }; 21 | } 22 | return first; 23 | } 24 | 25 | function hasSortingHelper (element, ui) { 26 | var helperOption = element.sortable('option','helper'); 27 | return helperOption === 'clone' || (typeof helperOption === 'function' && ui.item.sortable.isCustomHelperUsed()); 28 | } 29 | 30 | var opts = {}; 31 | 32 | var callbacks = { 33 | receive: null, 34 | remove:null, 35 | start:null, 36 | stop:null, 37 | update:null 38 | }; 39 | 40 | var wrappers = { 41 | helper: null 42 | }; 43 | 44 | angular.extend(opts, uiSortableConfig, scope.$eval(attrs.uiSortable)); 45 | 46 | if (!angular.element.fn || !angular.element.fn.jquery) { 47 | $log.error('ui.sortable: jQuery should be included before AngularJS!'); 48 | return; 49 | } 50 | 51 | if (ngModel) { 52 | 53 | // When we add or remove elements, we need the sortable to 'refresh' 54 | // so it can find the new/removed elements. 55 | scope.$watch(attrs.ngModel+'.length', function() { 56 | // Timeout to let ng-repeat modify the DOM 57 | $timeout(function() { 58 | // ensure that the jquery-ui-sortable widget instance 59 | // is still bound to the directive's element 60 | if (!!element.data('ui-sortable')) { 61 | element.sortable('refresh'); 62 | } 63 | }); 64 | }); 65 | 66 | callbacks.start = function(e, ui) { 67 | // Save the starting position of dragged item 68 | ui.item.sortable = { 69 | index: ui.item.index(), 70 | cancel: function () { 71 | ui.item.sortable._isCanceled = true; 72 | }, 73 | isCanceled: function () { 74 | return ui.item.sortable._isCanceled; 75 | }, 76 | isCustomHelperUsed: function () { 77 | return !!ui.item.sortable._isCustomHelperUsed; 78 | }, 79 | _isCanceled: false, 80 | _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed 81 | }; 82 | }; 83 | 84 | callbacks.activate = function(/*e, ui*/) { 85 | // We need to make a copy of the current element's contents so 86 | // we can restore it after sortable has messed it up. 87 | // This is inside activate (instead of start) in order to save 88 | // both lists when dragging between connected lists. 89 | savedNodes = element.contents(); 90 | 91 | // If this list has a placeholder (the connected lists won't), 92 | // don't inlcude it in saved nodes. 93 | var placeholder = element.sortable('option','placeholder'); 94 | 95 | // placeholder.element will be a function if the placeholder, has 96 | // been created (placeholder will be an object). If it hasn't 97 | // been created, either placeholder will be false if no 98 | // placeholder class was given or placeholder.element will be 99 | // undefined if a class was given (placeholder will be a string) 100 | if (placeholder && placeholder.element && typeof placeholder.element === 'function') { 101 | var phElement = placeholder.element(); 102 | // workaround for jquery ui 1.9.x, 103 | // not returning jquery collection 104 | phElement = angular.element(phElement); 105 | 106 | // exact match with the placeholder's class attribute to handle 107 | // the case that multiple connected sortables exist and 108 | // the placehoilder option equals the class of sortable items 109 | var excludes = element.find('[class="' + phElement.attr('class') + '"]'); 110 | 111 | savedNodes = savedNodes.not(excludes); 112 | } 113 | }; 114 | 115 | callbacks.update = function(e, ui) { 116 | // Save current drop position but only if this is not a second 117 | // update that happens when moving between lists because then 118 | // the value will be overwritten with the old value 119 | if(!ui.item.sortable.received) { 120 | ui.item.sortable.dropindex = ui.item.index(); 121 | ui.item.sortable.droptarget = ui.item.parent(); 122 | 123 | // Cancel the sort (let ng-repeat do the sort for us) 124 | // Don't cancel if this is the received list because it has 125 | // already been canceled in the other list, and trying to cancel 126 | // here will mess up the DOM. 127 | element.sortable('cancel'); 128 | } 129 | 130 | // Put the nodes back exactly the way they started (this is very 131 | // important because ng-repeat uses comment elements to delineate 132 | // the start and stop of repeat sections and sortable doesn't 133 | // respect their order (even if we cancel, the order of the 134 | // comments are still messed up). 135 | if (hasSortingHelper(element, ui) && !ui.item.sortable.received && 136 | element.sortable( 'option', 'appendTo' ) === 'parent') { 137 | // restore all the savedNodes except .ui-sortable-helper element 138 | // (which is placed last). That way it will be garbage collected. 139 | savedNodes = savedNodes.not(savedNodes.last()); 140 | } 141 | savedNodes.appendTo(element); 142 | 143 | // If this is the target connected list then 144 | // it's safe to clear the restored nodes since: 145 | // update is currently running and 146 | // stop is not called for the target list. 147 | if(ui.item.sortable.received) { 148 | savedNodes = null; 149 | } 150 | 151 | // If received is true (an item was dropped in from another list) 152 | // then we add the new item to this list otherwise wait until the 153 | // stop event where we will know if it was a sort or item was 154 | // moved here from another list 155 | if(ui.item.sortable.received && !ui.item.sortable.isCanceled()) { 156 | scope.$apply(function () { 157 | ngModel.$modelValue.splice(ui.item.sortable.dropindex, 0, 158 | ui.item.sortable.moved); 159 | }); 160 | } 161 | }; 162 | 163 | callbacks.stop = function(e, ui) { 164 | // If the received flag hasn't be set on the item, this is a 165 | // normal sort, if dropindex is set, the item was moved, so move 166 | // the items in the list. 167 | if(!ui.item.sortable.received && 168 | ('dropindex' in ui.item.sortable) && 169 | !ui.item.sortable.isCanceled()) { 170 | 171 | scope.$apply(function () { 172 | ngModel.$modelValue.splice( 173 | ui.item.sortable.dropindex, 0, 174 | ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0]); 175 | }); 176 | } else { 177 | // if the item was not moved, then restore the elements 178 | // so that the ngRepeat's comment are correct. 179 | if ((!('dropindex' in ui.item.sortable) || ui.item.sortable.isCanceled()) && 180 | !hasSortingHelper(element, ui)) { 181 | savedNodes.appendTo(element); 182 | } 183 | } 184 | 185 | // It's now safe to clear the savedNodes 186 | // since stop is the last callback. 187 | savedNodes = null; 188 | }; 189 | 190 | callbacks.receive = function(e, ui) { 191 | // An item was dropped here from another list, set a flag on the 192 | // item. 193 | ui.item.sortable.received = true; 194 | }; 195 | 196 | callbacks.remove = function(e, ui) { 197 | // Workaround for a problem observed in nested connected lists. 198 | // There should be an 'update' event before 'remove' when moving 199 | // elements. If the event did not fire, cancel sorting. 200 | if (!('dropindex' in ui.item.sortable)) { 201 | element.sortable('cancel'); 202 | ui.item.sortable.cancel(); 203 | } 204 | 205 | // Remove the item from this list's model and copy data into item, 206 | // so the next list can retrive it 207 | if (!ui.item.sortable.isCanceled()) { 208 | scope.$apply(function () { 209 | ui.item.sortable.moved = ngModel.$modelValue.splice( 210 | ui.item.sortable.index, 1)[0]; 211 | }); 212 | } 213 | }; 214 | 215 | wrappers.helper = function (inner) { 216 | if (inner && typeof inner === 'function') { 217 | return function (e, item) { 218 | var innerResult = inner(e, item); 219 | item.sortable._isCustomHelperUsed = item !== innerResult; 220 | return innerResult; 221 | }; 222 | } 223 | return inner; 224 | }; 225 | 226 | scope.$watch(attrs.uiSortable, function(newVal /*, oldVal*/) { 227 | // ensure that the jquery-ui-sortable widget instance 228 | // is still bound to the directive's element 229 | if (!!element.data('ui-sortable')) { 230 | angular.forEach(newVal, function(value, key) { 231 | if(callbacks[key]) { 232 | if( key === 'stop' ){ 233 | // call apply after stop 234 | value = combineCallbacks( 235 | value, function() { scope.$apply(); }); 236 | } 237 | // wrap the callback 238 | value = combineCallbacks(callbacks[key], value); 239 | } else if (wrappers[key]) { 240 | value = wrappers[key](value); 241 | } 242 | 243 | element.sortable('option', key, value); 244 | }); 245 | } 246 | }, true); 247 | 248 | angular.forEach(callbacks, function(value, key) { 249 | opts[key] = combineCallbacks(value, opts[key]); 250 | }); 251 | 252 | } else { 253 | $log.info('ui.sortable: ngModel not provided!', element); 254 | } 255 | 256 | // Create sortable 257 | element.sortable(opts); 258 | } 259 | }; 260 | } 261 | ]); --------------------------------------------------------------------------------