|
| 1 | +Extending Schema Form |
| 2 | +===================== |
| 3 | +Schema Form is designed to be easily extended and there are two basic ways to do it: |
| 4 | + |
| 5 | +1. Add a new type of field |
| 6 | +2. Add a new decorator |
| 7 | + |
| 8 | + |
| 9 | +Adding a Field |
| 10 | +-------------- |
| 11 | +To add a new field to Schema Form you need to create a new form type and match that form type with |
| 12 | +a template snippet. To do this you use the `schemaFormDecoratorsProvider.addMapping()` function. |
| 13 | + |
| 14 | +Ex. from the [datepicker add-on](https://github.com/Textalk/angular-schema-form-datepicker/blob/master/src/bootstrap-datepicker.js#L18) |
| 15 | +```javascript |
| 16 | + schemaFormDecoratorsProvider.addMapping( |
| 17 | + 'bootstrapDecorator', |
| 18 | + 'datepicker', |
| 19 | + 'directives/decorators/bootstrap/datepicker/datepicker.html' |
| 20 | +); |
| 21 | +``` |
| 22 | + |
| 23 | +The second argument is the name of your new form type, in this case `datepicker`, and the third is |
| 24 | +the template we bind to it (the first is the decorator, use `bootstrapDecorator` unless you know |
| 25 | +what you are doing). |
| 26 | + |
| 27 | +What this means is that a form definition like this: |
| 28 | +```javascript |
| 29 | +$scope.form = [ |
| 30 | + { |
| 31 | + key: "birthday", |
| 32 | + type: "datepicker" |
| 33 | + } |
| 34 | +]; |
| 35 | +``` |
| 36 | +...will result in the `datepicker.html` template to be used to render that field in the form. |
| 37 | + |
| 38 | +But wait, where is all the code? Basically it's then up to the template to use directives to |
| 39 | +implement whatever it likes to do. It does have some help though, lets look at template example and |
| 40 | +go through the basics. |
| 41 | + |
| 42 | +This is the template for the datepicker: |
| 43 | +```html |
| 44 | +<div class="form-group" ng-class="{'has-error': hasError()}"> |
| 45 | + <label class="control-label" ng-show="showTitle()">{{form.title}}</label> |
| 46 | + |
| 47 | + <input ng-show="form.key" |
| 48 | + style="background-color: white" |
| 49 | + type="text" |
| 50 | + class="form-control" |
| 51 | + schema-validate="form" |
| 52 | + ng-model="$$value$$" |
| 53 | + pick-a-date |
| 54 | + min-date="form.minDate" |
| 55 | + max-date="form.maxDate" |
| 56 | + format="form.format" /> |
| 57 | + |
| 58 | + <span class="help-block">{{ (hasError() && errorMessage(schemaError())) || form.description}}</span> |
| 59 | +</div> |
| 60 | +``` |
| 61 | + |
| 62 | +### What's on the scope? |
| 63 | +Each form field will be rendered inside a decorator directive, created by the |
| 64 | +`schemaFormDecorators` factory service, *do* |
| 65 | +[check the source](https://github.com/Textalk/angular-schema-form/blob/master/src/services/decorators.js#L33). |
| 66 | + |
| 67 | +This means you have several helper functions and values on scope, most important of this `form`. The |
| 68 | +`form` variable contains the merged form definition for that field, i.e. your supplied form object + |
| 69 | +the defaults from the schema (it also has its part of the schema under *form.schema*). |
| 70 | +This is how you define and use new form field options, whatever is set on the form object is |
| 71 | +available here for you to act on. |
| 72 | + |
| 73 | + | Name | What it does | |
| 74 | + |----------|----------------| |
| 75 | + | form | Form definition object | |
| 76 | + | showTitle() | Shorthand for `form && form.notitle !== true && form.title` | |
| 77 | + | errorMessage(msg) | Error message formatting, makes validationMessage option work. | |
| 78 | + | evalInScope(expr, locals) | Eval supplied expression, ie scope.$eval | |
| 79 | + | evalExpr(expr, locals) | Eval an expression in the parent scope of the main `sf-schema` directive. | |
| 80 | + | buttonClick($event, form) | Use this with ng-click to execute form.onClick | |
| 81 | + |
| 82 | +### The magic $$value$$ |
| 83 | +Schema Form wants to play nice with the built in Angular directives for form. Especially `ng-model` |
| 84 | +which we want to handle the two way binding against our model value. Also by using `ng-model` we |
| 85 | +get all the nice validation states from the `ngModelController` and `FormController` that we all |
| 86 | +know and love. |
| 87 | + |
| 88 | +To get that working properly we had to resort to a bit of trickery, right before we let Angular |
| 89 | +compile the field template we do a simple string replacement of `$$value$$` and replace that |
| 90 | +with the path to the current form field on the model, i.e. `form.key`. |
| 91 | + |
| 92 | +So `ng-model="$$value$$"` becomes something like `ng-model="model['person']['address']['street']"`, |
| 93 | +you can see this if you inspect the final form in the browser. |
| 94 | + |
| 95 | +So basically always have a `ng-model="$$value$$"` (Pro tip: ng-model is fine on any element, put |
| 96 | + it on the same div as your custom directive and require the ngModelController for full control). |
| 97 | + |
| 98 | +### schema-validate directive |
| 99 | +`schema-validate` is a directive that you should put on the same element as your `ng-model`. It is |
| 100 | +responsible for validating the value against the schema using [tv4js](https://github.com/geraintluff/tv4) |
| 101 | +It takes the form definition as an argument. |
| 102 | + |
| 103 | +`schema-validate` also exports some things on the scope: |
| 104 | +| Name | What it does | |
| 105 | +|----------|--------------| |
| 106 | +| ngModel | the ngModelController | |
| 107 | +| hasSuccess() | Shorthand for `ngModel.$valid && (!ngModel.$pristine || !ngModel.$isEmpty(ngModel.$modelValue))` | |
| 108 | +| hasError() | Shorthand for `ngModel.$invalid && !ngModel.$pristine` | |
| 109 | +| schemaError() | The current error object from tv4js | |
| 110 | + |
| 111 | + |
| 112 | +### Setting up schema defualts |
| 113 | +So you got this shiny new add-on that adds a fancy field type, but feel a bit bummed out that you |
| 114 | +need to specify it in the form definition all the time? Fear not because you can also add a "rule" |
| 115 | +to map certain types and conditions in the schema to default to your type. |
| 116 | + |
| 117 | +You do this by adding to the `schemaFormProvider.defaults` object. The `schemaFormProvider.defaults` |
| 118 | +is an object with a key for each type *in JSON Schema* with a array of functions as its value. |
| 119 | + |
| 120 | +```javscript |
| 121 | +var defaults = { |
| 122 | + string: [], |
| 123 | + object: [], |
| 124 | + number: [], |
| 125 | + integer: [], |
| 126 | + boolean: [], |
| 127 | + array: [] |
| 128 | +}; |
| 129 | +``` |
| 130 | + |
| 131 | +When schema form traverses the JSON Schema to create default form definitions it first checks the |
| 132 | +*JSON Schema type* and then calls on each function in the corresponding list *in order* until a |
| 133 | +function actually returns something. That is then used as a defualt. |
| 134 | + |
| 135 | +This is the function that makes it a datepicker if its a string and has format "date" or "date-time": |
| 136 | + |
| 137 | +```javascript |
| 138 | +var datepicker = function(name, schema, options) { |
| 139 | + if (schema.type === 'string' && (schema.format === 'date' || schema.format === 'date-time')) { |
| 140 | + var f = schemaFormProvider.stdFormObj(name, schema, options); |
| 141 | + f.key = options.path; |
| 142 | + f.type = 'datepicker'; |
| 143 | + options.lookup[sfPathProvider.stringify(options.path)] = f; |
| 144 | + return f; |
| 145 | + } |
| 146 | +}; |
| 147 | + |
| 148 | +// Put it first in the list of functions |
| 149 | +schemaFormProvider.defaults.string.unshift(datepicker); |
| 150 | +``` |
| 151 | + |
| 152 | +Decorators |
| 153 | +---------- |
| 154 | +Decorators are a second way to extend Schema Form, the thought being that you should easily be able |
| 155 | +to change *every* field. Maybe you like it old school and want to use bootstrap 2. Or maybe you like |
| 156 | +to generate a table with the data instead? Right now there are no other decorators than bootstrap 3. |
| 157 | + |
| 158 | +Basically a *decorator* sets up all the mappings between form types and their respective templates |
| 159 | +using the `decoratorsProvider.createDecorator()` function. |
| 160 | + |
| 161 | +```javascript |
| 162 | +var base = 'directives/decorators/bootstrap/'; |
| 163 | + |
| 164 | +decoratorsProvider.createDecorator('bootstrapDecorator', { |
| 165 | + textarea: base + 'textarea.html', |
| 166 | + fieldset: base + 'fieldset.html', |
| 167 | + array: base + 'array.html', |
| 168 | + tabarray: base + 'tabarray.html', |
| 169 | + tabs: base + 'tabs.html', |
| 170 | + section: base + 'section.html', |
| 171 | + conditional: base + 'section.html', |
| 172 | + actions: base + 'actions.html', |
| 173 | + select: base + 'select.html', |
| 174 | + checkbox: base + 'checkbox.html', |
| 175 | + checkboxes: base + 'checkboxes.html', |
| 176 | + number: base + 'default.html', |
| 177 | + password: base + 'default.html', |
| 178 | + submit: base + 'submit.html', |
| 179 | + button: base + 'submit.html', |
| 180 | + radios: base + 'radios.html', |
| 181 | + 'radios-inline': base + 'radios-inline.html', |
| 182 | + radiobuttons: base + 'radio-buttons.html', |
| 183 | + help: base + 'help.html', |
| 184 | + 'default': base + 'default.html' |
| 185 | +}, [ |
| 186 | + function(form) { |
| 187 | + if (form.readonly && form.key && form.type !== 'fieldset') { |
| 188 | + return base + 'readonly.html'; |
| 189 | + } |
| 190 | + } |
| 191 | +]); |
| 192 | +``` |
| 193 | +`decoratorsProvider.createDecorator(name, mapping, rules)` takes a name argument, a mapping object |
| 194 | +(type -> template) and an optional list of rule functions. |
| 195 | + |
| 196 | +When the decorator is trying to match a form type against a tempate it first executes all the rules |
| 197 | +in order. If one returns that is used as template, otherwise it checks the mappings. |
0 commit comments