├── .gitignore ├── .jscsrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── angular-schema-form-material-bundled.js ├── angular-schema-form-material-bundled.min.js ├── angular-schema-form-material.js └── angular-schema-form-material.min.js ├── examples ├── data │ ├── array.json │ ├── autocomplete.json │ ├── complex-keys.json │ ├── conditional-required.json │ ├── grid.json │ ├── remote-titlemap-books.json │ ├── remote-titlemap-movies.json │ ├── simple-oneOf.json │ ├── simple.json │ ├── sink.json │ ├── switch.json │ ├── tabarray.json │ ├── tabs.json │ ├── titlemaps.json │ └── types.json └── example.html ├── gulpfile.js ├── material-decorator.js ├── material-decorator.min.js ├── package.json ├── src ├── material-class.js ├── material-decorator.js ├── material │ ├── actions-trcl.html │ ├── actions.html │ ├── array.html │ ├── autocomplete.html │ ├── card-content.html │ ├── card.html │ ├── checkbox.html │ ├── checkboxes.html │ ├── chips.html │ ├── date.html │ ├── default.html │ ├── fieldset-trcl.html │ ├── fieldset.html │ ├── help.html │ ├── radio-buttons.html │ ├── radios-inline.html │ ├── radios.html │ ├── section.html │ ├── select.html │ ├── slider.html │ ├── submit.html │ ├── switch.html │ ├── tabarray.html │ ├── tabs.html │ └── textarea.html ├── module.js └── type-parser.js ├── test └── protractor │ ├── conf.js │ └── specs │ ├── validation-messages.js │ └── validation-tv4-errors.js ├── webpack.config.dist.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/osx,windows,phpstorm 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | 31 | ### Windows ### 32 | # Windows image file caches 33 | Thumbs.db 34 | ehthumbs.db 35 | 36 | # Folder config file 37 | Desktop.ini 38 | 39 | # Recycle Bin used on file shares 40 | $RECYCLE.BIN/ 41 | 42 | # Windows Installer files 43 | *.cab 44 | *.msi 45 | *.msm 46 | *.msp 47 | 48 | # Windows shortcuts 49 | *.lnk 50 | 51 | 52 | ### PhpStorm ### 53 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 54 | 55 | *.iml 56 | 57 | ## Directory-based project format: 58 | .idea/ 59 | # if you remove the above rule, at least ignore the following: 60 | 61 | # User-specific stuff: 62 | # .idea/workspace.xml 63 | # .idea/tasks.xml 64 | # .idea/dictionaries 65 | 66 | # Sensitive or high-churn files: 67 | # .idea/dataSources.ids 68 | # .idea/dataSources.xml 69 | # .idea/sqlDataSources.xml 70 | # .idea/dynamic.xml 71 | # .idea/uiDesigner.xml 72 | 73 | # Gradle: 74 | # .idea/gradle.xml 75 | # .idea/libraries 76 | 77 | # Mongo Explorer plugin: 78 | # .idea/mongoSettings.xml 79 | 80 | ## File-based project format: 81 | *.ipr 82 | *.iws 83 | 84 | ## Plugin-specific files: 85 | 86 | # IntelliJ 87 | /out/ 88 | 89 | # mpeltonen/sbt-idea plugin 90 | .idea_modules/ 91 | 92 | # JIRA plugin 93 | atlassian-ide-plugin.xml 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | 100 | 101 | bower_components 102 | node_modules 103 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInsideObjectBrackets": null, 4 | "requireSpacesInsideObjectBrackets": { 5 | "allExcept": [ "[", "]", "{", "}" ] 6 | }, 7 | "disallowSpacesInsideArrayBrackets": null, 8 | "requireSpacesInsideArrayBrackets": { 9 | "allExcept": [ "[", "]", "{", "}" ] 10 | }, 11 | "disallowKeywordsOnNewLine": [ ], 12 | "disallowMultipleVarDecl": null, 13 | "maximumLineLength": 120, 14 | "requireSemicolons": true 15 | } 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | We love contributions! 4 | 5 | Contact @davidlgj or @Anthropic to ensure you don't work on something already in progress! 6 | 7 | A few things to note: 8 | * Error messages are not handled in the standard Angular Material way due to ASF behaviour. 9 | To add messages to a template add **sf-messages** to the **parent** you want messages to be the last child of. 10 | * Look at the API page for the Angular Material component you are looking at, there are possibly a few attributes 11 | not used in the demo that may be useful. Please try to add as many useful attributes as you can, we can discuss the 12 | best way to implement them in the ASF form data. 13 | 14 | As an example, if the template requires a remote titleMap, consider adding optionData as a variable in the form definition that can contain a text string reference to a variable on the sf-form $scope. 15 | 16 | **Please base any merge request on the *development* branch instead of *master*.** 17 | 18 | The reason for this is that we're only really using *development* for now but will eventually start trying to use 19 | [git flow](http://danielkummer.github.io/git-flow-cheatsheet/), and it makes merging your pull 20 | request a heck of a lot easier for us. 21 | 22 | Please **avoid including the material-decorator.js or material-decorator.min.js** as that can make merging harder, and we 23 | will always generate these files when we make a new release. 24 | 25 | With new features we love to see updates to the docs as well as tests, that makes it super 26 | easy and fast for us to merge it! 27 | 28 | Also consider running any code through the **JavaScript Code Style** checker [jscs](https://github.com/mdevils/node-jscs) 29 | (or even better use it in your editor) using the .jscsrc file in the repo root, which should be picked up by the IDE. You can also us `gulp jscs` to 30 | check your code. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 JSON Schema Form 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angular Material Decorator 2 | ========================== 3 | 4 | [![Join the chat at https://gitter.im/json-schema-form/angular-schema-form-material](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/json-schema-form/angular-schema-form-material?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | For https://github.com/json-schema-form/angular-schema-form 7 | 8 | Work In Progress 9 | ---------------- 10 | Angular Material has reached 1.0.0 however I do not feel it is remotely stable yet, this decorator is progressing very cautiously until that project hits a more stable milestone. That said, I have made an early alpha available and will continue to release alpha releases as I add new features, these **are not production quality** as the name alpha implies. 11 | 12 | All this means is that it is **very** much a **work in progress**. 13 | 14 | Testing 15 | ------------ 16 | To test clone repo and: 17 | ``` 18 | npm install 19 | bower install 20 | gulp minify 21 | ``` 22 | 23 | Start favorite http server (http-server or puer for instance) and open 24 | `examples/material-example.html` 25 | 26 | There is also a `gulp watch` task that minifys on change. 27 | 28 | Known Issues 29 | ------------ 30 | * Almost nothing works if the schema uses bootstrap decorator features, it does not have array or complex keys yet and many other features are still missing or have no equivalent. 31 | * Needs development branch of angular schema form. 32 | * Only basic support for inputs, textarea, radios, radiobuttons, checkboxes, datepicker and tabs are implemented. 33 | * Angular material theme only works when `$mdThemingProvider.alwaysWatchTheme(true);` is used. 34 | * Until Angular Material hits 1.0.0 there is still chances that features may break again. 35 | 36 | Contributing 37 | ------------ 38 | Contributions are welcome! Please see [Contributing.md](CONTRIBUTING.md) for more info. 39 | 40 | Future 41 | ------ 42 | Using the new builder opens up for a lot of optimization. Primarily we can get rid of a lot of small 43 | watches by using build helpers. For instance, slapping on a `sf-changed` directive *only* if the 44 | form definition has an `onChange` option. 45 | 46 | Testing 47 | ------- 48 | ``` 49 | npm install -g protractor 50 | protractor test/protractor/conf.js 51 | ``` 52 | 53 | change baseurl in test/protractor/conf.js to match ur local environment. 54 | 55 | Copyright (c) 2016 Marcel John Bennett, David Jensen 56 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-schema-form-material", 3 | "version": "1.0.0-alpha.1", 4 | "authors": [ 5 | "Marcel Bennett " 6 | ], 7 | "description": "Angular Material decorator for Angular Schema Form", 8 | "main": "dist/angular-schema-form-material.js", 9 | "keywords": [ 10 | "angular-schema-form-decorator", 11 | "angular material", 12 | "material design", 13 | "json-schema-form" 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/json-schema-form/angular-schema-form-material", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ], 24 | "dependencies": { 25 | "angular-schema-form": ">=0.8.13" 26 | }, 27 | "devDependencies": { 28 | "angular": "1.5.5", 29 | "angular-animate": "1.5.5", 30 | "angular-material": "1.1.1", 31 | "angular-sanitize": "1.5.5", 32 | "angular-messages": "1.5.5", 33 | "angular-ui-ace": "~0.2.3", 34 | "moment": "~2.10.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dist/angular-schema-form-material.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-schema-form-material 3 | * @version 1.0.0-alpha.2 4 | * @date Mon, 02 Jan 2017 12:21:23 GMT 5 | * @link https://github.com/json-schema-form/angular-schema-form-material 6 | * @license MIT 7 | * Copyright (c) 2014-2017 JSON Schema Form 8 | */ 9 | /******/ (function(modules) { // webpackBootstrap 10 | /******/ // The module cache 11 | /******/ var installedModules = {}; 12 | 13 | /******/ // The require function 14 | /******/ function __webpack_require__(moduleId) { 15 | 16 | /******/ // Check if module is in cache 17 | /******/ if(installedModules[moduleId]) 18 | /******/ return installedModules[moduleId].exports; 19 | 20 | /******/ // Create a new module (and put it into the cache) 21 | /******/ var module = installedModules[moduleId] = { 22 | /******/ i: moduleId, 23 | /******/ l: false, 24 | /******/ exports: {} 25 | /******/ }; 26 | 27 | /******/ // Execute the module function 28 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 29 | 30 | /******/ // Flag the module as loaded 31 | /******/ module.l = true; 32 | 33 | /******/ // Return the exports of the module 34 | /******/ return module.exports; 35 | /******/ } 36 | 37 | 38 | /******/ // expose the modules object (__webpack_modules__) 39 | /******/ __webpack_require__.m = modules; 40 | 41 | /******/ // expose the module cache 42 | /******/ __webpack_require__.c = installedModules; 43 | 44 | /******/ // identity function for calling harmony imports with the correct context 45 | /******/ __webpack_require__.i = function(value) { return value; }; 46 | 47 | /******/ // define getter function for harmony exports 48 | /******/ __webpack_require__.d = function(exports, name, getter) { 49 | /******/ if(!__webpack_require__.o(exports, name)) { 50 | /******/ Object.defineProperty(exports, name, { 51 | /******/ configurable: false, 52 | /******/ enumerable: true, 53 | /******/ get: getter 54 | /******/ }); 55 | /******/ } 56 | /******/ }; 57 | 58 | /******/ // getDefaultExport function for compatibility with non-harmony modules 59 | /******/ __webpack_require__.n = function(module) { 60 | /******/ var getter = module && module.__esModule ? 61 | /******/ function getDefault() { return module['default']; } : 62 | /******/ function getModuleExports() { return module; }; 63 | /******/ __webpack_require__.d(getter, 'a', getter); 64 | /******/ return getter; 65 | /******/ }; 66 | 67 | /******/ // Object.prototype.hasOwnProperty.call 68 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 69 | 70 | /******/ // __webpack_public_path__ 71 | /******/ __webpack_require__.p = ""; 72 | 73 | /******/ // Load entry module and return exports 74 | /******/ return __webpack_require__(__webpack_require__.s = 24); 75 | /******/ }) 76 | /************************************************************************/ 77 | /******/ ([ 78 | /* 0 */ 79 | /***/ function(module, exports) { 80 | 81 | var path = '/material/default.html'; 82 | var html = "\r\n \r\n \r\n\r\n"; 83 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 84 | module.exports = path; 85 | 86 | /***/ }, 87 | /* 1 */ 88 | /***/ function(module, exports) { 89 | 90 | var path = '/material/checkbox.html'; 91 | var html = "
\r\n \r\n {{::form.title}}\r\n \r\n
\r\n"; 92 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 93 | module.exports = path; 94 | 95 | /***/ }, 96 | /* 2 */ 97 | /***/ function(module, exports) { 98 | 99 | var path = '/material/submit.html'; 100 | var html = "
\r\n \r\n {{::form.tip}}\r\n {{::form.title}}\r\n \r\n
\r\n"; 101 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 102 | module.exports = path; 103 | 104 | /***/ }, 105 | /* 3 */ 106 | /***/ function(module, exports, __webpack_require__) { 107 | 108 | __webpack_require__(22); 109 | __webpack_require__(21); 110 | __webpack_require__(20); 111 | 112 | 113 | /***/ }, 114 | /* 4 */ 115 | /***/ function(module, exports) { 116 | 117 | var path = '/material/actions.html'; 118 | var html = "
\r\n"; 119 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 120 | module.exports = path; 121 | 122 | /***/ }, 123 | /* 5 */ 124 | /***/ function(module, exports) { 125 | 126 | var path = '/material/array.html'; 127 | var html = "
\r\n \r\n \r\n \r\n = modelArray.length\"\r\n class=\"md-icon-button\" aria-label=\"More\"\r\n style=\"position: relative; z-index: 20;\">\r\n close\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n \r\n \r\n {{ form.add || 'Add'}}\r\n \r\n
\r\n
\r\n"; 128 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 129 | module.exports = path; 130 | 131 | /***/ }, 132 | /* 6 */ 133 | /***/ function(module, exports) { 134 | 135 | var path = '/material/autocomplete.html'; 136 | var html = "
\r\n \r\n \r\n {{item.name}}\r\n \r\n \r\n No matches found\r\n \r\n \r\n
\r\n"; 137 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 138 | module.exports = path; 139 | 140 | /***/ }, 141 | /* 7 */ 142 | /***/ function(module, exports) { 143 | 144 | var path = '/material/checkboxes.html'; 145 | var html = "
\r\n \r\n
\r\n \r\n {{::form.titleMap[$index].name}}\r\n \r\n
\r\n
\r\n"; 146 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 147 | module.exports = path; 148 | 149 | /***/ }, 150 | /* 8 */ 151 | /***/ function(module, exports) { 152 | 153 | var path = '/material/date.html'; 154 | var html = "
\r\n \r\n \r\n \r\n
\r\n"; 155 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 156 | module.exports = path; 157 | 158 | /***/ }, 159 | /* 9 */ 160 | /***/ function(module, exports) { 161 | 162 | var path = '/material/fieldset.html'; 163 | var html = "
\r\n {{ form.title }}\r\n
\r\n"; 164 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 165 | module.exports = path; 166 | 167 | /***/ }, 168 | /* 10 */ 169 | /***/ function(module, exports) { 170 | 171 | var path = '/material/help.html'; 172 | var html = "
\r\n"; 173 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 174 | module.exports = path; 175 | 176 | /***/ }, 177 | /* 11 */ 178 | /***/ function(module, exports) { 179 | 180 | var path = '/material/radio-buttons.html'; 181 | var html = "
\r\n
\r\n \r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n"; 182 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 183 | module.exports = path; 184 | 185 | /***/ }, 186 | /* 12 */ 187 | /***/ function(module, exports) { 188 | 189 | var path = '/material/radios-inline.html'; 190 | var html = "
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n"; 191 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 192 | module.exports = path; 193 | 194 | /***/ }, 195 | /* 13 */ 196 | /***/ function(module, exports) { 197 | 198 | var path = '/material/radios.html'; 199 | var html = "
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n"; 200 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 201 | module.exports = path; 202 | 203 | /***/ }, 204 | /* 14 */ 205 | /***/ function(module, exports) { 206 | 207 | var path = '/material/section.html'; 208 | var html = "\r\n\r\n"; 209 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 210 | module.exports = path; 211 | 212 | /***/ }, 213 | /* 15 */ 214 | /***/ function(module, exports) { 215 | 216 | var path = '/material/select.html'; 217 | var html = "\r\n \r\n \r\n \r\n {{::filtered.name}}\r\n \r\n {{::opt.name}}\r\n \r\n\r\n"; 218 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 219 | module.exports = path; 220 | 221 | /***/ }, 222 | /* 16 */ 223 | /***/ function(module, exports) { 224 | 225 | var path = '/material/switch.html'; 226 | var html = "\r\n \r\n {{::form.title}}\r\n \r\n\r\n"; 227 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 228 | module.exports = path; 229 | 230 | /***/ }, 231 | /* 17 */ 232 | /***/ function(module, exports) { 233 | 234 | var path = '/material/tabarray.html'; 235 | var html = "\r\n
\r\n
\r\n \r\n
\r\n\r\n
\r\n
\r\n
\r\n \r\n\r\n\r\n \r\n
\r\n
\r\n
\r\n\r\n \r\n\r\n
\r\n"; 236 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 237 | module.exports = path; 238 | 239 | /***/ }, 240 | /* 18 */ 241 | /***/ function(module, exports) { 242 | 243 | var path = '/material/tabs.html'; 244 | var html = "
\r\n \r\n
\r\n"; 245 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 246 | module.exports = path; 247 | 248 | /***/ }, 249 | /* 19 */ 250 | /***/ function(module, exports) { 251 | 252 | var path = '/material/textarea.html'; 253 | var html = "\r\n \r\n \r\n\r\n"; 254 | window.angular.module('ng').run(['$templateCache', function(c) { c.put(path, html) }]); 255 | module.exports = path; 256 | 257 | /***/ }, 258 | /* 20 */ 259 | /***/ function(module, exports) { 260 | 261 | angular.module('schemaForm').directive('sfMaterialClass', sfMaterialClassDirective); 262 | 263 | sfMaterialClassDirective.$inject = [ 264 | '$compile', '$timeout' 265 | ]; 266 | 267 | function sfMaterialClassDirective($compile, $timeout) { 268 | return { 269 | restrict : 'A', 270 | scope : false, 271 | link : function(scope, element, attrs, ngModel) { 272 | function reduceHelper(obj, i) {return obj[i]} 273 | 274 | var modelValue; 275 | try { 276 | modelValue = scope.form.key.reduce(reduceHelper, scope.model); 277 | } catch (e) { 278 | modelValue = undefined; 279 | } 280 | 281 | // Element class is not set in DOM if executed immediately. 282 | // I don't understand exactly why but it's probably related to other directive job. 283 | $timeout(function() { 284 | if (modelValue !== null && typeof modelValue !== 'undefined' && modelValue !== false) { 285 | element.addClass(attrs.sfMaterialClass); 286 | } 287 | }, 0); 288 | } 289 | }; 290 | } 291 | 292 | 293 | /***/ }, 294 | /* 21 */ 295 | /***/ function(module, exports, __webpack_require__) { 296 | 297 | "use strict"; 298 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__material_actions_html__ = __webpack_require__(4); 299 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__material_actions_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__material_actions_html__); 300 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__material_array_html__ = __webpack_require__(5); 301 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__material_array_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1__material_array_html__); 302 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__material_autocomplete_html__ = __webpack_require__(6); 303 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__material_autocomplete_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2__material_autocomplete_html__); 304 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__material_checkbox_html__ = __webpack_require__(1); 305 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__material_checkbox_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3__material_checkbox_html__); 306 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__material_submit_html__ = __webpack_require__(2); 307 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__material_submit_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4__material_submit_html__); 308 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__material_checkboxes_html__ = __webpack_require__(7); 309 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__material_checkboxes_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_5__material_checkboxes_html__); 310 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__material_date_html__ = __webpack_require__(8); 311 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__material_date_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_6__material_date_html__); 312 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_7__material_default_html__ = __webpack_require__(0); 313 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_7__material_default_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_7__material_default_html__); 314 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_8__material_fieldset_html__ = __webpack_require__(9); 315 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_8__material_fieldset_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_8__material_fieldset_html__); 316 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_9__material_help_html__ = __webpack_require__(10); 317 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_9__material_help_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_9__material_help_html__); 318 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_10__material_radios_html__ = __webpack_require__(13); 319 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_10__material_radios_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_10__material_radios_html__); 320 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_11__material_radios_inline_html__ = __webpack_require__(12); 321 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_11__material_radios_inline_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_11__material_radios_inline_html__); 322 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_12__material_radio_buttons_html__ = __webpack_require__(11); 323 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_12__material_radio_buttons_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_12__material_radio_buttons_html__); 324 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_13__material_section_html__ = __webpack_require__(14); 325 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_13__material_section_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_13__material_section_html__); 326 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_14__material_select_html__ = __webpack_require__(15); 327 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_14__material_select_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_14__material_select_html__); 328 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_15__material_switch_html__ = __webpack_require__(16); 329 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_15__material_switch_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_15__material_switch_html__); 330 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_16__material_tabs_html__ = __webpack_require__(18); 331 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_16__material_tabs_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_16__material_tabs_html__); 332 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_17__material_tabarray_html__ = __webpack_require__(17); 333 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_17__material_tabarray_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_17__material_tabarray_html__); 334 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_18__material_textarea_html__ = __webpack_require__(19); 335 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_18__material_textarea_html___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_18__material_textarea_html__); 336 | // ngtemplate-loader embeds the html on build 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | angular 362 | .module('schemaForm') 363 | .config(materialDecoratorConfig) 364 | .directive('sfmExternalOptions', sfmExternalOptionsDirective) 365 | .filter('sfCamelKey', sfCamelKeyFilter); 366 | 367 | materialDecoratorConfig.$inject = [ 368 | 'schemaFormProvider', 'schemaFormDecoratorsProvider', 'sfBuilderProvider', 'sfPathProvider', '$injector' 369 | ]; 370 | 371 | function materialDecoratorConfig( 372 | schemaFormProvider, decoratorsProvider, sfBuilderProvider, sfPathProvider, $injector) { 373 | var base = 'decorators/material/'; 374 | 375 | var simpleTransclusion = sfBuilderProvider.builders.simpleTransclusion; 376 | var ngModelOptions = sfBuilderProvider.builders.ngModelOptions; 377 | var ngModel = sfBuilderProvider.builders.ngModel; 378 | var sfField = sfBuilderProvider.builders.sfField; 379 | var condition = sfBuilderProvider.builders.condition; 380 | var array = sfBuilderProvider.builders.array; 381 | var numeric = sfBuilderProvider.builders.numeric; 382 | 383 | var sfLayout = sfLayout; 384 | var sfMessagesNode = sfMessagesNodeHandler(); 385 | var sfMessages = sfMessagesBuilder; 386 | var sfOptions = sfOptionsBuilder; 387 | var mdAutocomplete = mdAutocompleteBuilder; 388 | var mdSwitch = mdSwitchBuilder; 389 | var mdDatepicker = mdDatepickerBuilder; 390 | var mdTabs = mdTabsBuilder; 391 | var textarea = textareaBuilder; 392 | 393 | var core = [ sfField, ngModel, ngModelOptions, condition, sfLayout ]; 394 | var defaults = core.concat(sfMessages); 395 | var arrays = core.concat(array); 396 | 397 | schemaFormProvider.defaults.string.unshift(dateDefault); 398 | 399 | decoratorsProvider.defineDecorator('materialDecorator', { 400 | actions: { template: __WEBPACK_IMPORTED_MODULE_0__material_actions_html___default.a, builder: [ sfField, simpleTransclusion, condition ] }, 401 | array: { template: __WEBPACK_IMPORTED_MODULE_1__material_array_html___default.a, builder: arrays }, 402 | autocomplete: { template: __WEBPACK_IMPORTED_MODULE_2__material_autocomplete_html___default.a, builder: defaults.concat(mdAutocomplete) }, 403 | boolean: { template: __WEBPACK_IMPORTED_MODULE_3__material_checkbox_html___default.a, builder: defaults }, 404 | button: { template: __WEBPACK_IMPORTED_MODULE_4__material_submit_html___default.a, builder: defaults }, 405 | checkbox: { template: __WEBPACK_IMPORTED_MODULE_3__material_checkbox_html___default.a, builder: defaults }, 406 | checkboxes: { template: __WEBPACK_IMPORTED_MODULE_5__material_checkboxes_html___default.a, builder: arrays }, 407 | date: { template: __WEBPACK_IMPORTED_MODULE_6__material_date_html___default.a, builder: defaults.concat(mdDatepicker) }, 408 | 'default': { template: __WEBPACK_IMPORTED_MODULE_7__material_default_html___default.a, builder: defaults }, 409 | fieldset: { template: __WEBPACK_IMPORTED_MODULE_8__material_fieldset_html___default.a, builder: [ sfField, simpleTransclusion, condition ] }, 410 | help: { template: __WEBPACK_IMPORTED_MODULE_9__material_help_html___default.a, builder: defaults }, 411 | number: { template: __WEBPACK_IMPORTED_MODULE_7__material_default_html___default.a, builder: defaults.concat(numeric) }, 412 | password: { template: __WEBPACK_IMPORTED_MODULE_7__material_default_html___default.a, builder: defaults }, 413 | radios: { template: __WEBPACK_IMPORTED_MODULE_10__material_radios_html___default.a, builder: defaults }, 414 | 'radios-inline': { template: __WEBPACK_IMPORTED_MODULE_11__material_radios_inline_html___default.a, builder: defaults }, 415 | radiobuttons: { template: __WEBPACK_IMPORTED_MODULE_12__material_radio_buttons_html___default.a, builder: defaults }, 416 | section: { template: __WEBPACK_IMPORTED_MODULE_13__material_section_html___default.a, builder: [ sfField, simpleTransclusion, condition, sfLayout ] }, 417 | select: { template: __WEBPACK_IMPORTED_MODULE_14__material_select_html___default.a, builder: defaults.concat(sfOptions) }, 418 | submit: { template: __WEBPACK_IMPORTED_MODULE_4__material_submit_html___default.a, builder: defaults }, 419 | tabs: { template: __WEBPACK_IMPORTED_MODULE_16__material_tabs_html___default.a, builder: [ sfField, mdTabs, condition ] }, 420 | tabarray: { template: __WEBPACK_IMPORTED_MODULE_17__material_tabarray_html___default.a, builder: arrays }, 421 | textarea: { template: __WEBPACK_IMPORTED_MODULE_18__material_textarea_html___default.a, builder: defaults.concat(textarea) }, 422 | 'switch': { template: __WEBPACK_IMPORTED_MODULE_15__material_switch_html___default.a, builder: defaults.concat(mdSwitch) } 423 | }); 424 | 425 | function sfLayout(args) { 426 | var layoutDiv = args.fieldFrag.querySelector('[sf-layout]'); 427 | 428 | if (args.form.grid) { 429 | Object.getOwnPropertyNames(args.form.grid).forEach(function(property, idx, array) { 430 | layoutDiv.setAttribute(property, args.form.grid[property]); 431 | }); 432 | }; 433 | }; 434 | 435 | function sfMessagesNodeHandler() { 436 | var html = '
'; 437 | var div = document.createElement('div'); 438 | div.innerHTML = html; 439 | return div.firstChild; 440 | }; 441 | 442 | function sfMessagesBuilder(args) { 443 | var messagesDiv = args.fieldFrag.querySelector('[sf-messages]'); 444 | if (messagesDiv && sfMessagesNode) { 445 | var child = sfMessagesNode.cloneNode(); 446 | messagesDiv.appendChild(child); 447 | } 448 | }; 449 | 450 | function textareaBuilder(args) { 451 | var textareaFrag = args.fieldFrag.querySelector('textarea'); 452 | var maxLength = args.form.maxlength || false; 453 | if (textareaFrag && maxLength) { 454 | textareaFrag.setAttribute('md-maxlength', maxLength); 455 | }; 456 | }; 457 | 458 | function mdAutocompleteBuilder(args) { 459 | var mdAutocompleteFrag = args.fieldFrag.querySelector('md-autocomplete'); 460 | var minLength = args.form.minLength || 1; 461 | var maxLength = args.form.maxLength || false; 462 | var title = args.form.title || args.form.placeholder || args.form.key.slice(-1)[0]; 463 | 464 | if (mdAutocompleteFrag) { 465 | if (args.form.onChange) { 466 | mdAutocompleteFrag.setAttribute('md-selected-item-change', 'args.form.onChange()'); 467 | mdAutocompleteFrag.setAttribute('md-search-text-change', 'args.form.onChange(searchText)'); 468 | }; 469 | 470 | // mdAutocompleteFrag.setAttribute('md-items', 'item in $filter(''autocomplete'')(searchText);'); 471 | mdAutocompleteFrag.setAttribute('md-min-length', minLength); 472 | if (maxLength) { 473 | mdAutocompleteFrag.setAttribute('md-max-length', maxLength); 474 | }; 475 | 476 | if (title) { 477 | mdAutocompleteFrag.setAttribute('md-floating-label', title); 478 | }; 479 | }; 480 | }; 481 | 482 | function mdSwitchBuilder(args) { 483 | var mdSwitchFrag = args.fieldFrag.querySelector('md-switch'); 484 | if (args.form.schema.titleMap) { 485 | mdSwitchFrag.setAttribute('ng-true-value', args.form.schema.titleMap.true); 486 | mdSwitchFrag.setAttribute('ng-false-value', args.form.schema.titleMap.false); 487 | }; 488 | }; 489 | 490 | function sfOptionsBuilder(args) { 491 | var mdSelectFrag = args.fieldFrag.querySelector('md-select'); 492 | var enumTitleMap = []; 493 | var i; 494 | var mdSelectFrag; 495 | 496 | args.form.selectOptions = []; 497 | args.form.getOptions = getOptionsHandler; 498 | 499 | if (args.form.schema.links && (typeof args.form.schema.links) === 'object') { 500 | var link; 501 | var related = /({)([^}]*)(})/gm; 502 | var source = /{{([^}]*)}}/gm; 503 | var matched; 504 | 505 | for (i = 0; i < args.form.schema.links.length; i++) { 506 | link = args.form.schema.links[i]; 507 | if (link.rel === 'options') { 508 | // TODO enable filter to allow processing results 509 | // args.form.optionSource = link.href.replace(related, '$1$1 model.$2 | _externalOptionUri $3$3'); 510 | args.form.optionSource = link.href.replace(related, '$1$1 model.$2 $3$3'); 511 | }; 512 | }; 513 | 514 | mdSelectFrag.setAttribute('sfm-external-options', args.form.optionSource); 515 | } 516 | else { 517 | args.form.selectOptions = sfOptionsProcessor(args.form); 518 | }; 519 | }; 520 | 521 | function mdDatepickerBuilder(args) { 522 | var mdDatepickerFrag = args.fieldFrag.querySelector('md-datepicker'); 523 | if (mdDatepickerFrag) { 524 | if (args.form.onChange) { 525 | mdDatepickerFrag.setAttribute('ng-change', 'args.form.onChange(searchText)'); 526 | } 527 | // mdDatepickerFrag.setAttribute('md-items', 'item in $filter(''autocomplete'')(searchText);'); 528 | var minDate = args.form.minimum || false; 529 | var maxDate = args.form.maximum || false; 530 | if (minDate) { 531 | mdDatepickerFrag.setAttribute('md-max-date', minDate); 532 | } 533 | if (maxDate) { 534 | mdDatepickerFrag.setAttribute('md-max-date', maxDate); 535 | } 536 | } 537 | }; 538 | 539 | function mdTabsBuilder(args) { 540 | if (args.form.tabs && args.form.tabs.length > 0) { 541 | var mdTabsFrag = args.fieldFrag.querySelector('md-tabs'); 542 | 543 | args.form.tabs.forEach(function(tab, index) { 544 | var evalExpr = '(evalExpr(' + args.path + '.tabs[' + index + ']' + 545 | '.condition, { model: model, "arrayIndex": $index}))'; 546 | var mdTab = document.createElement('md-tab'); 547 | if(!!tab.condition) { 548 | mdTab.setAttribute('ng-if', evalExpr); 549 | }; 550 | mdTab.setAttribute('label', '{{' + args.path + '.tabs[' + index + '].title}}'); 551 | var mdTabBody = document.createElement('md-tab-body'); 552 | var childFrag = args.build(tab.items, args.path + '.tabs[' + index + '].items', args.state); 553 | mdTabBody.appendChild(childFrag); 554 | mdTab.appendChild(mdTabBody); 555 | mdTabsFrag.appendChild(mdTab); 556 | }); 557 | } 558 | }; 559 | 560 | /** 561 | * Material Datepicker 562 | */ 563 | function dateDefault(name, schema, options) { 564 | if (schema.type === 'string' && (schema.format === 'date' || schema.format === 'date-time')) { 565 | var f = schemaFormProvider.stdFormObj(name, schema, options); 566 | f.key = options.path; 567 | f.type = 'date'; 568 | options.lookup[sfPathProvider.stringify(options.path)] = f; 569 | return f; 570 | } 571 | }; 572 | }; 573 | 574 | function getOptionsHandler(form, evalExpr) { 575 | if (form.optionData) { 576 | return evalExpr(form.optionData); 577 | }; 578 | 579 | if (form.selectOptions) { 580 | return form.selectOptions; 581 | }; 582 | 583 | return []; 584 | }; 585 | 586 | function sfOptionsProcessor(data) { 587 | var enumTitleMap = []; 588 | 589 | if (data.titleMap) { 590 | return data.titleMap; 591 | } 592 | else if (data.enum && data.enum.length) { 593 | for (i = 0; i < data.enum.length; i++) { 594 | if (data.enum[i] && data.enum[i].length) { 595 | enumTitleMap.push({ name: data.enum[i], value: data.enum[i] }); 596 | }; 597 | }; 598 | }; 599 | 600 | return enumTitleMap; 601 | }; 602 | 603 | sfmExternalOptionsDirective.$inject = [ '$http' ]; 604 | 605 | function sfmExternalOptionsDirective($http) { 606 | var directive = { 607 | link: link, 608 | restrict: 'A' 609 | }; 610 | 611 | return directive; 612 | 613 | function link(scope, element, attrs) { 614 | attrs.$observe('sfmExternalOptions', function(dataURI) { 615 | $http.get(dataURI) 616 | .then(function(response) { 617 | scope.form.selectOptions = sfOptionsProcessor(response.data); 618 | }); 619 | }); 620 | }; 621 | }; 622 | 623 | /** 624 | * sfCamelKey Filter 625 | */ 626 | function sfCamelKeyFilter() { 627 | return function(formKey) { 628 | if (!formKey) { return ''; }; 629 | var part, i, key; 630 | key = formKey.slice(); 631 | for (i = 0; i < key.length; i++) { 632 | part = key[i].toLowerCase().split(''); 633 | if (i && part.length) { part[0] = part[0].toUpperCase(); }; 634 | key[i] = part.join(''); 635 | }; 636 | return key.join(''); 637 | }; 638 | }; 639 | 640 | /* 641 | TODO add default filter for autocomplete which allows form.optionFilter or 'autocompleteFilter' to override 642 | Something along the following lines... 643 | if ($injector.has('autocompleteFilter')) { 644 | result = $filter('autocomplete')(input); 645 | } 646 | else 647 | if ($injector.has(args.form.optionFilter + 'Filter')) { 648 | result = $filter(args.form.optionFilter)(input); 649 | } 650 | else { 651 | if (args.form.optionFilter) { 652 | mdAutocomplete.setAttribute('md-items', 653 | 'item in evalExpr("this[\""+form.optionFilter+"\"](\""+searchText+"\")")'); 654 | } 655 | } 656 | 657 | .filter('autocompleteMovieTest', function() { 658 | function autocompleteMovieTestFilter(array, input){ 659 | var current = input; 660 | // You could also call multiple filters here using: 661 | // current = $filter('filterName')(input) 662 | if(typeof current === 'string') { 663 | current = current.replace(' ','-').toLowerCase(); 664 | } 665 | current = (!current) ? '_undefined' : current; 666 | return current; 667 | } 668 | 669 | return externalOptionUriFilter; 670 | }) 671 | */ 672 | 673 | 674 | /***/ }, 675 | /* 22 */ 676 | /***/ function(module, exports) { 677 | 678 | /** 679 | * It might be a bug, but currently input[type=number] does not add 680 | * a parser, so the model gets populated with a string. It does however stop non numbers so it 681 | * must have some preproccessing. Anyway, this adds parser before schema-validate hooks into it. 682 | * FIXME: this is still not a complete solution. Inputting a string in an input[type=number] results 683 | * in parsers never firing and ngModel value removed. So no validation from schema-validate either. 684 | */ 685 | angular.module('schemaForm').directive('sfTypeParser', function() { 686 | return { 687 | restrict: 'A', 688 | scope: false, 689 | require: 'ngModel', 690 | link: function(scope, element, attrs, ngModel) { 691 | var once = scope.$watch(attrs.sfTypeParser, function(schema) { 692 | if (!schema) { 693 | return; 694 | } 695 | 696 | var isNumber = schema.type.indexOf('number') !== -1; 697 | var isInteger = schema.type.indexOf('integer') !== -1; 698 | var numberRE = /^[0-9]*$/; 699 | // Use index of since type can be either an array with two values or a string. 700 | if (isNumber || isInteger) { 701 | // The timing here seems to work. i.e. we get in before schema-validate 702 | ngModel.$parsers.push(function(viewValue) { 703 | var value; 704 | if (isNumber) { 705 | value = parseFloat(viewValue); 706 | } else if (numberRE.test(viewValue)) { 707 | // We test the value to check that it's a valid integer, otherwise we can easily 708 | // get float -> integer parsing behind the scenes. 709 | value = parseInt(viewValue, 10); 710 | } 711 | console.log('parser', numberRE.test(viewValue), viewValue, value) 712 | if (value === undefined || isNaN(value)) { 713 | //Let the validation fail. @FIXME: it fails with "required" for some reason. 714 | return viewValue; 715 | } 716 | return value; 717 | }); 718 | } 719 | 720 | once(); 721 | }); 722 | } 723 | }; 724 | }); 725 | 726 | 727 | /***/ }, 728 | /* 23 */, 729 | /* 24 */ 730 | /***/ function(module, exports, __webpack_require__) { 731 | 732 | module.exports = __webpack_require__(3); 733 | 734 | 735 | /***/ } 736 | /******/ ]); -------------------------------------------------------------------------------- /dist/angular-schema-form-material.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-schema-form-material 3 | * @version 1.0.0-alpha.2 4 | * @date Mon, 02 Jan 2017 12:21:23 GMT 5 | * @link https://github.com/json-schema-form/angular-schema-form-material 6 | * @license MIT 7 | * Copyright (c) 2014-2017 JSON Schema Form 8 | */ 9 | !function(e){function r(t){if(n[t])return n[t].exports;var a=n[t]={i:t,l:!1,exports:{}};return e[t].call(a.exports,a,a.exports,r),a.l=!0,a.exports}var n={};return r.m=e,r.c=n,r.i=function(e){return e},r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},r.p="",r(r.s=27)}([function(e,r){var n="/material/default.html",t='\r\n \r\n \r\n\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/checkbox.html",t='
\r\n \r\n {{::form.title}}\r\n \r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/submit.html",t='
\r\n \r\n {{::form.tip}}\r\n {{::form.title}}\r\n \r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r,n){n(22),n(21),n(20)},function(e,r){var n="/material/actions.html",t='
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/array.html",t='
\r\n \r\n \r\n \r\n \r\n close\r\n \r\n \r\n \r\n
\r\n
\r\n\r\n \r\n \r\n {{ form.add || \'Add\'}}\r\n \r\n
\r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/autocomplete.html",t='
\r\n \r\n \r\n {{item.name}}\r\n \r\n \r\n No matches found\r\n \r\n \r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/checkboxes.html",t='
\r\n \r\n
\r\n \r\n {{::form.titleMap[$index].name}}\r\n \r\n
\r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/date.html",t='
\r\n \r\n \r\n \r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/fieldset.html",t='
\r\n {{ form.title }}\r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/help.html",t='
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/radio-buttons.html",t='
\r\n
\r\n \r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/radios-inline.html",t='
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/radios.html",t='
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/section.html",t='\r\n\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/select.html",t='\r\n \r\n \r\n \r\n {{::filtered.name}}\r\n \r\n {{::opt.name}}\r\n \r\n\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/switch.html",t='\r\n \r\n {{::form.title}}\r\n \r\n\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/tabarray.html",t='\r\n
\r\n
\r\n \r\n
\r\n\r\n
\r\n
\r\n
\r\n \r\n\r\n\r\n \r\n
\r\n
\r\n
\r\n\r\n \r\n\r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/tabs.html",t='
\r\n \r\n
\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){var n="/material/textarea.html",t='\r\n \r\n \r\n\r\n';window.angular.module("ng").run(["$templateCache",function(e){e.put(n,t)}]),e.exports=n},function(e,r){function n(e,r){return{restrict:"A",scope:!1,link:function(e,n,t,a){function l(e,r){return e[r]}var o;try{o=e.form.key.reduce(l,e.model)}catch(e){o=void 0}r(function(){null!==o&&"undefined"!=typeof o&&o!==!1&&n.addClass(t.sfMaterialClass)},0)}}}angular.module("schemaForm").directive("sfMaterialClass",n),n.$inject=["$compile","$timeout"]},function(e,r,n){"use strict";function t(e,r,n,t,o){function s(e){var r=e.fieldFrag.querySelector("[sf-layout]");e.form.grid&&Object.getOwnPropertyNames(e.form.grid).forEach(function(n,t,a){r.setAttribute(n,e.form.grid[n])})}function i(){var e='
',r=document.createElement("div");return r.innerHTML=e,r.firstChild}function m(e){var r=e.fieldFrag.querySelector("[sf-messages]");if(r&&N){var n=N.cloneNode();r.appendChild(n)}}function c(e){var r=e.fieldFrag.querySelector("textarea"),n=e.form.maxlength||!1;r&&n&&r.setAttribute("md-maxlength",n)}function u(e){var r=e.fieldFrag.querySelector("md-autocomplete"),n=e.form.minLength||1,t=e.form.maxLength||!1,a=e.form.title||e.form.placeholder||e.form.key.slice(-1)[0];r&&(e.form.onChange&&(r.setAttribute("md-selected-item-change","args.form.onChange()"),r.setAttribute("md-search-text-change","args.form.onChange(searchText)")),r.setAttribute("md-min-length",n),t&&r.setAttribute("md-max-length",t),a&&r.setAttribute("md-floating-label",a))}function h(e){var r=e.fieldFrag.querySelector("md-switch");e.form.schema.titleMap&&(r.setAttribute("ng-true-value",e.form.schema.titleMap.true),r.setAttribute("ng-false-value",e.form.schema.titleMap.false))}function b(e){var r,n,n=e.fieldFrag.querySelector("md-select");if(e.form.selectOptions=[],e.form.getOptions=a,e.form.schema.links&&"object"==typeof e.form.schema.links){var t,o=/({)([^}]*)(})/gm;for(r=0;r0){var r=e.fieldFrag.querySelector("md-tabs");e.form.tabs.forEach(function(n,t){var a="(evalExpr("+e.path+".tabs["+t+'].condition, { model: model, "arrayIndex": $index}))',l=document.createElement("md-tab");n.condition&&l.setAttribute("ng-if",a),l.setAttribute("label","{{"+e.path+".tabs["+t+"].title}}");var o=document.createElement("md-tab-body"),s=e.build(n.items,e.path+".tabs["+t+"].items",e.state);o.appendChild(s),l.appendChild(o),r.appendChild(l)})}}function C(r,n,a){if("string"===n.type&&("date"===n.format||"date-time"===n.format)){var l=e.stdFormObj(r,n,a);return l.key=a.path,l.type="date",a.lookup[t.stringify(a.path)]=l,l}}var A=n.builders.simpleTransclusion,M=n.builders.ngModelOptions,O=n.builders.ngModel,S=n.builders.sfField,K=n.builders.condition,P=n.builders.array,D=n.builders.numeric,s=s,N=i(),B=m,V=b,z=u,U=h,W=y,J=$,Q=c,X=[S,O,M,K,s],Y=X.concat(B),Z=X.concat(P);e.defaults.string.unshift(C),r.defineDecorator("materialDecorator",{actions:{template:d.a,builder:[S,A,K]},array:{template:f.a,builder:Z},autocomplete:{template:p.a,builder:Y.concat(z)},boolean:{template:g.a,builder:Y},button:{template:v.a,builder:Y},checkbox:{template:g.a,builder:Y},checkboxes:{template:x.a,builder:Z},date:{template:w.a,builder:Y.concat(W)},default:{template:k.a,builder:Y},fieldset:{template:T.a,builder:[S,A,K]},help:{template:E.a,builder:Y},number:{template:k.a,builder:Y.concat(D)},password:{template:k.a,builder:Y},radios:{template:F.a,builder:Y},"radios-inline":{template:j.a,builder:Y},radiobuttons:{template:H.a,builder:Y},section:{template:q.a,builder:[S,A,K,s]},select:{template:I.a,builder:Y.concat(V)},submit:{template:v.a,builder:Y},tabs:{template:G.a,builder:[S,J,K]},tabarray:{template:_.a,builder:Z},textarea:{template:R.a,builder:Y.concat(Q)},switch:{template:L.a,builder:Y.concat(U)}})}function a(e,r){return e.optionData?r(e.optionData):e.selectOptions?e.selectOptions:[]}function l(e){var r=[];if(e.titleMap)return e.titleMap;if(e.enum&&e.enum.length)for(i=0;iArray Example

Try adding a couple of forms, reorder by drag'n'drop.

" 44 | }, 45 | { 46 | "key": "comments", 47 | "add": "New", 48 | "style": { 49 | "add": "btn-success" 50 | }, 51 | "items": [ 52 | "comments[].name", 53 | "comments[].email", 54 | { 55 | "key": "comments[].spam", 56 | "type": "checkbox", 57 | "title": "Yes I want spam.", 58 | "condition": "model.comments[arrayIndex].email" 59 | }, 60 | { 61 | "key": "comments[].comment", 62 | "type": "textarea" 63 | } 64 | ] 65 | }, 66 | { 67 | "type": "submit", 68 | "style": "btn-info", 69 | "title": "OK" 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /examples/data/autocomplete.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "title": "Arnold Schwarzenegger", 5 | "properties": { 6 | "first": { 7 | "title": "What is your favourite Schwarzenegger movie?", 8 | "type": "object" 9 | }, 10 | "second": { 11 | "title": "What is your favourite Schwarzenegger movie?", 12 | "type": "object" 13 | } 14 | } 15 | }, 16 | "form": [ 17 | { 18 | "key": "first", 19 | "type": "autocomplete", 20 | "optionFilter": "querySearch" 21 | }, 22 | { 23 | "type": "help", 24 | "helpvalue": "I only need at least 1 character" 25 | }, 26 | { 27 | "title": "What is your second favourite Schwarzenegger movie?", 28 | "key": "second", 29 | "type": "autocomplete", 30 | "optionFilter": "querySearch", 31 | "minLength": 4 32 | }, 33 | { 34 | "type": "help", 35 | "helpvalue": "I need at least 4 non-space characters" 36 | }, 37 | { 38 | "type": "submit", 39 | "style": "btn-info", 40 | "title": "OK" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /examples/data/complex-keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "title": "Complex Key Support", 5 | "properties": { 6 | "a[\"b\"].c": { 7 | "type": "string" 8 | }, 9 | "simple": { 10 | "type": "object", 11 | "properties": { 12 | "prøp": { 13 | "title": "UTF8 in both dot and bracket notation", 14 | "type": "string" 15 | } 16 | } 17 | }, 18 | "array-key": { 19 | "type": "array", 20 | "items": { 21 | "type": "object", 22 | "properties": { 23 | "a'rr[\"l": { 24 | "title": "Control Characters", 25 | "type": "string" 26 | }, 27 | "˙∆∂∞˚¬": { 28 | "type": "string" 29 | } 30 | }, 31 | "required": [ 32 | "a'rr[\"l", 33 | "˙∆∂∞˚¬" 34 | ] 35 | } 36 | } 37 | } 38 | }, 39 | "form": [ 40 | { 41 | "type": "help", 42 | "helpvalue": "Complex keys are only supported with AngularJS version 1.3.x, see known limitations in the docs." 43 | }, 44 | "['a[\"b\"].c']", 45 | { 46 | "key": "array-key", 47 | "items": [ 48 | "['array-key'][]['a'rr[\"l']", 49 | { 50 | "key": "['array-key'][]['˙∆∂∞˚¬']", 51 | "title": "Unicode Characters" 52 | } 53 | ] 54 | }, 55 | { 56 | "key": "simple", 57 | "items": [ 58 | "simple.prøp" 59 | ] 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /examples/data/conditional-required.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "properties": { 5 | "switch": { 6 | "title": "Spam me, please", 7 | "type": "boolean" 8 | }, 9 | "email": { 10 | "title": "Email", 11 | "type": "string", 12 | "pattern": "^\\S+@\\S+$", 13 | "description": "Email will be used for evil." 14 | } 15 | }, 16 | "required": ["switch"] 17 | }, 18 | "form": [ 19 | { 20 | "type": "help", 21 | "helpvalue": "

Schema Form does not support oneOf (yet), but you can do a workaround and simulate certain scenarios with 'condition' and 'required' (and/or 'readonly') in the form.

" 22 | }, 23 | "switch", 24 | { 25 | "key": "email", 26 | "condition": "model.switch", 27 | "required": true 28 | }, 29 | { 30 | "key": "email", 31 | "condition": "!model.switch" 32 | }, 33 | { 34 | "type": "submit", 35 | "style": "btn-info", 36 | "title": "OK" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /examples/data/grid.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "title": "Comment", 5 | "properties": { 6 | "firstname": { 7 | "title": "Name", 8 | "type": "string" 9 | }, 10 | "lastname": { 11 | "title": "Surname", 12 | "type": "string" 13 | }, 14 | "address_line_1": { 15 | "title": "Address", 16 | "type": "string" 17 | }, 18 | "address_line_2": { 19 | "type": "string" 20 | }, 21 | "city": { 22 | "type": "string" 23 | }, 24 | "state": { 25 | "type": "string", 26 | "enum": ["VIC","NSW","QLD","WA","SA","TAS","ACT","NT"] 27 | }, 28 | "postcode": { 29 | "type": "integer", 30 | "minimum":1000, 31 | "maximum":9999 32 | }, 33 | "email": { 34 | "title": "Email", 35 | "type": "string", 36 | "pattern": "^\\S+@\\S+$", 37 | "description": "Email will be used for evil." 38 | }, 39 | "comment": { 40 | "title": "Comment", 41 | "type": "string", 42 | "maxLength": 20, 43 | "validationMessage": "Don't be greedy!" 44 | } 45 | }, 46 | "required": ["name","email","comment"] 47 | }, 48 | "form": [ 49 | { 50 | "type": "help", 51 | "helpvalue": "
Grid it up with FLEX
" 52 | }, 53 | { 54 | "type": "section", 55 | "grid": { "layout": "column" }, 56 | "items": [ 57 | { 58 | "type": "section", 59 | "grid": { "layout": "row" }, 60 | "items": [ 61 | { 62 | "key": "firstname", 63 | "grid": { "flex": 50 } 64 | }, 65 | { 66 | "key": "lastname", 67 | "grid": { "flex": "" } 68 | } 69 | ] 70 | }, 71 | { 72 | "key": "address_line_1", 73 | "grid": { "flex": 100, "layout": "row" } 74 | }, 75 | { 76 | "key": "address_line_2", 77 | "grid": { "flex": 100, "layout": "row" } 78 | }, 79 | { 80 | "type": "section", 81 | "grid": { "layout": "row" }, 82 | "items": [ 83 | { 84 | "key": "city", 85 | "grid": { "flex": 33, "layout": "column" } 86 | }, 87 | { 88 | "key": "state", 89 | "type":"select", 90 | "grid": { "flex": 33, "layout": "column" } 91 | }, 92 | { 93 | "key": "postcode", 94 | "grid": { "flex": 33, "layout": "column" } 95 | } 96 | ] 97 | }, 98 | { 99 | "type": "section", 100 | "grid": { "layout": "row" }, 101 | "items": ["email"] 102 | }, 103 | { 104 | "key": "comment", 105 | "type": "textarea", 106 | "placeholder": "Make a comment" 107 | } 108 | ] 109 | }, 110 | { 111 | "type": "submit", 112 | "style": "btn-info", 113 | "title": "OK" 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /examples/data/remote-titlemap-books.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleMap": [ 3 | { "value": "jkr1", "name": "Harry Potter", "group": "Childrens" }, 4 | { "value": "eb1", "name": "Blinky Bill", "group": "Childrens" }, 5 | { "value": "da1", "name": "A Hitchhiker's Guide To The Galaxy", "group": "Sci-Fi" }, 6 | { "value": "ow1", "name": "The Picture of Dorian Gray", "group": "Horror" }, 7 | { "value": "wc", "name": "A Nightmare On Elm Street", "group": "Horror" } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/data/remote-titlemap-movies.json: -------------------------------------------------------------------------------- 1 | { 2 | "titleMap": [ 3 | { "value": "ss1", "name": "Demolition Man", "group": "Stallone" }, 4 | { "value": "ss2", "name": "Rocky", "group": "Stallone" }, 5 | { "value": "ss3", "name": "Rambo", "group": "Stallone" }, 6 | { "value": "jcvd1", "name": "Double Impact", "group": "Van Damme" }, 7 | { "value": "jcvd2", "name": "Bloodsport", "group": "Van Damme" }, 8 | { "value": "jcvd3", "name": "Kickboxer", "group": "Van Damme" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/data/simple-oneOf.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "title": "Comment", 5 | "properties": { 6 | "name": { 7 | "oneOf": [ 8 | { 9 | "type": "string", 10 | "enum": ["Bob"] 11 | }, 12 | { 13 | "type": "string", 14 | "pattern": "^\\S+ \\S+$" 15 | } 16 | ] 17 | } 18 | }, 19 | "required": ["name"] 20 | }, 21 | "form": [ 22 | { 23 | "key": "name", 24 | "title": "Name" 25 | } 26 | 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /examples/data/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "title": "Comment", 5 | "properties": { 6 | "name": { 7 | "title": "Name", 8 | "type": "string" 9 | }, 10 | "date": { 11 | "title": "Date", 12 | "type": "string", 13 | "format":"date" 14 | }, 15 | "email": { 16 | "title": "Email", 17 | "type": "string", 18 | "pattern": "^\\S+@\\S+$", 19 | "description": "Email will be used for evil." 20 | }, 21 | "comment": { 22 | "title": "Comment", 23 | "type": "string", 24 | "maxLength": 20, 25 | "validationMessage": "Don't be greedy!" 26 | } 27 | }, 28 | "required": ["name","email","comment"] 29 | }, 30 | "form": [ 31 | "name", 32 | "date", 33 | "email", 34 | { 35 | "key": "comment", 36 | "type": "textarea", 37 | "placeholder": "Make a comment" 38 | }, 39 | { 40 | "type": "submit", 41 | "style": "btn-info", 42 | "title": "OK" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /examples/data/sink.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "required": [ 5 | "name", 6 | "shoesizeLeft" 7 | ], 8 | "properties": { 9 | "name": { 10 | "title": "Name", 11 | "description": "Gimme yea name lad", 12 | "type": "string", 13 | "pattern": "^[^/]*$", 14 | "minLength": 2 15 | }, 16 | "invitation": { 17 | "type": "string", 18 | "format": "html", 19 | "title": "Invitation Design", 20 | "description": "Design the invitation in full technicolor HTML" 21 | }, 22 | "favorite": { 23 | "title": "Favorite", 24 | "type": "string", 25 | "enum": [ 26 | "undefined", 27 | "null", 28 | "NaN" 29 | ] 30 | }, 31 | "shoesizeLeft": { 32 | "title": "Shoe size (left)", 33 | "default": 42, 34 | "type": "number" 35 | }, 36 | "shoesizeRight": { 37 | "title": "Shoe size (right)", 38 | "default": 42, 39 | "type": "number" 40 | }, 41 | "attributes": { 42 | "type": "object", 43 | "title": "Attributes", 44 | "required": [ 45 | "eyecolor" 46 | ], 47 | "properties": { 48 | "eyecolor": { 49 | "type": "string", 50 | "format": "color", 51 | "title": "Eye color", 52 | "default": "pink" 53 | }, 54 | "haircolor": { 55 | "type": "string", 56 | "title": "Hair color" 57 | }, 58 | "shoulders": { 59 | "type": "object", 60 | "title": "Shoulders", 61 | "properties": { 62 | "left": { 63 | "type": "string", 64 | "title": "Left" 65 | }, 66 | "right": { 67 | "type": "string", 68 | "title": "Right" 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | "things": { 75 | "type": "array", 76 | "title": "I like...", 77 | "items": { 78 | "type": "string", 79 | "enum": [ 80 | "clowns", 81 | "compiling", 82 | "sleeping" 83 | ] 84 | } 85 | }, 86 | "dislike": { 87 | "type": "array", 88 | "title": "I dislike...", 89 | "items": { 90 | "type": "string", 91 | "title": "I hate" 92 | } 93 | }, 94 | "soul": { 95 | "title": "Terms Of Service", 96 | "description": "I agree to sell my undying soul", 97 | "type": "boolean", 98 | "default": true 99 | }, 100 | "soulserial": { 101 | "title": "Soul Serial No", 102 | "type": "string" 103 | }, 104 | "date": { 105 | "title": "Date of party", 106 | "type": "string", 107 | "format": "date" 108 | }, 109 | "radio": { 110 | "title": "Radio type", 111 | "type": "string", 112 | "enum": [ 113 | "Transistor", 114 | "Tube" 115 | ] 116 | }, 117 | "radio2": { 118 | "title": "My Second Radio", 119 | "type": "string", 120 | "enum": [ 121 | "Transistor", 122 | "Tube" 123 | ] 124 | }, 125 | "radiobuttons": { 126 | "type": "string", 127 | "enum": [ 128 | "Select me!", 129 | "No me!" 130 | ] 131 | } 132 | } 133 | }, 134 | "form": [ 135 | { 136 | "type": "fieldset", 137 | "title": "Stuff", 138 | "items": [ 139 | { 140 | "type": "tabs", 141 | "tabs": [ 142 | { 143 | "title": "Simple stuff", 144 | "items": [ 145 | { 146 | "key": "name", 147 | "placeholder": "Check the console", 148 | "onChange": "log(modelValue)", 149 | "feedback": "{'glyphicon': true, 'glyphicon-ok': hasSuccess(), 'glyphicon-star': !hasSuccess() }" 150 | }, 151 | { 152 | "key": "favorite", 153 | "feedback": false 154 | } 155 | ] 156 | }, 157 | { 158 | "title": "More stuff", 159 | "items": [ 160 | "attributes.eyecolor", 161 | "attributes.haircolor", 162 | { 163 | "key": "attributes.shoulders.left", 164 | "title": "Left shoulder", 165 | "description": "This value is copied to attributes.shoulders.right in the model", 166 | "copyValueTo": ["attributes.shoulders.right"] 167 | }, 168 | { 169 | "key": "shoesizeLeft", 170 | "feedback": false, 171 | "copyValueTo":["shoesizeRight"] 172 | }, 173 | { 174 | "key": "shoesizeRight" 175 | }, 176 | "things", 177 | "dislike" 178 | ] 179 | } 180 | ] 181 | } 182 | ] 183 | }, 184 | { 185 | "type": "help", 186 | "helpvalue": "
" 187 | }, 188 | "soul", 189 | { 190 | "condition": "modelData.soul", 191 | "key": "soulserial", 192 | "placeholder": "ex. 666" 193 | }, 194 | { 195 | "key": "date", 196 | "minDate": "2015-06-20" 197 | }, 198 | { 199 | "key": "radio", 200 | "type": "radios", 201 | "titleMap": [ 202 | { 203 | "value": "Transistor", 204 | "name": "Transistor
Not the tube kind." 205 | }, 206 | { 207 | "value": "Tube", 208 | "name": "Tube
The tube kind." 209 | } 210 | ] 211 | }, 212 | { 213 | "key": "radio2", 214 | "type": "radios-inline", 215 | "titleMap": [ 216 | { 217 | "value": "Transistor", 218 | "name": "Transistor
Not the tube kind." 219 | }, 220 | { 221 | "value": "Tube", 222 | "name": "Tube
The tube kind." 223 | } 224 | ] 225 | }, 226 | { 227 | "key": "radiobuttons", 228 | "style": { 229 | "selected": "btn-success", 230 | "unselected": "btn-default" 231 | }, 232 | "type": "radiobuttons", 233 | "notitle": true 234 | }, 235 | { 236 | "type": "actions", 237 | "items": [ 238 | { 239 | "type": "submit", 240 | "style": "md-primary", 241 | "title": "Do It!" 242 | }, 243 | { 244 | "type": "button", 245 | "style": "md-warn", 246 | "title": "Noooooooooooo", 247 | "onClick": "sayNo()" 248 | } 249 | ] 250 | } 251 | ] 252 | } 253 | -------------------------------------------------------------------------------- /examples/data/switch.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "properties": { 5 | "live": { 6 | "title": "Mode", 7 | "type": "string", 8 | "titleMap": { 9 | "true": "'Yes I do'", 10 | "false": "'Hell no'" 11 | } 12 | } 13 | } 14 | }, 15 | "form": [ 16 | { 17 | "key":"live", 18 | "type":"switch" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /examples/data/tabarray.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "title": "Comment", 5 | "properties": { 6 | "comments": { 7 | "type": "array", 8 | "items": { 9 | "type": "object", 10 | "properties": { 11 | "name": { 12 | "title": "Name", 13 | "type": "string" 14 | }, 15 | "email": { 16 | "title": "Email", 17 | "type": "string", 18 | "pattern": "^\\S+@\\S+$", 19 | "description": "Email will be used for evil." 20 | }, 21 | "comment": { 22 | "title": "Comment", 23 | "type": "string", 24 | "maxLength": 20, 25 | "validationMessage": "Don't be greedy!" 26 | } 27 | }, 28 | "required": ["name","email","comment"] 29 | } 30 | } 31 | } 32 | }, 33 | "form": [ 34 | { 35 | "type": "help", 36 | "helpvalue": "

Tabbed Array Example

Tab arrays can have tabs to the left, top or right.

" 37 | }, 38 | { 39 | "key": "comments", 40 | "type": "tabarray", 41 | "add": "New", 42 | "remove": "Delete", 43 | "style": { 44 | "remove": "btn-danger" 45 | }, 46 | "title": "{{ value.name || 'Tab '+$index }}", 47 | "items": [ 48 | "comments[].name", 49 | "comments[].email", 50 | { 51 | "key": "comments[].comment", 52 | "type": "textarea" 53 | } 54 | ] 55 | }, 56 | { 57 | "type": "submit", 58 | "style": "btn-default", 59 | "title": "OK" 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /examples/data/tabs.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "properties": { 5 | "clan": { 6 | "title": "Ninja Clan", 7 | "description": "Which clan do you wish to complete your task?", 8 | "type": "string", 9 | "enum": [ "Iga", "koga" ] 10 | }, 11 | "armySize": { 12 | "type": "number", 13 | "title": "How many Ninja will be required?", 14 | "min": 1, 15 | "max": 7 16 | }, 17 | "weaponOfChoice": { 18 | "type": "string", 19 | "title": "Favourite tool", 20 | "enum": ["Nunchuku","Sai","Hanbo","Tonfa"] 21 | }, 22 | "colour": { 23 | "type": "string", 24 | "title": "Color preference", 25 | "enum": ["Black"] 26 | }, 27 | "date": { 28 | "title": "Date of assault", 29 | "type": "string", 30 | "format": "date" 31 | }, 32 | "victim": { 33 | "title": "Target", 34 | "type": "string" 35 | }, 36 | "assassinationStyle": { 37 | "title": "How would you like to dispatch your target?", 38 | "type": "string", 39 | "enum": [ "With honour", "With much blood", "With much suffering" ] 40 | }, 41 | "fee": { 42 | "title": "I accept late fee payment will result in a most dishonourable and painfully slow death", 43 | "type": "boolean" 44 | }, 45 | "suggestions": { 46 | "title": "Any suggestions based on local knowledge?", 47 | "type": "string" 48 | } 49 | } 50 | }, 51 | "form": [ 52 | { 53 | "type": "tabs", 54 | "tabs": [ 55 | { 56 | "title": "Ninja Details", 57 | "items": [ 58 | "clan", 59 | "armySize", 60 | "weaponOfChoice", 61 | "colour" 62 | ] 63 | }, 64 | { 65 | "title": "Job Properties", 66 | "items": [ 67 | "date", 68 | "victim", 69 | "assassinationStyle", 70 | "fee" 71 | ] 72 | }, 73 | { 74 | "title": "Further Information", 75 | "items": [ 76 | { 77 | "key":"suggestions", 78 | "type": "textarea" 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /examples/data/titlemaps.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "select": "a", 4 | "array": ["b"] 5 | }, 6 | "schema": { 7 | "type": "object", 8 | "properties": { 9 | "select": { 10 | "title": "Select without titleMap", 11 | "type": "string", 12 | "enum": ["a","b","c"] 13 | }, 14 | "select2": { 15 | "title": "Select with titleMap (old style)", 16 | "type": "string", 17 | "enum": ["a","b","c"] 18 | }, 19 | "noenum": { "type": "string", "title": "No enum, but forms says it's a select", "default": "a3" }, 20 | "selectOptionData": { 21 | "type": "string", 22 | "title": "Dynamic Options", 23 | "default": "The Terminator" 24 | }, 25 | "radios": { 26 | "title": "Basic radio button example to pick remote dataset for below select 'Remote Options'", 27 | "type": "string", 28 | "enum": ["books","movies"] 29 | }, 30 | "selectOptionRemote": { 31 | "type": "string", 32 | "title": "Remote Options", 33 | "links":[ 34 | { "rel": "options", "href": "./data/remote-titlemap-{radios}.json" } 35 | ] 36 | }, 37 | "array": { 38 | "title": "Array with enum defaults to 'checkboxes'", 39 | "type": "array", 40 | "items": { 41 | "type": "string", 42 | "enum": ["a","b","c"] 43 | } 44 | }, 45 | "array2": { 46 | "title": "Array with titleMap", 47 | "type": "array", 48 | "default": ["b","c"], 49 | "items": { 50 | "type": "string", 51 | "enum": ["a","b","c"] 52 | } 53 | }, 54 | "radiobuttons": { 55 | "title": "Radio buttons used to switch a boolean or multiple values", 56 | "type": "boolean", 57 | "default": false 58 | } 59 | } 60 | }, 61 | "form": [ 62 | { 63 | "type": "section", 64 | "grid": { "flex": "", "layout": "row" }, 65 | "items": [ 66 | { 67 | "key": "select", 68 | "type": "select", 69 | "grid": { "flex": "" } 70 | }, 71 | { 72 | "key": "select2", 73 | "type": "select", 74 | "grid": { "flex": "" }, 75 | "titleMap": { 76 | "a": "A", 77 | "b": "B", 78 | "c": "C" 79 | } 80 | } 81 | ] 82 | }, 83 | { 84 | "type": "section", 85 | "grid": { "flex": "", "layout": "row" }, 86 | "items": [ 87 | { 88 | "key": "noenum", 89 | "type": "select", 90 | "grid": { "flex": "" }, 91 | "titleMap": [ 92 | { "value":"a1", "name": "A1", "group": "A" }, 93 | { "value":"a2", "name":"A2", "group": "A" }, 94 | { "value":"a3", "name":"A3", "group": "A" }, 95 | { "value":"b1", "name": "B1", "group": "B" }, 96 | { "value":"b2", "name":"B2", "group": "B" }, 97 | { "value":"b3", "name":"B3", "group": "B" } 98 | ] 99 | }, 100 | { 101 | "key": "selectOptionData", 102 | "type": "select", 103 | "grid": { "flex": "" }, 104 | "optionData": "arnieFlix" 105 | } 106 | ] 107 | }, 108 | { 109 | "key": "radios", 110 | "type": "radios", 111 | "grid": { "layout": "row" }, 112 | "titleMap": [ 113 | { "value":"books", "name": "Books" }, 114 | { "value":"movies", "name":"Movies" } 115 | ] 116 | }, 117 | { 118 | "key": "selectOptionRemote", 119 | "type": "select", 120 | "grid": { "flex": 100, "layout": "column" } 121 | }, 122 | "array", 123 | { 124 | "key": "array2", 125 | "type": "checkboxes", 126 | "titleMap": [ 127 | { "value":"a", "name": "A" }, 128 | { "value":"b", "name":"B" }, 129 | { "value":"c", "name":"C" } 130 | ] 131 | }, 132 | { 133 | "key":"radiobuttons", 134 | "type": "radiobuttons", 135 | "titleMap": [ 136 | {"value": false, "name": "No way"}, 137 | {"value": null, "name": "Maybe"}, 138 | {"value": true, "name": "OK"} 139 | ] 140 | } 141 | ] 142 | } 143 | -------------------------------------------------------------------------------- /examples/data/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "type": "object", 4 | "title": "Types", 5 | "properties": { 6 | "string": { 7 | "type": "string", 8 | "minLength": 3 9 | }, 10 | "integer": { 11 | "type": "integer" 12 | }, 13 | "number": { 14 | "type": "number" 15 | }, 16 | "boolean": { 17 | "type": "boolean" 18 | } 19 | }, 20 | "required": ["number"] 21 | }, 22 | "form": [ 23 | "*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Material Design Schema Form Example 6 | 7 | 8 | 50 | 51 | 52 | 53 | 54 | 99 | 100 | 101 |
102 |

Schema Form Example

103 |

Material Design

104 |
105 |
106 |

The Generated Form

107 | 108 |
115 | 116 |
Form is valid
117 |
Form is not valid
118 | 119 |

Model

120 |
{{pretty()}}
121 |
122 |
123 |

Select Example

124 |
125 | 126 | 127 | {{::obj.name}} 128 | 129 | 130 |
131 |

Form

132 |
134 |

Schema

135 |
137 |
138 |
139 |
140 |

141 | 142 | JavaScript license information 143 | 144 |

145 |
146 |
147 | 148 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 166 | 167 | 168 | 169 | 428 | 429 | 430 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | streamqueue = require('streamqueue'), 3 | minifyHtml = require('gulp-minify-html'), 4 | templateCache = require('gulp-angular-templatecache'), 5 | concat = require('gulp-concat'), 6 | rename = require('gulp-rename'), 7 | uglify = require('gulp-uglify'), 8 | jscs = require('gulp-jscs'); 9 | 10 | gulp.task('build', function() { 11 | var stream = streamqueue({ objectMode: true }); 12 | stream.queue( 13 | gulp.src('./src/**/*.html') 14 | .pipe(minifyHtml({ 15 | empty: true, 16 | spare: true, 17 | quotes: true 18 | })) 19 | .pipe(templateCache({ 20 | module: 'schemaForm', 21 | root: 'decorators/material/' 22 | })) 23 | ); 24 | stream.queue(gulp.src('./src/**/*.js')); 25 | 26 | stream.done() 27 | .pipe(concat('material-decorator.js')) 28 | .pipe(gulp.dest('./')) 29 | .pipe(uglify()) 30 | .pipe(rename('material-decorator.min.js')) 31 | .pipe(gulp.dest('./')); 32 | 33 | }); 34 | gulp.task('templates', function() { 35 | var stream = streamqueue({ objectMode: true }); 36 | stream.queue( 37 | gulp.src('./src/**/*.html') 38 | .pipe(minifyHtml({ 39 | empty: true, 40 | spare: true, 41 | quotes: true 42 | })) 43 | .pipe(templateCache({ 44 | module: 'schemaForm', 45 | root: 'decorators/material/' 46 | })) 47 | ); 48 | 49 | stream.done() 50 | .pipe(concat('angular-schema-form-material-templates.js')) 51 | .pipe(gulp.dest('./src')) 52 | .pipe(uglify()) 53 | .pipe(rename('angular-schema-form-material-templates.min.js')) 54 | .pipe(gulp.dest('./src')); 55 | 56 | }); 57 | 58 | gulp.task('jscs', function() { 59 | gulp.src('./src/**/*.js') 60 | .pipe(jscs()); 61 | }); 62 | 63 | gulp.task('watch', function() { 64 | gulp.watch('./src/**/*', [ 'default' ]); 65 | }); 66 | 67 | gulp.task('default', [ 'templates' ]); 68 | -------------------------------------------------------------------------------- /material-decorator.js: -------------------------------------------------------------------------------- 1 | angular.module("schemaForm").run(["$templateCache", function($templateCache) {$templateCache.put("decorators/material/actions-trcl.html","
"); 2 | $templateCache.put("decorators/material/actions.html","
"); 3 | $templateCache.put("decorators/material/array.html","
= modelArray.length\" class=\"md-icon-button\" aria-label=\"More\" style=\"position: relative; z-index: 20;\">close
{{ form.add || \'Add\'}}
"); 4 | $templateCache.put("decorators/material/autocomplete.html","
{{item.name}}No matches found
"); 5 | $templateCache.put("decorators/material/card-content.html",""); 6 | $templateCache.put("decorators/material/card.html",""); 7 | $templateCache.put("decorators/material/checkbox.html","
{{::form.title}}
"); 8 | $templateCache.put("decorators/material/checkboxes.html","
{{::form.titleMap[$index].name}}
"); 9 | $templateCache.put("decorators/material/chips.html","
{{$chip}}
"); 10 | $templateCache.put("decorators/material/date.html","
"); 11 | $templateCache.put("decorators/material/default.html"," "); 12 | $templateCache.put("decorators/material/fieldset-trcl.html","
{{ form.title }}
"); 13 | $templateCache.put("decorators/material/fieldset.html","
{{ form.title }}
"); 14 | $templateCache.put("decorators/material/help.html","
"); 15 | $templateCache.put("decorators/material/radio-buttons.html","
"); 16 | $templateCache.put("decorators/material/radios-inline.html","
"); 17 | $templateCache.put("decorators/material/radios.html","
"); 18 | $templateCache.put("decorators/material/section.html",""); 19 | $templateCache.put("decorators/material/select.html","{{::filtered.name}}{{::opt.name}}"); 20 | $templateCache.put("decorators/material/slider.html",""); 21 | $templateCache.put("decorators/material/submit.html","
{{::form.tip}}{{::form.title}}
"); 22 | $templateCache.put("decorators/material/switch.html","{{::form.title}}"); 23 | $templateCache.put("decorators/material/tabarray.html",""); 24 | $templateCache.put("decorators/material/tabs.html","
"); 25 | $templateCache.put("decorators/material/textarea.html"," ");}]); 26 | (function(angular, undefined) {'use strict'; 27 | angular 28 | .module('schemaForm') 29 | .config(materialDecoratorConfig) 30 | .directive('sfmExternalOptions', sfmExternalOptionsDirective) 31 | .filter('sfCamelKey', sfCamelKeyFilter); 32 | 33 | materialDecoratorConfig.$inject = [ 34 | 'schemaFormProvider', 'schemaFormDecoratorsProvider', 'sfBuilderProvider', 'sfPathProvider', '$injector' 35 | ]; 36 | 37 | function materialDecoratorConfig( 38 | schemaFormProvider, decoratorsProvider, sfBuilderProvider, sfPathProvider, $injector) { 39 | var base = 'decorators/material/'; 40 | 41 | var simpleTransclusion = sfBuilderProvider.builders.simpleTransclusion; 42 | var ngModelOptions = sfBuilderProvider.builders.ngModelOptions; 43 | var ngModel = sfBuilderProvider.builders.ngModel; 44 | var sfField = sfBuilderProvider.builders.sfField; 45 | var condition = sfBuilderProvider.builders.condition; 46 | var array = sfBuilderProvider.builders.array; 47 | var numeric = sfBuilderProvider.builders.numeric; 48 | 49 | var sfLayout = sfLayout; 50 | var sfMessagesNode = sfMessagesNodeHandler(); 51 | var sfMessages = sfMessagesBuilder; 52 | var sfOptions = sfOptionsBuilder; 53 | var mdAutocomplete = mdAutocompleteBuilder; 54 | var mdSwitch = mdSwitchBuilder; 55 | var mdDatepicker = mdDatepickerBuilder; 56 | var mdTabs = mdTabsBuilder; 57 | var textarea = textareaBuilder; 58 | 59 | var core = [ sfField, ngModel, ngModelOptions, condition, sfLayout ]; 60 | var defaults = core.concat(sfMessages); 61 | var arrays = core.concat(array); 62 | 63 | schemaFormProvider.defaults.string.unshift(dateDefault); 64 | 65 | decoratorsProvider.defineDecorator('materialDecorator', { 66 | actions: { template: base + 'actions.html', builder: [ sfField, simpleTransclusion, condition ] }, 67 | array: { template: base + 'array.html', builder: arrays }, 68 | autocomplete: { template: base + 'autocomplete.html', builder: defaults.concat(mdAutocomplete) }, 69 | boolean: { template: base + 'checkbox.html', builder: defaults }, 70 | button: { template: base + 'submit.html', builder: defaults }, 71 | checkbox: { template: base + 'checkbox.html', builder: defaults }, 72 | checkboxes: { template: base + 'checkboxes.html', builder: arrays }, 73 | date: { template: base + 'date.html', builder: defaults.concat(mdDatepicker) }, 74 | 'default': { template: base + 'default.html', builder: defaults }, 75 | fieldset: { template: base + 'fieldset.html', builder: [ sfField, simpleTransclusion, condition ] }, 76 | help: { template: base + 'help.html', builder: defaults }, 77 | number: { template: base + 'default.html', builder: defaults.concat(numeric) }, 78 | password: { template: base + 'default.html', builder: defaults }, 79 | radios: { template: base + 'radios.html', builder: defaults }, 80 | 'radios-inline': { template: base + 'radios-inline.html', builder: defaults }, 81 | radiobuttons: { template: base + 'radio-buttons.html', builder: defaults }, 82 | section: { template: base + 'section.html', builder: [ sfField, simpleTransclusion, condition, sfLayout ] }, 83 | select: { template: base + 'select.html', builder: defaults.concat(sfOptions) }, 84 | submit: { template: base + 'submit.html', builder: defaults }, 85 | tabs: { template: base + 'tabs.html', builder: [ sfField, mdTabs, condition ] }, 86 | tabarray: { template: base + 'tabarray.html', builder: arrays }, 87 | textarea: { template: base + 'textarea.html', builder: defaults.concat(textarea) }, 88 | switch: { template: base + 'switch.html', builder: defaults.concat(mdSwitch) } 89 | }); 90 | 91 | function sfLayout(args) { 92 | var layoutDiv = args.fieldFrag.querySelector('[sf-layout]'); 93 | 94 | if (args.form.grid) { 95 | Object.getOwnPropertyNames(args.form.grid).forEach(function(property, idx, array) { 96 | layoutDiv.setAttribute(property, args.form.grid[property]); 97 | }); 98 | }; 99 | }; 100 | 101 | function sfMessagesNodeHandler() { 102 | var html = '
'; 103 | var div = document.createElement('div'); 104 | div.innerHTML = html; 105 | return div.firstChild; 106 | }; 107 | 108 | function sfMessagesBuilder(args) { 109 | var messagesDiv = args.fieldFrag.querySelector('[sf-messages]'); 110 | if (messagesDiv && sfMessagesNode) { 111 | var child = sfMessagesNode.cloneNode(); 112 | messagesDiv.appendChild(child); 113 | } 114 | }; 115 | 116 | function textareaBuilder(args) { 117 | var textareaFrag = args.fieldFrag.querySelector('textarea'); 118 | var maxLength = args.form.maxlength || false; 119 | if (textareaFrag && maxLength) { 120 | textareaFrag.setAttribute('md-maxlength', maxLength); 121 | }; 122 | }; 123 | 124 | function mdAutocompleteBuilder(args) { 125 | var mdAutocompleteFrag = args.fieldFrag.querySelector('md-autocomplete'); 126 | var minLength = args.form.minLength || 1; 127 | var maxLength = args.form.maxLength || false; 128 | var title = args.form.title || args.form.placeholder || args.form.key.slice(-1)[0]; 129 | 130 | if (mdAutocompleteFrag) { 131 | if (args.form.onChange) { 132 | mdAutocompleteFrag.setAttribute('md-selected-item-change', 'args.form.onChange()'); 133 | mdAutocompleteFrag.setAttribute('md-search-text-change', 'args.form.onChange(searchText)'); 134 | }; 135 | 136 | // mdAutocompleteFrag.setAttribute('md-items', 'item in $filter(''autocomplete'')(searchText);'); 137 | mdAutocompleteFrag.setAttribute('md-min-length', minLength); 138 | if (maxLength) { 139 | mdAutocompleteFrag.setAttribute('md-max-length', maxLength); 140 | }; 141 | 142 | if (title) { 143 | mdAutocompleteFrag.setAttribute('md-floating-label', title); 144 | }; 145 | }; 146 | }; 147 | 148 | function mdSwitchBuilder(args) { 149 | var mdSwitchFrag = args.fieldFrag.querySelector('md-switch'); 150 | if (args.form.schema.titleMap) { 151 | mdSwitchFrag.setAttribute('ng-true-value', args.form.schema.titleMap.true); 152 | mdSwitchFrag.setAttribute('ng-false-value', args.form.schema.titleMap.false); 153 | }; 154 | }; 155 | 156 | function sfOptionsBuilder(args) { 157 | var mdSelectFrag = args.fieldFrag.querySelector('md-select'); 158 | var enumTitleMap = []; 159 | var i; 160 | var mdSelectFrag; 161 | 162 | args.form.selectOptions = []; 163 | args.form.getOptions = getOptionsHandler; 164 | 165 | if (args.form.schema.links && (typeof args.form.schema.links) === 'object') { 166 | var link; 167 | var related = /({)([^}]*)(})/gm; 168 | var source = /{{([^}]*)}}/gm; 169 | var matched; 170 | 171 | for (i = 0; i < args.form.schema.links.length; i++) { 172 | link = args.form.schema.links[i]; 173 | if (link.rel === 'options') { 174 | // TODO enable filter to allow processing results 175 | // args.form.optionSource = link.href.replace(related, '$1$1 model.$2 | _externalOptionUri $3$3'); 176 | args.form.optionSource = link.href.replace(related, '$1$1 model.$2 $3$3'); 177 | }; 178 | }; 179 | 180 | mdSelectFrag.setAttribute('sfm-external-options', args.form.optionSource); 181 | } 182 | else { 183 | args.form.selectOptions = sfOptionsProcessor(args.form); 184 | }; 185 | }; 186 | 187 | function mdDatepickerBuilder(args) { 188 | var mdDatepickerFrag = args.fieldFrag.querySelector('md-datepicker'); 189 | if (mdDatepickerFrag) { 190 | if (args.form.onChange) { 191 | mdDatepickerFrag.setAttribute('ng-change', 'args.form.onChange(searchText)'); 192 | } 193 | // mdDatepickerFrag.setAttribute('md-items', 'item in $filter(''autocomplete'')(searchText);'); 194 | var minDate = args.form.minimum || false; 195 | var maxDate = args.form.maximum || false; 196 | if (minDate) { 197 | mdDatepickerFrag.setAttribute('md-max-date', minDate); 198 | } 199 | if (maxDate) { 200 | mdDatepickerFrag.setAttribute('md-max-date', maxDate); 201 | } 202 | } 203 | }; 204 | 205 | function mdTabsBuilder(args) { 206 | if (args.form.tabs && args.form.tabs.length > 0) { 207 | var mdTabsFrag = args.fieldFrag.querySelector('md-tabs'); 208 | 209 | args.form.tabs.forEach(function(tab, index) { 210 | var mdTab = document.createElement('md-tab'); 211 | mdTab.setAttribute('label', '{{' + args.path + '.tabs[' + index + '].title}}'); 212 | var mdTabBody = document.createElement('md-tab-body'); 213 | var childFrag = args.build(tab.items, args.path + '.tabs[' + index + '].items', args.state); 214 | mdTabBody.appendChild(childFrag); 215 | mdTab.appendChild(mdTabBody); 216 | mdTabsFrag.appendChild(mdTab); 217 | }); 218 | } 219 | }; 220 | 221 | /** 222 | * Material Datepicker 223 | */ 224 | function dateDefault(name, schema, options) { 225 | if (schema.type === 'string' && (schema.format === 'date' || schema.format === 'date-time')) { 226 | var f = schemaFormProvider.stdFormObj(name, schema, options); 227 | f.key = options.path; 228 | f.type = 'date'; 229 | options.lookup[sfPathProvider.stringify(options.path)] = f; 230 | return f; 231 | } 232 | }; 233 | }; 234 | 235 | function getOptionsHandler(form, evalExpr) { 236 | if (form.optionData) { 237 | return evalExpr(form.optionData); 238 | }; 239 | 240 | if (form.selectOptions) { 241 | return form.selectOptions; 242 | }; 243 | 244 | return []; 245 | }; 246 | 247 | function sfOptionsProcessor(data) { 248 | var enumTitleMap = []; 249 | 250 | if (data.titleMap) { 251 | return data.titleMap; 252 | } 253 | else if (data.enum && data.enum.length) { 254 | for (i = 0; i < data.enum.length; i++) { 255 | if (data.enum[i] && data.enum[i].length) { 256 | enumTitleMap.push({ name: data.enum[i], value: data.enum[i] }); 257 | }; 258 | }; 259 | }; 260 | 261 | return enumTitleMap; 262 | }; 263 | 264 | sfmExternalOptionsDirective.$inject = [ '$http' ]; 265 | 266 | function sfmExternalOptionsDirective($http) { 267 | var directive = { 268 | link: link, 269 | restrict: 'A' 270 | }; 271 | 272 | return directive; 273 | 274 | function link(scope, element, attrs) { 275 | attrs.$observe('sfmExternalOptions', function(dataURI) { 276 | $http.get(dataURI) 277 | .then(function(response) { 278 | scope.form.selectOptions = sfOptionsProcessor(response.data); 279 | }); 280 | }); 281 | }; 282 | }; 283 | 284 | /** 285 | * sfCamelKey Filter 286 | */ 287 | function sfCamelKeyFilter() { 288 | return function(formKey) { 289 | if (!formKey) { return ''; }; 290 | var part, i, key; 291 | key = formKey.slice(); 292 | for (i = 0; i < key.length; i++) { 293 | part = key[i].toLowerCase().split(''); 294 | if (i && part.length) { part[0] = part[0].toUpperCase(); }; 295 | key[i] = part.join(''); 296 | }; 297 | return key.join(''); 298 | }; 299 | }; 300 | 301 | })(angular, undefined); 302 | /* 303 | TODO add default filter for autocomplete which allows form.optionFilter or 'autocompleteFilter' to override 304 | Something along the following lines... 305 | if ($injector.has('autocompleteFilter')) { 306 | result = $filter('autocomplete')(input); 307 | } 308 | else 309 | if ($injector.has(args.form.optionFilter + 'Filter')) { 310 | result = $filter(args.form.optionFilter)(input); 311 | } 312 | else { 313 | if (args.form.optionFilter) { 314 | mdAutocomplete.setAttribute('md-items', 315 | 'item in evalExpr("this[\""+form.optionFilter+"\"](\""+searchText+"\")")'); 316 | } 317 | } 318 | 319 | .filter('autocompleteMovieTest', function() { 320 | function autocompleteMovieTestFilter(array, input){ 321 | var current = input; 322 | // You could also call multiple filters here using: 323 | // current = $filter('filterName')(input) 324 | if(typeof current === 'string') { 325 | current = current.replace(' ','-').toLowerCase(); 326 | } 327 | current = (!current) ? '_undefined' : current; 328 | return current; 329 | } 330 | 331 | return externalOptionUriFilter; 332 | }) 333 | */ 334 | 335 | require('../material-decorator-templates.js'); 336 | require('./type-parser.js'); 337 | require('./material-decorator.js'); 338 | 339 | /** 340 | * It might be a bug, but currently input[type=number] does not add 341 | * a parser, so the model gets populated with a string. It does however stop non numbers so it 342 | * must have some preproccessing. Anyway, this adds parser before schema-validate hooks into it. 343 | * FIXME: this is still not a complete solution. Inputting a string in an input[type=number] results 344 | * in parsers never firing and ngModel value removed. So no validation from schema-validate either. 345 | */ 346 | angular.module('schemaForm').directive('sfTypeParser', function() { 347 | return { 348 | restrict: 'A', 349 | scope: false, 350 | require: 'ngModel', 351 | link: function(scope, element, attrs, ngModel) { 352 | var once = scope.$watch(attrs.sfTypeParser, function(schema) { 353 | if (!schema) { 354 | return; 355 | } 356 | 357 | var isNumber = schema.type.indexOf('number') !== -1; 358 | var isInteger = schema.type.indexOf('integer') !== -1; 359 | var numberRE = /^[0-9]*$/; 360 | // Use index of since type can be either an array with two values or a string. 361 | if (isNumber || isInteger) { 362 | // The timing here seems to work. i.e. we get in before schema-validate 363 | ngModel.$parsers.push(function(viewValue) { 364 | var value; 365 | if (isNumber) { 366 | value = parseFloat(viewValue); 367 | } else if (numberRE.test(viewValue)) { 368 | // We test the value to check that it's a valid integer, otherwise we can easily 369 | // get float -> integer parsing behind the scenes. 370 | value = parseInt(viewValue, 10); 371 | } 372 | console.log('parser', numberRE.test(viewValue), viewValue, value) 373 | if (value === undefined || isNaN(value)) { 374 | //Let the validation fail. @FIXME: it fails with "required" for some reason. 375 | return viewValue; 376 | } 377 | return value; 378 | }); 379 | } 380 | 381 | once(); 382 | }); 383 | } 384 | }; 385 | }); 386 | -------------------------------------------------------------------------------- /material-decorator.min.js: -------------------------------------------------------------------------------- 1 | angular.module("schemaForm").run(["$templateCache",function(e){e.put("decorators/material/actions-trcl.html",'
'),e.put("decorators/material/actions.html",'
'),e.put("decorators/material/array.html",'
close
{{ form.add || \'Add\'}}
'),e.put("decorators/material/autocomplete.html",'
{{item.name}}No matches found
'),e.put("decorators/material/card-content.html",''),e.put("decorators/material/card.html",''),e.put("decorators/material/checkbox.html",'
{{::form.title}}
'),e.put("decorators/material/checkboxes.html",'
{{::form.titleMap[$index].name}}
'),e.put("decorators/material/chips.html",'
{{$chip}}
'),e.put("decorators/material/date.html",'
'),e.put("decorators/material/default.html",' '),e.put("decorators/material/fieldset-trcl.html",'
{{ form.title }}
'),e.put("decorators/material/fieldset.html",'
{{ form.title }}
'),e.put("decorators/material/help.html",'
'),e.put("decorators/material/radio-buttons.html",'
'),e.put("decorators/material/radios-inline.html",'
'),e.put("decorators/material/radios.html",'
'),e.put("decorators/material/section.html",''),e.put("decorators/material/select.html",'{{::filtered.name}}{{::opt.name}}'),e.put("decorators/material/slider.html",''),e.put("decorators/material/submit.html",'
{{::form.tip}}{{::form.title}}
'),e.put("decorators/material/switch.html",'{{::form.title}}'),e.put("decorators/material/tabarray.html",''),e.put("decorators/material/tabs.html",'
'),e.put("decorators/material/textarea.html",' ')}]),function(e,t){"use strict";function a(e,t,a,s,o){function m(e){var t=e.fieldFrag.querySelector("[sf-layout]");e.form.grid&&Object.getOwnPropertyNames(e.form.grid).forEach(function(a,r,l){t.setAttribute(a,e.form.grid[a])})}function i(){var e='
',t=document.createElement("div");return t.innerHTML=e,t.firstChild}function n(e){var t=e.fieldFrag.querySelector("[sf-messages]");if(t&&A){var a=A.cloneNode();t.appendChild(a)}}function d(e){var t=e.fieldFrag.querySelector("textarea"),a=e.form.maxlength||!1;t&&a&&t.setAttribute("md-maxlength",a)}function c(e){var t=e.fieldFrag.querySelector("md-autocomplete"),a=e.form.minLength||1,r=e.form.maxLength||!1,l=e.form.title||e.form.placeholder||e.form.key.slice(-1)[0];t&&(e.form.onChange&&(t.setAttribute("md-selected-item-change","args.form.onChange()"),t.setAttribute("md-search-text-change","args.form.onChange(searchText)")),t.setAttribute("md-min-length",a),r&&t.setAttribute("md-max-length",r),l&&t.setAttribute("md-floating-label",l))}function f(e){var t=e.fieldFrag.querySelector("md-switch");e.form.schema.titleMap&&(t.setAttribute("ng-true-value",e.form.schema.titleMap["true"]),t.setAttribute("ng-false-value",e.form.schema.titleMap["false"]))}function h(e){var t,a,a=e.fieldFrag.querySelector("md-select");if(e.form.selectOptions=[],e.form.getOptions=r,e.form.schema.links&&"object"==typeof e.form.schema.links){var s,o=/({)([^}]*)(})/gm;for(t=0;t0){var t=e.fieldFrag.querySelector("md-tabs");e.form.tabs.forEach(function(a,r){var l=document.createElement("md-tab");l.setAttribute("label","{{"+e.path+".tabs["+r+"].title}}");var s=document.createElement("md-tab-body"),o=e.build(a.items,e.path+".tabs["+r+"].items",e.state);s.appendChild(o),l.appendChild(s),t.appendChild(l)})}}function g(t,a,r){if("string"===a.type&&("date"===a.format||"date-time"===a.format)){var l=e.stdFormObj(t,a,r);return l.key=r.path,l.type="date",r.lookup[s.stringify(r.path)]=l,l}}var b="decorators/material/",y=a.builders.simpleTransclusion,v=a.builders.ngModelOptions,x=a.builders.ngModel,$=a.builders.sfField,k=a.builders.condition,C=a.builders.array,w=a.builders.numeric,m=m,A=i(),T=n,E=h,M=c,S=f,F=u,O=p,j=d,K=[$,x,v,k,m],q=K.concat(T),H=K.concat(C);e.defaults.string.unshift(g),t.defineDecorator("materialDecorator",{actions:{template:b+"actions.html",builder:[$,y,k]},array:{template:b+"array.html",builder:H},autocomplete:{template:b+"autocomplete.html",builder:q.concat(M)},"boolean":{template:b+"checkbox.html",builder:q},button:{template:b+"submit.html",builder:q},checkbox:{template:b+"checkbox.html",builder:q},checkboxes:{template:b+"checkboxes.html",builder:H},date:{template:b+"date.html",builder:q.concat(F)},"default":{template:b+"default.html",builder:q},fieldset:{template:b+"fieldset.html",builder:[$,y,k]},help:{template:b+"help.html",builder:q},number:{template:b+"default.html",builder:q.concat(w)},password:{template:b+"default.html",builder:q},radios:{template:b+"radios.html",builder:q},"radios-inline":{template:b+"radios-inline.html",builder:q},radiobuttons:{template:b+"radio-buttons.html",builder:q},section:{template:b+"section.html",builder:[$,y,k,m]},select:{template:b+"select.html",builder:q.concat(E)},submit:{template:b+"submit.html",builder:q},tabs:{template:b+"tabs.html",builder:[$,O,k]},tabarray:{template:b+"tabarray.html",builder:H},textarea:{template:b+"textarea.html",builder:q.concat(j)},"switch":{template:b+"switch.html",builder:q.concat(S)}})}function r(e,t){return e.optionData?t(e.optionData):e.selectOptions?e.selectOptions:[]}function l(e){var t=[];if(e.titleMap)return e.titleMap;if(e["enum"]&&e["enum"].length)for(i=0;i (https://github.com/Anthropic)" 19 | ], 20 | "license": "MIT", 21 | "dependencies": { 22 | "angular": ">= 1.2", 23 | "angular-messages": "^1.5.0", 24 | "angular-sanitize": ">= 1.2", 25 | "tv4": "~1.0.15" 26 | }, 27 | "devDependencies": { 28 | "babel": "^6.5.2", 29 | "babel-core": "^6.10.4", 30 | "babel-loader": "^6.2.4", 31 | "babel-polyfill": "^6.9.1", 32 | "babel-preset-es2015": "^6.9.0", 33 | "chai": "^3.5.0", 34 | "coveralls": "^2.11.0", 35 | "html": "^1.0.0", 36 | "html-loader": "^0.4.4", 37 | "html-webpack-plugin": "^2.25.0", 38 | "karma": "^0.13.22", 39 | "karma-chai-sinon": "^0.1.5", 40 | "karma-coverage": "^1.0.0", 41 | "karma-growler-reporter": "0.0.1", 42 | "karma-mocha": "^1.0.1", 43 | "karma-phantomjs-launcher": "^0.1.4", 44 | "karma-webpack": "^1.7.0", 45 | "mocha": "^2.5.3", 46 | "mocha-lcov-reporter": "0.0.1", 47 | "ngtemplate-loader": "^1.3.1", 48 | "protractor": "^2.5.1", 49 | "sinon": "^1.17.4", 50 | "sinon-chai": "^2.8.0", 51 | "streamqueue": "^0.1.3", 52 | "uglify-js": "^2.6.2", 53 | "webpack": "^2.1.0-beta.28", 54 | "webpack-dev-server": "^2.1.0-beta.12" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/material-class.js: -------------------------------------------------------------------------------- 1 | angular.module('schemaForm').directive('sfMaterialClass', sfMaterialClassDirective); 2 | 3 | sfMaterialClassDirective.$inject = [ 4 | '$compile', '$timeout' 5 | ]; 6 | 7 | function sfMaterialClassDirective($compile, $timeout) { 8 | return { 9 | restrict : 'A', 10 | scope : false, 11 | link : function(scope, element, attrs, ngModel) { 12 | function reduceHelper(obj, i) {return obj[i]} 13 | 14 | var modelValue; 15 | try { 16 | modelValue = scope.form.key.reduce(reduceHelper, scope.model); 17 | } catch (e) { 18 | modelValue = undefined; 19 | } 20 | 21 | // Element class is not set in DOM if executed immediately. 22 | // I don't understand exactly why but it's probably related to other directive job. 23 | $timeout(function() { 24 | if (modelValue !== null && typeof modelValue !== 'undefined' && modelValue !== false) { 25 | element.addClass(attrs.sfMaterialClass); 26 | } 27 | }, 0); 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/material-decorator.js: -------------------------------------------------------------------------------- 1 | // ngtemplate-loader embeds the html on build 2 | import actionsTemplate from './material/actions.html'; 3 | import arrayTemplate from './material/array.html'; 4 | import autocompleteTemplate from './material/autocomplete.html'; 5 | import booleanTemplate from './material/checkbox.html'; 6 | import buttonTemplate from './material/submit.html'; 7 | import checkboxTemplate from './material/checkbox.html'; 8 | import checkboxesTemplate from './material/checkboxes.html'; 9 | import dateTemplate from './material/date.html'; 10 | import defaultTemplate from './material/default.html'; 11 | import fieldsetTemplate from './material/fieldset.html'; 12 | import helpTemplate from './material/help.html'; 13 | import numberTemplate from './material/default.html'; 14 | import passwordTemplate from './material/default.html'; 15 | import radiosTemplate from './material/radios.html'; 16 | import radiosInlineTemplate from './material/radios-inline.html'; 17 | import radiobuttonsTemplate from './material/radio-buttons.html'; 18 | import sectionTemplate from './material/section.html'; 19 | import selectTemplate from './material/select.html'; 20 | import submitTemplate from './material/submit.html'; 21 | import switchTemplate from './material/switch.html'; 22 | import tabsTemplate from './material/tabs.html'; 23 | import tabarrayTemplate from './material/tabarray.html'; 24 | import textareaTemplate from './material/textarea.html'; 25 | 26 | angular 27 | .module('schemaForm') 28 | .config(materialDecoratorConfig) 29 | .directive('sfmExternalOptions', sfmExternalOptionsDirective) 30 | .filter('sfCamelKey', sfCamelKeyFilter); 31 | 32 | materialDecoratorConfig.$inject = [ 33 | 'schemaFormProvider', 'schemaFormDecoratorsProvider', 'sfBuilderProvider', 'sfPathProvider', '$injector' 34 | ]; 35 | 36 | function materialDecoratorConfig( 37 | schemaFormProvider, decoratorsProvider, sfBuilderProvider, sfPathProvider, $injector) { 38 | var base = 'decorators/material/'; 39 | 40 | var simpleTransclusion = sfBuilderProvider.builders.simpleTransclusion; 41 | var ngModelOptions = sfBuilderProvider.builders.ngModelOptions; 42 | var ngModel = sfBuilderProvider.builders.ngModel; 43 | var sfField = sfBuilderProvider.builders.sfField; 44 | var condition = sfBuilderProvider.builders.condition; 45 | var array = sfBuilderProvider.builders.array; 46 | var numeric = sfBuilderProvider.builders.numeric; 47 | 48 | var sfLayout = sfLayout; 49 | var sfMessagesNode = sfMessagesNodeHandler(); 50 | var sfMessages = sfMessagesBuilder; 51 | var sfOptions = sfOptionsBuilder; 52 | var mdAutocomplete = mdAutocompleteBuilder; 53 | var mdSwitch = mdSwitchBuilder; 54 | var mdDatepicker = mdDatepickerBuilder; 55 | var mdTabs = mdTabsBuilder; 56 | var textarea = textareaBuilder; 57 | 58 | var core = [ sfField, ngModel, ngModelOptions, condition, sfLayout ]; 59 | var defaults = core.concat(sfMessages); 60 | var arrays = core.concat(array); 61 | 62 | schemaFormProvider.defaults.string.unshift(dateDefault); 63 | 64 | decoratorsProvider.defineDecorator('materialDecorator', { 65 | actions: { template: actionsTemplate, builder: [ sfField, simpleTransclusion, condition ] }, 66 | array: { template: arrayTemplate, builder: arrays }, 67 | autocomplete: { template: autocompleteTemplate, builder: defaults.concat(mdAutocomplete) }, 68 | boolean: { template: checkboxTemplate, builder: defaults }, 69 | button: { template: submitTemplate, builder: defaults }, 70 | checkbox: { template: checkboxTemplate, builder: defaults }, 71 | checkboxes: { template: checkboxesTemplate, builder: arrays }, 72 | date: { template: dateTemplate, builder: defaults.concat(mdDatepicker) }, 73 | 'default': { template: defaultTemplate, builder: defaults }, 74 | fieldset: { template: fieldsetTemplate, builder: [ sfField, simpleTransclusion, condition ] }, 75 | help: { template: helpTemplate, builder: defaults }, 76 | number: { template: defaultTemplate, builder: defaults.concat(numeric) }, 77 | password: { template: defaultTemplate, builder: defaults }, 78 | radios: { template: radiosTemplate, builder: defaults }, 79 | 'radios-inline': { template: radiosInlineTemplate, builder: defaults }, 80 | radiobuttons: { template: radiobuttonsTemplate, builder: defaults }, 81 | section: { template: sectionTemplate, builder: [ sfField, simpleTransclusion, condition, sfLayout ] }, 82 | select: { template: selectTemplate, builder: defaults.concat(sfOptions) }, 83 | submit: { template: submitTemplate, builder: defaults }, 84 | tabs: { template: tabsTemplate, builder: [ sfField, mdTabs, condition ] }, 85 | tabarray: { template: tabarrayTemplate, builder: arrays }, 86 | textarea: { template: textareaTemplate, builder: defaults.concat(textarea) }, 87 | 'switch': { template: switchTemplate, builder: defaults.concat(mdSwitch) } 88 | }); 89 | 90 | function sfLayout(args) { 91 | var layoutDiv = args.fieldFrag.querySelector('[sf-layout]'); 92 | 93 | if (args.form.grid) { 94 | Object.getOwnPropertyNames(args.form.grid).forEach(function(property, idx, array) { 95 | layoutDiv.setAttribute(property, args.form.grid[property]); 96 | }); 97 | }; 98 | }; 99 | 100 | function sfMessagesNodeHandler() { 101 | var html = '
'; 102 | var div = document.createElement('div'); 103 | div.innerHTML = html; 104 | return div.firstChild; 105 | }; 106 | 107 | function sfMessagesBuilder(args) { 108 | var messagesDiv = args.fieldFrag.querySelector('[sf-messages]'); 109 | if (messagesDiv && sfMessagesNode) { 110 | var child = sfMessagesNode.cloneNode(); 111 | messagesDiv.appendChild(child); 112 | } 113 | }; 114 | 115 | function textareaBuilder(args) { 116 | var textareaFrag = args.fieldFrag.querySelector('textarea'); 117 | var maxLength = args.form.maxlength || false; 118 | if (textareaFrag && maxLength) { 119 | textareaFrag.setAttribute('md-maxlength', maxLength); 120 | }; 121 | }; 122 | 123 | function mdAutocompleteBuilder(args) { 124 | var mdAutocompleteFrag = args.fieldFrag.querySelector('md-autocomplete'); 125 | var minLength = args.form.minLength || 1; 126 | var maxLength = args.form.maxLength || false; 127 | var title = args.form.title || args.form.placeholder || args.form.key.slice(-1)[0]; 128 | 129 | if (mdAutocompleteFrag) { 130 | if (args.form.onChange) { 131 | mdAutocompleteFrag.setAttribute('md-selected-item-change', 'args.form.onChange()'); 132 | mdAutocompleteFrag.setAttribute('md-search-text-change', 'args.form.onChange(searchText)'); 133 | }; 134 | 135 | // mdAutocompleteFrag.setAttribute('md-items', 'item in $filter(''autocomplete'')(searchText);'); 136 | mdAutocompleteFrag.setAttribute('md-min-length', minLength); 137 | if (maxLength) { 138 | mdAutocompleteFrag.setAttribute('md-max-length', maxLength); 139 | }; 140 | 141 | if (title) { 142 | mdAutocompleteFrag.setAttribute('md-floating-label', title); 143 | }; 144 | }; 145 | }; 146 | 147 | function mdSwitchBuilder(args) { 148 | var mdSwitchFrag = args.fieldFrag.querySelector('md-switch'); 149 | if (args.form.schema.titleMap) { 150 | mdSwitchFrag.setAttribute('ng-true-value', args.form.schema.titleMap.true); 151 | mdSwitchFrag.setAttribute('ng-false-value', args.form.schema.titleMap.false); 152 | }; 153 | }; 154 | 155 | function sfOptionsBuilder(args) { 156 | var mdSelectFrag = args.fieldFrag.querySelector('md-select'); 157 | var enumTitleMap = []; 158 | var i; 159 | var mdSelectFrag; 160 | 161 | args.form.selectOptions = []; 162 | args.form.getOptions = getOptionsHandler; 163 | 164 | if (args.form.schema.links && (typeof args.form.schema.links) === 'object') { 165 | var link; 166 | var related = /({)([^}]*)(})/gm; 167 | var source = /{{([^}]*)}}/gm; 168 | var matched; 169 | 170 | for (i = 0; i < args.form.schema.links.length; i++) { 171 | link = args.form.schema.links[i]; 172 | if (link.rel === 'options') { 173 | // TODO enable filter to allow processing results 174 | // args.form.optionSource = link.href.replace(related, '$1$1 model.$2 | _externalOptionUri $3$3'); 175 | args.form.optionSource = link.href.replace(related, '$1$1 model.$2 $3$3'); 176 | }; 177 | }; 178 | 179 | mdSelectFrag.setAttribute('sfm-external-options', args.form.optionSource); 180 | } 181 | else { 182 | args.form.selectOptions = sfOptionsProcessor(args.form); 183 | }; 184 | }; 185 | 186 | function mdDatepickerBuilder(args) { 187 | var mdDatepickerFrag = args.fieldFrag.querySelector('md-datepicker'); 188 | if (mdDatepickerFrag) { 189 | if (args.form.onChange) { 190 | mdDatepickerFrag.setAttribute('ng-change', 'args.form.onChange(searchText)'); 191 | } 192 | // mdDatepickerFrag.setAttribute('md-items', 'item in $filter(''autocomplete'')(searchText);'); 193 | var minDate = args.form.minimum || false; 194 | var maxDate = args.form.maximum || false; 195 | if (minDate) { 196 | mdDatepickerFrag.setAttribute('md-max-date', minDate); 197 | } 198 | if (maxDate) { 199 | mdDatepickerFrag.setAttribute('md-max-date', maxDate); 200 | } 201 | } 202 | }; 203 | 204 | function mdTabsBuilder(args) { 205 | if (args.form.tabs && args.form.tabs.length > 0) { 206 | var mdTabsFrag = args.fieldFrag.querySelector('md-tabs'); 207 | 208 | args.form.tabs.forEach(function(tab, index) { 209 | var evalExpr = '(evalExpr(' + args.path + '.tabs[' + index + ']' + 210 | '.condition, { model: model, "arrayIndex": $index}))'; 211 | var mdTab = document.createElement('md-tab'); 212 | if(!!tab.condition) { 213 | mdTab.setAttribute('ng-if', evalExpr); 214 | }; 215 | mdTab.setAttribute('label', '{{' + args.path + '.tabs[' + index + '].title}}'); 216 | var mdTabBody = document.createElement('md-tab-body'); 217 | var childFrag = args.build(tab.items, args.path + '.tabs[' + index + '].items', args.state); 218 | mdTabBody.appendChild(childFrag); 219 | mdTab.appendChild(mdTabBody); 220 | mdTabsFrag.appendChild(mdTab); 221 | }); 222 | } 223 | }; 224 | 225 | /** 226 | * Material Datepicker 227 | */ 228 | function dateDefault(name, schema, options) { 229 | if (schema.type === 'string' && (schema.format === 'date' || schema.format === 'date-time')) { 230 | var f = schemaFormProvider.stdFormObj(name, schema, options); 231 | f.key = options.path; 232 | f.type = 'date'; 233 | options.lookup[sfPathProvider.stringify(options.path)] = f; 234 | return f; 235 | } 236 | }; 237 | }; 238 | 239 | function getOptionsHandler(form, evalExpr) { 240 | if (form.optionData) { 241 | return evalExpr(form.optionData); 242 | }; 243 | 244 | if (form.selectOptions) { 245 | return form.selectOptions; 246 | }; 247 | 248 | return []; 249 | }; 250 | 251 | function sfOptionsProcessor(data) { 252 | var enumTitleMap = []; 253 | 254 | if (data.titleMap) { 255 | return data.titleMap; 256 | } 257 | else if (data.enum && data.enum.length) { 258 | for (i = 0; i < data.enum.length; i++) { 259 | if (data.enum[i] && data.enum[i].length) { 260 | enumTitleMap.push({ name: data.enum[i], value: data.enum[i] }); 261 | }; 262 | }; 263 | }; 264 | 265 | return enumTitleMap; 266 | }; 267 | 268 | sfmExternalOptionsDirective.$inject = [ '$http' ]; 269 | 270 | function sfmExternalOptionsDirective($http) { 271 | var directive = { 272 | link: link, 273 | restrict: 'A' 274 | }; 275 | 276 | return directive; 277 | 278 | function link(scope, element, attrs) { 279 | attrs.$observe('sfmExternalOptions', function(dataURI) { 280 | $http.get(dataURI) 281 | .then(function(response) { 282 | scope.form.selectOptions = sfOptionsProcessor(response.data); 283 | }); 284 | }); 285 | }; 286 | }; 287 | 288 | /** 289 | * sfCamelKey Filter 290 | */ 291 | function sfCamelKeyFilter() { 292 | return function(formKey) { 293 | if (!formKey) { return ''; }; 294 | var part, i, key; 295 | key = formKey.slice(); 296 | for (i = 0; i < key.length; i++) { 297 | part = key[i].toLowerCase().split(''); 298 | if (i && part.length) { part[0] = part[0].toUpperCase(); }; 299 | key[i] = part.join(''); 300 | }; 301 | return key.join(''); 302 | }; 303 | }; 304 | 305 | /* 306 | TODO add default filter for autocomplete which allows form.optionFilter or 'autocompleteFilter' to override 307 | Something along the following lines... 308 | if ($injector.has('autocompleteFilter')) { 309 | result = $filter('autocomplete')(input); 310 | } 311 | else 312 | if ($injector.has(args.form.optionFilter + 'Filter')) { 313 | result = $filter(args.form.optionFilter)(input); 314 | } 315 | else { 316 | if (args.form.optionFilter) { 317 | mdAutocomplete.setAttribute('md-items', 318 | 'item in evalExpr("this[\""+form.optionFilter+"\"](\""+searchText+"\")")'); 319 | } 320 | } 321 | 322 | .filter('autocompleteMovieTest', function() { 323 | function autocompleteMovieTestFilter(array, input){ 324 | var current = input; 325 | // You could also call multiple filters here using: 326 | // current = $filter('filterName')(input) 327 | if(typeof current === 'string') { 328 | current = current.replace(' ','-').toLowerCase(); 329 | } 330 | current = (!current) ? '_undefined' : current; 331 | return current; 332 | } 333 | 334 | return externalOptionUriFilter; 335 | }) 336 | */ 337 | -------------------------------------------------------------------------------- /src/material/actions-trcl.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/material/actions.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/material/array.html: -------------------------------------------------------------------------------- 1 |
4 | 5 | 6 | 11 | 18 | close 19 | 20 | 21 | 22 |
23 |
26 | 27 | 32 | 33 | {{ form.add || 'Add'}} 34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/material/autocomplete.html: -------------------------------------------------------------------------------- 1 |
4 | 17 | 18 | {{item.name}} 19 | 20 | 21 | No matches found 22 | 23 | 24 |
25 | -------------------------------------------------------------------------------- /src/material/card-content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/material/card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/material/checkbox.html: -------------------------------------------------------------------------------- 1 |
4 | 12 | {{::form.title}} 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/material/checkboxes.html: -------------------------------------------------------------------------------- 1 |
5 | 6 |
7 | 14 | {{::form.titleMap[$index].name}} 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/material/chips.html: -------------------------------------------------------------------------------- 1 |
3 | 5 | 6 | 7 | {{$chip}} 8 | 9 | 10 |
11 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/material/date.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/material/default.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /src/material/fieldset-trcl.html: -------------------------------------------------------------------------------- 1 |
2 | {{ form.title }} 3 |
4 |
5 | -------------------------------------------------------------------------------- /src/material/fieldset.html: -------------------------------------------------------------------------------- 1 |
2 | {{ form.title }} 3 |
4 | -------------------------------------------------------------------------------- /src/material/help.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/material/radio-buttons.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | 5 |
6 |
7 | 8 | 21 | 22 | 23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/material/radios-inline.html: -------------------------------------------------------------------------------- 1 |
3 | 4 | 13 | 14 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/material/radios.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
5 | 10 | 15 | 16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/material/section.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/material/select.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 11 | {{::filtered.name}} 14 | 15 | {{::opt.name}} 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/material/slider.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/material/submit.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | {{::form.tip}} 7 | {{::form.title}} 8 | 9 |
10 | -------------------------------------------------------------------------------- /src/material/switch.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | {{::form.title}} 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/material/tabarray.html: -------------------------------------------------------------------------------- 1 | 2 |
4 | 21 | 22 |
23 |
24 |
28 | 29 | 30 | 31 | 38 |
39 |
40 |
41 | 42 | 57 | 58 |
59 | -------------------------------------------------------------------------------- /src/material/tabs.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/material/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | require('./type-parser.js'); 2 | require('./material-decorator.js'); 3 | require('./material-class.js'); 4 | -------------------------------------------------------------------------------- /src/type-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * It might be a bug, but currently input[type=number] does not add 3 | * a parser, so the model gets populated with a string. It does however stop non numbers so it 4 | * must have some preproccessing. Anyway, this adds parser before schema-validate hooks into it. 5 | * FIXME: this is still not a complete solution. Inputting a string in an input[type=number] results 6 | * in parsers never firing and ngModel value removed. So no validation from schema-validate either. 7 | */ 8 | angular.module('schemaForm').directive('sfTypeParser', function() { 9 | return { 10 | restrict: 'A', 11 | scope: false, 12 | require: 'ngModel', 13 | link: function(scope, element, attrs, ngModel) { 14 | var once = scope.$watch(attrs.sfTypeParser, function(schema) { 15 | if (!schema) { 16 | return; 17 | } 18 | 19 | var isNumber = schema.type.indexOf('number') !== -1; 20 | var isInteger = schema.type.indexOf('integer') !== -1; 21 | var numberRE = /^[0-9]*$/; 22 | // Use index of since type can be either an array with two values or a string. 23 | if (isNumber || isInteger) { 24 | // The timing here seems to work. i.e. we get in before schema-validate 25 | ngModel.$parsers.push(function(viewValue) { 26 | var value; 27 | if (isNumber) { 28 | value = parseFloat(viewValue); 29 | } else if (numberRE.test(viewValue)) { 30 | // We test the value to check that it's a valid integer, otherwise we can easily 31 | // get float -> integer parsing behind the scenes. 32 | value = parseInt(viewValue, 10); 33 | } 34 | console.log('parser', numberRE.test(viewValue), viewValue, value) 35 | if (value === undefined || isNaN(value)) { 36 | //Let the validation fail. @FIXME: it fails with "required" for some reason. 37 | return viewValue; 38 | } 39 | return value; 40 | }); 41 | } 42 | 43 | once(); 44 | }); 45 | } 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /test/protractor/conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | specs: [ 'specs/*.js' ], 3 | baseUrl: 'http://localhost:63342/', 4 | multiCapabilities: [ 5 | { 'browserName': 'firefox' }, 6 | { 'browserName': 'chrome' } 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /test/protractor/specs/validation-messages.js: -------------------------------------------------------------------------------- 1 | /* global browser, it, describe, element, by */ 2 | 3 | describe('Schema Form validation messages', function() { 4 | 5 | describe('#string', function() { 6 | var URL = 'angular-schema-form-material/examples/material-example.html#/86fb7505a8ab6a43bc70'; 7 | 8 | it('should not complain if it gets a normal string', function() { 9 | browser.get(URL); 10 | var input = element.all(by.css('form[name=ngform] input')).first(); 11 | input.sendKeys('string'); 12 | 13 | expect(input.getAttribute('value')).toEqual('string'); 14 | expect(input.evaluate('ngModel.$valid')).toEqual(true); 15 | 16 | }); 17 | 18 | 19 | var validationMessageTestBuider = function(nr, value, validationMessage) { 20 | it('should say "' + validationMessage + '" when input is ' + value, function() { 21 | browser.get(URL); 22 | var input = element.all(by.css('form[name=ngform] input')).get(nr); 23 | input.sendKeys(value); 24 | 25 | var message = element.all(by.css('form[name=ngform] div[sf-message]'));//.get(nr); 26 | expect(input.evaluate('ngModel.$valid')).toEqual(false); 27 | 28 | /* no error messages at this point */ 29 | //expect(message.getText()).toEqual(validationMessage); 30 | 31 | }); 32 | }; 33 | 34 | var stringTests = { 35 | 's': 'String is too short (1 chars), minimum 3', 36 | 'tooo long string': 'String is too long (11 chars), maximum 10', 37 | 'foo 66': 'String does not match pattern: ^[a-zA-Z ]+$' 38 | }; 39 | 40 | Object.keys(stringTests).forEach(function(value) { 41 | validationMessageTestBuider(0, value, stringTests[value]); 42 | }); 43 | 44 | 45 | var integerTests = { 46 | '3': '3 is less than the allowed minimum of 6', 47 | '66': '66 is greater than the allowed maximum of 50', 48 | '11': 'Value is not a multiple of 3', 49 | // Chrome no longer lets you input anything but numbers in a type="number" input. 50 | 'aaa': 'Required' //'Value is not a valid number' 51 | }; 52 | 53 | Object.keys(integerTests).forEach(function(value) { 54 | validationMessageTestBuider(1, value, integerTests[value]); 55 | }); 56 | 57 | 58 | it('should say "Required" when fields are required', function() { 59 | browser.get(URL); 60 | element.all(by.css('form[name=ngform]')).submit(); 61 | var input = element.all(by.css('form[name=ngform] input')).get(1); 62 | 63 | var message = element.all(by.css('form[name=ngform] div[sf-message]'));//.get(1); 64 | expect(input.evaluate('ngModel.$valid')).toEqual(false); 65 | 66 | /* no error messages at this point */ 67 | //expect(message.getText()).toEqual('Required'); 68 | 69 | }); 70 | }); 71 | }); -------------------------------------------------------------------------------- /test/protractor/specs/validation-tv4-errors.js: -------------------------------------------------------------------------------- 1 | /* global browser, it, describe, element, by */ 2 | 3 | describe('Schema Form validation messages', function () { 4 | 5 | describe('tv4-validators', function () { 6 | var URL = 'angular-schema-form-material/examples/material-example.html#/86fb7505a8ab6a43bc70'; 7 | 8 | var validationMessageTestBuilder = function (testToDo) { 9 | 10 | Object.keys(testToDo.validationCodes).forEach(function (validationCodeIndex) { 11 | it('validation test, expecting class "' + testToDo.validationCodes[validationCodeIndex] + ' when input is ' + testToDo.value, function () { 12 | 13 | browser.get(URL); 14 | var input = element.all(by.css('form[name=ngform] input')).get(testToDo.field); 15 | input.sendKeys(testToDo.value); 16 | 17 | expect(input.evaluate('ngModel.$valid')).toEqual(testToDo.valid); 18 | expect(input.getAttribute('class')).toMatch(testToDo.validationCodes[validationCodeIndex]) 19 | 20 | 21 | }); 22 | 23 | }); 24 | 25 | }; 26 | 27 | 28 | var testsToDo = [ 29 | { 30 | field: 0, //0 string, 1 integer 31 | value: "s", 32 | validationCodes: ['ng-invalid-tv4-200'], 33 | valid: false 34 | }, 35 | { 36 | field: 0, 37 | value: 'tooo long string', 38 | validationCodes: ['ng-invalid-tv4-201'], 39 | valid: false 40 | }, 41 | { 42 | field: 0, 43 | value: 'foo 66', 44 | validationCodes: ['ng-invalid-tv4-202'], 45 | valid: false 46 | }, 47 | { 48 | field: 0, 49 | value: 'foobar', 50 | validationCodes: ['ng-valid-tv4-200'], 51 | valid: true 52 | }, 53 | { 54 | field: 1, 55 | value: '3', 56 | validationCodes: ['ng-invalid-tv4-101'], 57 | valid: false 58 | }, 59 | { 60 | field: 1, 61 | value: '66', 62 | validationCodes: ['ng-invalid-tv4-103'], 63 | valid: false 64 | }, 65 | { 66 | field: 1, 67 | value: '11', 68 | validationCodes: ['ng-invalid-tv4-100'], 69 | valid: false 70 | }, 71 | { 72 | field: 1, 73 | value: 'aaa', 74 | validationCodes: ['ng-invalid-tv4-302'], 75 | valid: false 76 | } 77 | ]; 78 | 79 | 80 | Object.keys(testsToDo).forEach(function (testToDoIndex) { 81 | validationMessageTestBuilder(testsToDo[testToDoIndex]); 82 | }); 83 | 84 | 85 | }); 86 | }); -------------------------------------------------------------------------------- /webpack.config.dist.js: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.config.js'); 2 | const path = require('path'); 3 | 4 | config.entry = { 5 | 'angular-schema-form-material': [ path.join(__dirname, 'src', 'module') ], 6 | 'angular-schema-form-material-bundled': [ 'angular-schema-form', path.join(__dirname, 'src', 'module') ], 7 | 'angular-schema-form-material.min': [ path.join(__dirname, 'src', 'module') ], 8 | 'angular-schema-form-material-bundled.min': [ 'angular-schema-form', path.join(__dirname, 'src', 'module') ], 9 | } 10 | 11 | module.exports = config; 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | /** 3 | * NOTE in order to build with angular-schema-form you must 4 | * have it cloned as a sibling directory to this one or npm 5 | * installed with the version you wish to build with. 6 | */ 7 | const webpack = require('webpack'); 8 | const path = require('path'); 9 | const package = require('./package.json'); 10 | const buildDate = new Date(); 11 | console.log('Angular Schema Form Material v' + package.version); 12 | const plugins = [ 13 | new webpack.BannerPlugin( 14 | 'angular-schema-form-material\n' + 15 | '@version ' + package.version + '\n' + 16 | '@date ' + buildDate.toUTCString() + '\n' + 17 | '@link https://github.com/json-schema-form/angular-schema-form-material\n' + 18 | '@license MIT\n' + 19 | 'Copyright (c) 2014-' + buildDate.getFullYear() + ' JSON Schema Form'), 20 | /* Minification only occurs if the output is named .min */ 21 | new webpack.optimize.UglifyJsPlugin( 22 | { 23 | include: /\.min\.js$/, 24 | minimize: true 25 | }) 26 | ]; 27 | 28 | module.exports = { 29 | entry: { 30 | 'angular-schema-form-material': [ path.join(__dirname, 'src', 'module') ], 31 | 'angular-schema-form-material-bundled': [ 'angular-schema-form', path.join(__dirname, 'src', 'module') ], 32 | }, 33 | output: { 34 | path: path.join(__dirname, 'dist'), 35 | filename: '[name].js', 36 | sourceMapFilename: '[name].map' 37 | }, 38 | resolve: { 39 | modules: [ 40 | path.join(__dirname, "src"), 41 | path.join(__dirname, "src", "material"), 42 | path.join(__dirname, "..", "angular-schema-form", "dist"), 43 | 'node_modules', 44 | ], 45 | extensions: [ '.js', '.html' ] 46 | }, 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.js$/, 51 | use: [{ 52 | loader: 'babel-loader', 53 | options: { 54 | presets: [ 55 | [ "es2015", { "modules": false }] 56 | ] 57 | } 58 | }], 59 | exclude: /(node_modules|angular-schema-form)/ 60 | }, 61 | { 62 | test: /\.html$/, 63 | use: [{ 64 | loader: 'ngtemplate-loader', 65 | options: { 66 | relativeTo: path.join(__dirname, 'src') 67 | } 68 | }, 'html-loader'], 69 | exclude: /(index)/ 70 | } 71 | ] 72 | }, 73 | externals: { 74 | 'angular': 'var angular', 75 | 'tv4': 'var tv4', 76 | 'bundle!angular-schema-form': 'commonjs angular-schema-form' 77 | }, 78 | plugins: plugins 79 | }; 80 | --------------------------------------------------------------------------------