mirror of
https://github.com/nasa/openmct.git
synced 2024-12-25 07:41:06 +00:00
[Forms] Merge in forms bundle
Merge in forms bundle from its development branch to begin work integrating Forms into the general UI, for WTD-593.
This commit is contained in:
commit
2bee842509
@ -10,7 +10,8 @@
|
|||||||
"platform/features/layout",
|
"platform/features/layout",
|
||||||
"platform/features/plot",
|
"platform/features/plot",
|
||||||
"platform/features/scrolling",
|
"platform/features/scrolling",
|
||||||
|
"platform/forms",
|
||||||
|
|
||||||
"example/generator",
|
"example/generator",
|
||||||
"example/persistence"
|
"example/persistence"
|
||||||
]
|
]
|
||||||
|
18
example/forms/bundle.json
Normal file
18
example/forms/bundle.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "Declarative Forms example",
|
||||||
|
"sources": "src",
|
||||||
|
"extensions": {
|
||||||
|
"controllers": [
|
||||||
|
{
|
||||||
|
"key": "ExampleFormController",
|
||||||
|
"implementation": "ExampleFormController.js",
|
||||||
|
"depends": [ "$scope" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"templateUrl": "templates/exampleForm.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
19
example/forms/res/templates/exampleForm.html
Normal file
19
example/forms/res/templates/exampleForm.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<div ng-controller="ExampleFormController">
|
||||||
|
<mct-form structure="form" ng-model="state" name="aForm">
|
||||||
|
</mct-form>
|
||||||
|
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Dirty: {{aForm.$dirty}}</li>
|
||||||
|
<li>Valid: {{aForm.$valid}}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
|
||||||
|
|
||||||
|
<textarea>
|
||||||
|
{{state | json}}
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
</pre>
|
||||||
|
</div>
|
79
example/forms/src/ExampleFormController.js
Normal file
79
example/forms/src/ExampleFormController.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function ExampleFormController($scope) {
|
||||||
|
$scope.state = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.form = {
|
||||||
|
name: "An example form.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: "First section",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
name: "Check me",
|
||||||
|
control: "checkbox",
|
||||||
|
key: "checkMe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Enter your name",
|
||||||
|
required: true,
|
||||||
|
control: "textfield",
|
||||||
|
key: "yourName"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Enter a number",
|
||||||
|
control: "textfield",
|
||||||
|
pattern: "^\\d+$",
|
||||||
|
key: "aNumber"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Second section",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
name: "Pick a date",
|
||||||
|
required: true,
|
||||||
|
description: "Enter date in form YYYY-DDD",
|
||||||
|
control: "datetime",
|
||||||
|
key: "aDate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Choose something",
|
||||||
|
control: "select",
|
||||||
|
options: [
|
||||||
|
{ name: "Hats", value: "hats" },
|
||||||
|
{ name: "Bats", value: "bats" },
|
||||||
|
{ name: "Cats", value: "cats" },
|
||||||
|
{ name: "Mats", value: "mats" }
|
||||||
|
],
|
||||||
|
key: "aChoice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Choose something",
|
||||||
|
control: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ name: "Hats", value: "hats" },
|
||||||
|
{ name: "Bats", value: "bats" },
|
||||||
|
{ name: "Cats", value: "cats" },
|
||||||
|
{ name: "Mats", value: "mats" }
|
||||||
|
],
|
||||||
|
key: "aRequiredChoice"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExampleFormController;
|
||||||
|
}
|
||||||
|
);
|
@ -1,3 +1,87 @@
|
|||||||
This bundle contains a general implementation of forms in Open MCT Web.
|
# Overview
|
||||||
This allows forms to be expressed using a reasonably concise declarative
|
|
||||||
syntax, and rendered as Angular templates in a consistent fashion.
|
This bundle contains a general implementation of forms in Open MCT Web.
|
||||||
|
This allows forms to be expressed using a reasonably concise declarative
|
||||||
|
syntax, and rendered as Angular templates in a consistent fashion.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
To include a form with a declarative definition, use the `mct-form`
|
||||||
|
directive, e.g.:
|
||||||
|
|
||||||
|
<mct-form ng-model="myModel" structure="myStructure" name="myForm">
|
||||||
|
</mct-form>
|
||||||
|
|
||||||
|
The attributes utilized by this form are as follows:
|
||||||
|
|
||||||
|
* `ng-model`: The object which should contain the full form input. Individual
|
||||||
|
fields in this model are bound to individual controls; the names used for
|
||||||
|
these fields are provided in the form structure (see below).
|
||||||
|
* `structure`: The structure of the form; e.g. sections, rows, their names,
|
||||||
|
and so forth. The value of this attribute should be an Angular expression.
|
||||||
|
* `name`: The name in the containing scope under which to publish form
|
||||||
|
"meta-state", e.g. `$valid`, `$dirty`, etc. This is as the behavior of
|
||||||
|
`ng-form`. Passed as plain text in the attribute.
|
||||||
|
|
||||||
|
## Form structure
|
||||||
|
|
||||||
|
A form's structure is described as a JavaScript object in the following form:
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": ... title to display for the form, as a string ...,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"name": ... title to display for the section ...,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"name": ... title to display for this row ...,
|
||||||
|
"control": ... symbolic key for the control ...,
|
||||||
|
"key": ... field name in ng-model ...
|
||||||
|
"pattern": ... optional, reg exp to match against ...
|
||||||
|
"required": ... optional boolean ...
|
||||||
|
"options": [
|
||||||
|
"name": ... name to display (e.g. in a select) ...,
|
||||||
|
"value": ... value to store in the model ...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
... and other rows ...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
... and other sections ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Note that `pattern` may be specified as a string, to simplify storing
|
||||||
|
for structures as JSON when necessary. The string should be given in
|
||||||
|
a form appropriate to pass to a `RegExp` constructor.
|
||||||
|
|
||||||
|
## Adding controls
|
||||||
|
|
||||||
|
Four standard control types are included in the forms bundle:
|
||||||
|
|
||||||
|
* `textfield`: An area to enter plain text.
|
||||||
|
* `select`: A drop-down list of options.
|
||||||
|
* `checkbox`: A box which may be checked/unchecked.
|
||||||
|
* `datetime`: An input for UTC date/time entry; gives result as a
|
||||||
|
UNIX timestamp, in milliseconds since start of 1970, UTC.
|
||||||
|
|
||||||
|
New controls may be added as extensions of the `controls` category.
|
||||||
|
Extensions of this category have two properites:
|
||||||
|
|
||||||
|
* `key`: The symbolic name for this control (matched against the
|
||||||
|
`control` field in rows of the form structure).
|
||||||
|
* `templateUrl`: The URL to the control's Angular template, relative
|
||||||
|
to the resources directory of the bundle which exposes the extension.
|
||||||
|
|
||||||
|
Within the template for a control, the following variables will be
|
||||||
|
included in scope:
|
||||||
|
|
||||||
|
* `ngModel`: The model where form input will be stored. Notably we
|
||||||
|
also need to look at `field` (see below) to determine which field
|
||||||
|
in the model should be modified.
|
||||||
|
* `ngRequired`: True if input is required.
|
||||||
|
* `ngPattern`: The pattern to match against (for text entry.)
|
||||||
|
* `options`: The options for this control, as passed from the
|
||||||
|
`options` property of an individual row.
|
||||||
|
* `field`: Name of the field in `ngModel` which will hold the value
|
||||||
|
for this control.
|
72
platform/forms/bundle.json
Normal file
72
platform/forms/bundle.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "MCT Forms",
|
||||||
|
"description": "Form generator; includes directive and some controls.",
|
||||||
|
"extensions": {
|
||||||
|
"directives": [
|
||||||
|
{
|
||||||
|
"key": "mctForm",
|
||||||
|
"implementation": "MCTForm.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "mctControl",
|
||||||
|
"implementation": "MCTControl.js",
|
||||||
|
"depends": [ "controls[]" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"controls": [
|
||||||
|
{
|
||||||
|
"key": "checkbox",
|
||||||
|
"templateUrl": "templates/controls/checkbox.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "datetime",
|
||||||
|
"templateUrl": "templates/controls/datetime.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "select",
|
||||||
|
"templateUrl": "templates/controls/select.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "textfield",
|
||||||
|
"templateUrl": "templates/controls/textfield.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"controllers": [
|
||||||
|
{
|
||||||
|
"key": "DateTimeController",
|
||||||
|
"implementation": "controllers/DateTimeController.js",
|
||||||
|
"depends": [ "$scope" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"templates": [
|
||||||
|
{
|
||||||
|
"key": "_checkbox",
|
||||||
|
"templateUrl": "templates/_checkbox.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "_checkboxes",
|
||||||
|
"templateUrl": "templates/_checkboxes.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "_datetime",
|
||||||
|
"templateUrl": "templates/_datetime.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "_select",
|
||||||
|
"templateUrl": "templates/_select.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "_selects",
|
||||||
|
"templateUrl": "templates/_selects.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "_textfield",
|
||||||
|
"templateUrl": "templates/_textfield.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "_textfields",
|
||||||
|
"templateUrl": "templates/_textfields.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
6
platform/forms/lib/moment.min.js
vendored
Normal file
6
platform/forms/lib/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
platform/forms/res/templates/controls/checkbox.html
Normal file
7
platform/forms/res/templates/controls/checkbox.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<label class="checkbox custom no-text">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="mctControl"
|
||||||
|
ng-model="ngModel[field]"
|
||||||
|
ng-disabled="ngDisabled">
|
||||||
|
<em> </em>
|
||||||
|
</label>
|
62
platform/forms/res/templates/controls/datetime.html
Normal file
62
platform/forms/res/templates/controls/datetime.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<div class='form-control complex datetime'>
|
||||||
|
|
||||||
|
<div class='field-hints'>
|
||||||
|
<span class='hint date'>Date</span>
|
||||||
|
<span class='hint time sm'>Hour</span>
|
||||||
|
<span class='hint time sm'>Min</span>
|
||||||
|
<span class='hint time sm'>Sec</span>
|
||||||
|
<span class='hint timezone'>Timezone</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-form name="mctControl">
|
||||||
|
<div class='fields' ng-controller="DateTimeController">
|
||||||
|
<span class='field control date'>
|
||||||
|
<input type='text'
|
||||||
|
name='date'
|
||||||
|
placeholder="YYYY-DDD"
|
||||||
|
ng-pattern="/\d\d\d\d-\d\d\d/"
|
||||||
|
ng-model='datetime.date'
|
||||||
|
ng-required='true'/>
|
||||||
|
</span>
|
||||||
|
<span class='field control time sm'>
|
||||||
|
<input type='text'
|
||||||
|
name='hour'
|
||||||
|
maxlength='2'
|
||||||
|
min='0'
|
||||||
|
max='23'
|
||||||
|
integer
|
||||||
|
ng-pattern='/\d+/'
|
||||||
|
ng-model="datetime.hour"
|
||||||
|
ng-required='true'/>
|
||||||
|
</span>
|
||||||
|
<span class='field control time sm'>
|
||||||
|
<input type='text'
|
||||||
|
name='min'
|
||||||
|
maxlength='2'
|
||||||
|
min='0'
|
||||||
|
max='59'
|
||||||
|
integer
|
||||||
|
ng-pattern='/\d+/'
|
||||||
|
ng-model="datetime.min"
|
||||||
|
ng-required='true'/>
|
||||||
|
</span>
|
||||||
|
<span class='field control time sm'>
|
||||||
|
<input type='text'
|
||||||
|
name='sec'
|
||||||
|
maxlength='2'
|
||||||
|
min='0'
|
||||||
|
max='59'
|
||||||
|
integer
|
||||||
|
ng-pattern='/\d+/'
|
||||||
|
ng-model="datetime.sec"
|
||||||
|
ng-required='true'/>
|
||||||
|
</span>
|
||||||
|
<span class='field control timezone'>
|
||||||
|
UTC
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-form>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
8
platform/forms/res/templates/controls/select.html
Normal file
8
platform/forms/res/templates/controls/select.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<select class='form-control input select'
|
||||||
|
ng-model="ngModel[field]"
|
||||||
|
ng-options="opt.value as opt.name for opt in options"
|
||||||
|
ng-required="ngRequired"
|
||||||
|
name="mctControl">
|
||||||
|
<option value="" ng-if="!ngModel[field]">- Select One -</option>
|
||||||
|
<span class='ui-symbol arw colorKey'>v</span>
|
||||||
|
</select>
|
9
platform/forms/res/templates/controls/textfield.html
Normal file
9
platform/forms/res/templates/controls/textfield.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<span class='form-control shell'>
|
||||||
|
<span class='field control'>
|
||||||
|
<input type="text"
|
||||||
|
ng-required="ngRequired"
|
||||||
|
ng-model="ngModel[field]"
|
||||||
|
ng-pattern="ngPattern"
|
||||||
|
name="mctControl">
|
||||||
|
</span>
|
||||||
|
</span>
|
55
platform/forms/res/templates/form.html
Normal file
55
platform/forms/res/templates/form.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<form name="mctForm" novalidate>
|
||||||
|
|
||||||
|
<div class="form">
|
||||||
|
<span ng-repeat="section in structure.sections">
|
||||||
|
<div class="section-header" ng-if="section.name">
|
||||||
|
{{section.name}}
|
||||||
|
</div>
|
||||||
|
<div class="form-section">
|
||||||
|
<ng-form name="mctFormInner" ng-repeat="row in section.rows">
|
||||||
|
<div class="form-row validates"
|
||||||
|
ng-class="{
|
||||||
|
req: row.required,
|
||||||
|
valid: mctFormInner.$dirty && mctFormInner.$valid,
|
||||||
|
invalid: mctFormInner.$dirty && !mctFormInner.$valid
|
||||||
|
}">
|
||||||
|
|
||||||
|
<div class='label' title="{{row.description}}">
|
||||||
|
{{row.name}}
|
||||||
|
<span ng-if="row.description"
|
||||||
|
class="ui-symbol">
|
||||||
|
i
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class='controls'>
|
||||||
|
<div class="wrapper" ng-if="row.control">
|
||||||
|
<mct-control key="row.control"
|
||||||
|
ng-model="ngModel"
|
||||||
|
ng-required="row.required"
|
||||||
|
ng-pattern="getRegExp(row.pattern)"
|
||||||
|
options="row.options"
|
||||||
|
structure="row"
|
||||||
|
field="row.key">
|
||||||
|
</mct-control>
|
||||||
|
</div>
|
||||||
|
<div ng-repeat="item in row.items" class="validates">
|
||||||
|
<ng-form name="mctFormItem">
|
||||||
|
<mct-control key="item.control"
|
||||||
|
ng-model="ngModel"
|
||||||
|
ng-required="item.required"
|
||||||
|
ng-pattern="getRegExp(item.pattern)"
|
||||||
|
options="item.options"
|
||||||
|
structure="row"
|
||||||
|
field="item.key">
|
||||||
|
</mct-control>
|
||||||
|
{{item.name}}
|
||||||
|
</ng-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-form>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
81
platform/forms/src/MCTControl.js
Normal file
81
platform/forms/src/MCTControl.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mct-control will dynamically include the control
|
||||||
|
* for a form element based on a symbolic key. Individual
|
||||||
|
* controls are defined under the extension category
|
||||||
|
* `controls`; this allows plug-ins to introduce new form
|
||||||
|
* control types while still making use of the form
|
||||||
|
* generator to ensure an overall consistent form style.
|
||||||
|
*/
|
||||||
|
function MCTControl(controls) {
|
||||||
|
var controlMap = {};
|
||||||
|
|
||||||
|
// Prepopulate controlMap for easy look up by key
|
||||||
|
controls.forEach(function (control) {
|
||||||
|
var path = [
|
||||||
|
control.bundle.path,
|
||||||
|
control.bundle.resources,
|
||||||
|
control.templateUrl
|
||||||
|
].join("/");
|
||||||
|
controlMap[control.key] = path;
|
||||||
|
});
|
||||||
|
|
||||||
|
function controller($scope) {
|
||||||
|
$scope.$watch("key", function (key) {
|
||||||
|
// Pass the template URL to ng-include via scope.
|
||||||
|
$scope.inclusion = controlMap[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Only show at the element level
|
||||||
|
restrict: "E",
|
||||||
|
|
||||||
|
// Use the included controller to populate scope
|
||||||
|
controller: controller,
|
||||||
|
|
||||||
|
// Use ng-include as a template; "inclusion" will be the real
|
||||||
|
// template path
|
||||||
|
template: '<ng-include src="inclusion"></ng-include>',
|
||||||
|
|
||||||
|
// ngOptions is terminal, so we need to be higher priority
|
||||||
|
priority: 1000,
|
||||||
|
|
||||||
|
// Pass through Angular's normal input field attributes
|
||||||
|
scope: {
|
||||||
|
// Used to choose which form control to use
|
||||||
|
key: "=",
|
||||||
|
|
||||||
|
// The state of the form value itself
|
||||||
|
ngModel: "=",
|
||||||
|
|
||||||
|
// Enabled/disabled state
|
||||||
|
ngDisabled: "=",
|
||||||
|
|
||||||
|
// Whether or not input is required
|
||||||
|
ngRequired: "=",
|
||||||
|
|
||||||
|
// Pattern (for input fields)
|
||||||
|
ngPattern: "=",
|
||||||
|
|
||||||
|
// Set of choices (if any)
|
||||||
|
options: "=",
|
||||||
|
|
||||||
|
// Structure (subtree of Form Structure)
|
||||||
|
structure: "=",
|
||||||
|
|
||||||
|
// Name, as in "<input name="...
|
||||||
|
field: "="
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return MCTControl;
|
||||||
|
}
|
||||||
|
);
|
107
platform/forms/src/MCTForm.js
Normal file
107
platform/forms/src/MCTForm.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*global define,Promise*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module defining MCTForm. Created by vwoeltje on 11/10/14.
|
||||||
|
*/
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Default ng-pattern; any non whitespace
|
||||||
|
var NON_WHITESPACE = /\S/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mct-form directive allows generation of displayable
|
||||||
|
* forms based on a declarative description of the form's
|
||||||
|
* structure.
|
||||||
|
*
|
||||||
|
* This directive accepts three attributes:
|
||||||
|
*
|
||||||
|
* * `ng-model`: The model for the form; where user input
|
||||||
|
* where be stored.
|
||||||
|
* * `structure`: The declarative structure of the form.
|
||||||
|
* Describes what controls should be shown and where
|
||||||
|
* their values should be read/written in the model.
|
||||||
|
* * `name`: The name under which to expose the form's
|
||||||
|
* dirty/valid state. This is similar to ng-form's use
|
||||||
|
* of name, except this will be made available in the
|
||||||
|
* parent scope.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function MCTForm() {
|
||||||
|
var templatePath = [
|
||||||
|
"platform/forms", //MCTForm.bundle.path,
|
||||||
|
"res", //MCTForm.bundle.resources,
|
||||||
|
"templates/form.html"
|
||||||
|
].join("/");
|
||||||
|
|
||||||
|
function controller($scope) {
|
||||||
|
var regexps = [];
|
||||||
|
|
||||||
|
// ng-pattern seems to want a RegExp, and not a
|
||||||
|
// string (despite what documentation says) but
|
||||||
|
// we want form structure to be JSON-expressible,
|
||||||
|
// so we make RegExp's from strings as-needed
|
||||||
|
function getRegExp(pattern) {
|
||||||
|
// If undefined, don't apply a pattern
|
||||||
|
if (!pattern) {
|
||||||
|
return NON_WHITESPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just echo if it's already a regexp
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, assume a string
|
||||||
|
// Cache for easy lookup later (so we don't
|
||||||
|
// creat a new RegExp every digest cycle)
|
||||||
|
if (!regexps[pattern]) {
|
||||||
|
regexps[pattern] = new RegExp(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
return regexps[pattern];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the form state under the requested
|
||||||
|
// name in the parent scope
|
||||||
|
$scope.$watch("mctForm", function (mctForm) {
|
||||||
|
if ($scope.name) {
|
||||||
|
$scope.$parent[$scope.name] = mctForm;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.getRegExp = getRegExp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Only show at the element level
|
||||||
|
restrict: "E",
|
||||||
|
|
||||||
|
// Load the forms template
|
||||||
|
templateUrl: templatePath,
|
||||||
|
|
||||||
|
// Use the controller defined above to
|
||||||
|
// populate/respond to changes in scope
|
||||||
|
controller: controller,
|
||||||
|
|
||||||
|
// Initial an isolate scope
|
||||||
|
scope: {
|
||||||
|
|
||||||
|
// The model: Where form input will actually go
|
||||||
|
ngModel: "=",
|
||||||
|
|
||||||
|
// Form structure; what sections/rows to show
|
||||||
|
structure: "=",
|
||||||
|
|
||||||
|
// Name under which to publish the form
|
||||||
|
name: "@"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return MCTForm;
|
||||||
|
}
|
||||||
|
);
|
48
platform/forms/src/controllers/DateTimeController.js
Normal file
48
platform/forms/src/controllers/DateTimeController.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*global define,moment*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../../lib/moment.min"],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var DATE_FORMAT = "YYYY-DDD";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for the `datetime` form control.
|
||||||
|
* This is a composite control; it includes multiple
|
||||||
|
* input fields but outputs a single timestamp (in
|
||||||
|
* milliseconds since start of 1970) to the ngModel.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function DateTimeController($scope) {
|
||||||
|
|
||||||
|
// Update the
|
||||||
|
function update() {
|
||||||
|
var date = $scope.datetime.date,
|
||||||
|
hour = $scope.datetime.hour,
|
||||||
|
min = $scope.datetime.min,
|
||||||
|
sec = $scope.datetime.sec,
|
||||||
|
fullDateTime = moment.utc(date, DATE_FORMAT)
|
||||||
|
.hour(hour || 0)
|
||||||
|
.minute(min || 0)
|
||||||
|
.second(sec || 0);
|
||||||
|
|
||||||
|
if (fullDateTime.isValid()) {
|
||||||
|
$scope.ngModel[$scope.field] = fullDateTime.valueOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update value whenever any field changes.
|
||||||
|
$scope.$watch("datetime.date", update);
|
||||||
|
$scope.$watch("datetime.hour", update);
|
||||||
|
$scope.$watch("datetime.min", update);
|
||||||
|
$scope.$watch("datetime.sec", update);
|
||||||
|
|
||||||
|
$scope.datetime = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeController;
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
60
platform/forms/test/MCTControlSpec.js
Normal file
60
platform/forms/test/MCTControlSpec.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../src/MCTControl"],
|
||||||
|
function (MCTControl) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("The mct-control directive", function () {
|
||||||
|
var testControls,
|
||||||
|
mockScope,
|
||||||
|
mctControl;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
testControls = [
|
||||||
|
{
|
||||||
|
key: "abc",
|
||||||
|
bundle: { path: "a", resources: "b" },
|
||||||
|
templateUrl: "c/template.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "xyz",
|
||||||
|
bundle: { path: "x", resources: "y" },
|
||||||
|
templateUrl: "z/template.html"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
|
||||||
|
|
||||||
|
mctControl = new MCTControl(testControls);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is restricted to the element level", function () {
|
||||||
|
expect(mctControl.restrict).toEqual("E");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("watches its passed key to choose a template", function () {
|
||||||
|
mctControl.controller(mockScope);
|
||||||
|
|
||||||
|
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||||
|
"key",
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes its template dynamically", function () {
|
||||||
|
mctControl.controller(mockScope);
|
||||||
|
|
||||||
|
mockScope.key = "xyz";
|
||||||
|
mockScope.$watch.mostRecentCall.args[1]("xyz");
|
||||||
|
|
||||||
|
// Should have communicated the template path to
|
||||||
|
// ng-include via the "inclusion" field in scope
|
||||||
|
expect(mockScope.inclusion).toEqual(
|
||||||
|
"x/y/z/template.html"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
90
platform/forms/test/MCTFormSpec.js
Normal file
90
platform/forms/test/MCTFormSpec.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../src/MCTForm"],
|
||||||
|
function (MCTForm) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("The mct-form directive", function () {
|
||||||
|
var mockScope,
|
||||||
|
mctForm;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
|
||||||
|
mockScope.$parent = {};
|
||||||
|
mctForm = new MCTForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is restricted to elements", function () {
|
||||||
|
expect(mctForm.restrict).toEqual("E");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("watches for changes in form by name", function () {
|
||||||
|
// mct-form needs to watch for the form by name
|
||||||
|
// in order to convey changes in $valid, $dirty, etc
|
||||||
|
// up to the parent scope.
|
||||||
|
mctForm.controller(mockScope);
|
||||||
|
|
||||||
|
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||||
|
"mctForm",
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("conveys form status to parent scope", function () {
|
||||||
|
var someState = { someKey: "some value" };
|
||||||
|
mockScope.name = "someName";
|
||||||
|
|
||||||
|
mctForm.controller(mockScope);
|
||||||
|
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](someState);
|
||||||
|
|
||||||
|
expect(mockScope.$parent.someName).toBe(someState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows strings to be converted to RegExps", function () {
|
||||||
|
// This is needed to support ng-pattern in the template
|
||||||
|
mctForm.controller(mockScope);
|
||||||
|
|
||||||
|
// Should have added getRegExp to the scope,
|
||||||
|
// to convert strings to regular expressions
|
||||||
|
expect(mockScope.getRegExp("^\\d+$")).toEqual(/^\d+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the same regexp instance for the same string", function () {
|
||||||
|
// Don't want new instances each digest cycle, for performance
|
||||||
|
var strRegExp = "^[a-z]\\d+$",
|
||||||
|
regExp;
|
||||||
|
|
||||||
|
// Add getRegExp to scope
|
||||||
|
mctForm.controller(mockScope);
|
||||||
|
regExp = mockScope.getRegExp(strRegExp);
|
||||||
|
|
||||||
|
// Same object instance each time...
|
||||||
|
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
|
||||||
|
expect(mockScope.getRegExp(strRegExp)).toBe(regExp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes RegExp objects through untouched", function () {
|
||||||
|
// Permit using forms to simply provide their own RegExp object
|
||||||
|
var regExp = /^\d+[a-d]$/;
|
||||||
|
|
||||||
|
// Add getRegExp to scope
|
||||||
|
mctForm.controller(mockScope);
|
||||||
|
|
||||||
|
// Should have added getRegExp to the scope,
|
||||||
|
// to convert strings to regular expressions
|
||||||
|
expect(mockScope.getRegExp(regExp)).toBe(regExp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes a non-whitespace regexp when no pattern is defined", function () {
|
||||||
|
// If no pattern is supplied, ng-pattern should match anything
|
||||||
|
mctForm.controller(mockScope);
|
||||||
|
expect(mockScope.getRegExp()).toEqual(/\S/);
|
||||||
|
expect(mockScope.getRegExp(undefined)).toEqual(/\S/);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
41
platform/forms/test/controllers/DateTimeControllerSpec.js
Normal file
41
platform/forms/test/controllers/DateTimeControllerSpec.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../../src/controllers/DateTimeController"],
|
||||||
|
function (DateTimeController) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("The date-time directive", function () {
|
||||||
|
var mockScope,
|
||||||
|
controller;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]);
|
||||||
|
controller = new DateTimeController(mockScope);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("watches for changes in fields", function () {
|
||||||
|
["date", "hour", "min", "sec"].forEach(function (fieldName) {
|
||||||
|
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||||
|
"datetime." + fieldName,
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts date-time input into a timestamp", function () {
|
||||||
|
mockScope.ngModel = {};
|
||||||
|
mockScope.field = "test";
|
||||||
|
mockScope.datetime.date = "2014-332";
|
||||||
|
mockScope.datetime.hour = 22;
|
||||||
|
mockScope.datetime.min = 55;
|
||||||
|
mockScope.datetime.sec = 13;
|
||||||
|
|
||||||
|
mockScope.$watch.mostRecentCall.args[1]();
|
||||||
|
|
||||||
|
expect(mockScope.ngModel.test).toEqual(1417215313000);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
5
platform/forms/test/suite.json
Normal file
5
platform/forms/test/suite.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"MCTControl",
|
||||||
|
"MCTForm",
|
||||||
|
"controllers/DateTimeController"
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user