├── src
├── material
│ ├── card.html
│ ├── actions-trcl.html
│ ├── actions.html
│ ├── section.html
│ ├── card-content.html
│ ├── help.html
│ ├── fieldset.html
│ ├── tabs.html
│ ├── fieldset-trcl.html
│ ├── slider.html
│ ├── submit.html
│ ├── textarea.html
│ ├── checkbox.html
│ ├── date.html
│ ├── switch.html
│ ├── chips.html
│ ├── checkboxes.html
│ ├── radios-inline.html
│ ├── default.html
│ ├── radios.html
│ ├── autocomplete.html
│ ├── select.html
│ ├── radio-buttons.html
│ ├── array.html
│ └── tabarray.html
├── module.js
├── material-class.js
├── type-parser.js
└── material-decorator.js
├── test
└── protractor
│ ├── conf.js
│ └── specs
│ ├── validation-tv4-errors.js
│ └── validation-messages.js
├── examples
├── data
│ ├── switch.json
│ ├── remote-titlemap-books.json
│ ├── remote-titlemap-movies.json
│ ├── types.json
│ ├── simple-oneOf.json
│ ├── conditional-required.json
│ ├── simple.json
│ ├── autocomplete.json
│ ├── complex-keys.json
│ ├── tabarray.json
│ ├── array.json
│ ├── tabs.json
│ ├── grid.json
│ ├── titlemaps.json
│ └── sink.json
└── example.html
├── .jscsrc
├── webpack.config.dist.js
├── bower.json
├── LICENSE
├── package.json
├── gulpfile.js
├── CONTRIBUTING.md
├── .gitignore
├── README.md
├── webpack.config.js
├── material-decorator.min.js
├── dist
├── angular-schema-form-material.min.js
└── angular-schema-form-material.js
└── material-decorator.js
/src/material/card.html:
--------------------------------------------------------------------------------
1 |
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/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/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 | -------------------------------------------------------------------------------- /src/material/select.html: -------------------------------------------------------------------------------- 1 |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 | -------------------------------------------------------------------------------- /src/material/array.html: -------------------------------------------------------------------------------- 1 |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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angular Material Decorator 2 | ========================== 3 | 4 | [](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/material/tabarray.html: -------------------------------------------------------------------------------- 1 | 2 | 59 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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": "{{pretty()}}
121 | 141 | 142 | JavaScript license information 143 | 144 |
145 |