AngularStrap and AngularUI dynamic select implementation(static/sync/async/http, filters) for Angular Schema Form
JavaScript HTML CSS
Clone or download
Pull request Compare This branch is 319 commits ahead of chengz:master.
Latest commit a59731b Nov 2, 2017

README.md

bower version npm version Join the chat at https://gitter.im/OptimalBPM/angular-schema-form-dynamic-select

Angular Schema Form Dynamic Select (ASFDS) add-on

This add-on integrates the angular-strap-select and the angular-ui-select components to provide fully featured drop downs to angular-schema-form.

It is drop-in compliant with angular-schema-forms existing selects, which makes using it a breeze, no adaptations are needed.

All settings are kept in the form, separating validation and UI-configuration.

Note about UI-select: The ui-select support is quite new and while many things work, it is still somewhat partial, so WRT the features below, they apply to angular-strap-select. For that reason, ui-select has a special section in the documentation.

Features:

  • Static and dynamic lists
  • Single and multiple select
  • Convenient HTTP GET/POST and property mapping functionality
  • Filters
  • Sync and Async callbacks
  • All callbacks referenced either by name (string) or reference
  • Angular schema form options
    • Supported:
      • key, type, title, description, placeholder
      • enum
      • notitle, onChange, condition
      • htmlClass, labelHtmlClass and fieldHtmlClass
      • validationMessage
    • Not supported(will be added):
      • readonly, copyValueTo,
    • Not applicable(will not be added due to the nature of drop downs, disagree?):
      • feedback

Example

There is a live example at http://demo.optimalbpm.se/angular-schema-form-dynamic-select/.

The example code is in the repository, it's made up of the index.html, app.js, test/testdata.json and test/testdata_mapped.json files.

To run it locally, simply clone the repository:

git clone https://github.com/OptimalBPM/angular-schema-form-dynamic-select.git
cd angular-schema-form-dynamic-select
bower update

..and open index.html in a browser or serve using your favorite IDE.

However, to make the entire example work properly, as it contains UI-select components, please install the ui-select dependencies as well.

(you will need to have bower installed, of course)

Help

What are my options if I feel I need help?

I don't understand the documentation

The prerequisite for understanding the ASFDS documentation below is that you have a basic understanding of how to use Angular Schema Form.
So if you understand that, and still cannot understand the documentation of ASFDS, it is probably not your fault. Please create an issue in those cases.

I have a question

If you have a question and cannot find an answer for it in the documentation below, please create an issue.
Questions and their answers have great value for the community.

I have found a bug

Please create an issue. Be sure to provide ample information, remember that any help won't be better than your explanation.

Unless something is obviously wrong, you are likely to be asked to provide a plunkr-example, displaying the erroneous behaviour.

While this might feel troublesome, a tip is to always make a plunkr that have the same external requirements as your project.
It is great for troubleshooting those annoying problems where you don't know if the problem is at your end or the components'.
And you can then easily fork and provide as an example.
You will answers and resolutions way quicker, also, many other open source projects require it.

I have a feature request

Good stuff! Please create an issue!
(features are more likely to be added the more users they seem to benefit)

I want to discuss ASFDS or reach out to the developers, or other ASFDS users

The gitter page is good for when you want to talk, but perhaps doesn't feel that the discussion has to be indexed for posterity.

Glossary

  • List items: the items that make up the selection list, for example the items in a drop down.
  • ASFDS: Angular-Schema-Form-Dynamic-Select

Installation and usage

ASFDS is an add-on to the angular-schema-form. To use it (in production), follow these steps:

Dependencies

Easiest way is to install is with bower, this will also include dependencies:

$ bower install angular-schema-form-dynamic-select

If you want to use the develop branch:

$ bower install angular-schema-form-dynamic-select#develop

#develop is not recommended for production, but perhaps you want to use stuff from the next version in development.

You can also use npm for installation:

$ npm i angular-schema-form-dynamic-select

HTML

Usage is straightforward, simply include and reference:

<link href="bower_components/bootstrap/dist/css/bootstrap.css" media="all" rel="stylesheet" />

<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.min.js"></script>
<script src='bower_components/angular-strap/dist/angular-strap.min.js'></script>
<script src='bower_components/angular-strap/dist/angular-strap.tpl.min.js'></script>
<script src="bower_components/tv4/tv4.js"></script>
<script src="bower_components/objectpath/lib/ObjectPath.js"></script>
<script src="bower_components/angular-schema-form/dist/schema-form.min.js"></script>
<script src="bower_components/angular-schema-form/dist/bootstrap-decorator.min.js"></script>
<script src="bower_components/angular-schema-form-dynamic-select/angular-schema-form-dynamic-select.js"></script>

Note: Make sure you load angular-schema-form-dynamic-select.js after loading angular schema form.

Configuring your angular module

When you create your module, be sure to make it depend on mgcrea.ngStrap as well:

    angular.module('yourModule', ['schemaForm', 'mgcrea.ngStrap']);

Note: Se the ui-select dependencies section for ui-select instructions

Form

ASFDS is configured using form settings. There are no ASFDS-specific settings in the schema.

This is to keep the schemas clean from UI-specific settings and kept usable anywhere in the solution and/or organization.

Form types

The add-on contributes the following new form types, strapselect, uiselect, uiselectmulti.

The strapselect implements angular-strap-selects and uiselect* implements angular-ui-select.

Built-in select-controls gets the bootstrap look but retain their functionality.

Form Definition

All settings reside in the form definition. See the app.js file for this example in use.

$scope.form = [

Single select from static list

The drop down items are defined by and array of value/name objects residing in the form

 {
   "key": 'select',
   "type": 'strapselect',
   "titleMap": [
      {"value": 'value1', "name": 'text1'},
      {"value": 'value2', "name": 'text2'},
      {"value": 'value3', "name": 'text3'}
    ]
 },

Multiple select from static list

Like the above, but allows multiple items to be selected.

 {
   "key": 'multiselect',
   "type": 'strapselect',
   "options": { 
    "multiple": "true"
   }
   "titleMap": [
        {"value": 'value1', "name": 'text1'},
        {"value": 'value2', "name": 'text2'},
        {"value": 'value3', "name": 'long very very long label3'}
   ]
 },

Single select from dynamically loaded list via synchronous callback function

Callback must return an array of value/name objects (see static list above). The "options" structure is passed to it as a parameter.

 {
   "key": "selectDynamic",
   "type": 'strapselect',
   "options": {
        "callback": $scope.callBackSD
   }
 },

For examples of how the different kinds of callbacks are implemented, please look at the relevant code in app.js,

Multiple select from dynamically loaded list via synchronous callback function

Like strapselectdynamic above, but allowed multiple items to be selected.

 {
   "key": "multiselectDynamic",
   "type": 'strapmultiselect',
   "options": {
       "multiple": "true"
       "callback": $scope.callBackMSD
   }
 },

Multiple select from asynchronous callback

The asyncCallback must return a http-style promise and the data the promise provides must be a JSON array of value/name objects.

 {
   "key": "multiselectDynamicAsync",
   "type": 'strapselect',
   "options": {
       "multiple": "true"
       "asyncCallback": "callBackMSDAsync"
       }
   }
 },

Note that in this example, the reference to the callback is a string, meaning a callback in the using controller scope. Also note, again, because this is a common misunderstanding, that asyncCallback should not return the array of items, but a http-promise, like the one $http.get()/$http.post() or jquery's deferred.promise. Returning the array would be a synchronous operation, see "callback" above.

Multiple select from dynamically loaded list via http get

Convenience function, makes a get request, no need for callback. Expects the server to return a JSON array of value/name objects.

 {
   "key": "multiselectDynamicHttpGet",
   "type": 'strapselect',
   "options": {
       "multiple": "true"
       "httpGet": {
           "url" : "test/testdata.json"
       }
   }
 },

Multiple select from dynamically loaded list via http post with an options callback

Like the get variant above function, but makes a JSON POST request passing the "parameter" as JSON.
This example makes use of the optionsCallback property. It is a callback that like the others, gets the options structure as a parameter, but allows its content to be modified and returned for use in the call. Here, the otherwise mandatory httpPost.url is not set in the options but in the callback.

See the stringOptionsCallback function in app.js for an example. The options-instance that is passed to the parameter is a copy of the instance in the form, so the form instance is not affected by any modifications by the callback.

 {
   "key": "multiselectDynamicHttpPost",
   "type": 'strapselect',
   "options": {
       "multiple": "true"
       "httpPost": {
           "optionsCallback" : "stringOptionsCallback",
           "parameter": { "myparam" : "Hello"}
       }
   }
 },

Property mapping

The angular-schema-form titleMap naming standard is value/name, but that is sometimes difficult to get from a server, it might not support it. Therefore, a "map"-property is provided.
The property in valueProperty says in what property to look for the value, and nameProperty the name. In this case:

{"nodeId" : 1, "nodeName": "Test", "nodeType": "99"}

which cannot be used, is converted into:

{"value" : 1, "name": "Test", "nodeId" : 1, nodeName: "Test", "nodeType": "99"}

which is the native format with the old options retained to not destroy auxiliary information. For example, a field like "nodeType" might be used for filtering(see Filters section, below). The options for that mapping look like this:

 {
   "key": "multiselectdynamic_http_get",
   "type": "strapselect",
   "options": {
        "multiple": "true"
        "httpGet": {
            "url": "test/testdata_mapped.json"
        },
        "map" : {"valueProperty": "nodeId", nameProperty: "nodeName"}
   }
 },    

The nameProperty can also be an array, in which case ASFDS looks for the first value. For example, in this case, one wants to first show the caption, and if that is not available, the name:

"map" : {"valueProperty": "nodeId", nameProperty: ["nodeCaption", "nodeName"]}

For more complicated mappings, and situations where the source data is in a completely different format, the callback and asyncCallback options can be used instead.

Filters

Filters, like conditions, handle visibility, but for each item in the options list.

It works by evaluating the filter expression for each row, if it evaluates to true, the option remains in the list. One could compare it with an SQL join.

The options are:

  • filter : An expression, evaluated in the user scope, with the "item" local variable injected. "item" is the current list item, "model.select==item.category"
  • filterTrigger : An array of expressions triggering the filtering, "model.select"

Example:

{
    "key": 'multiselect',
    "type": 'strapselect',
    options: {
        "multiple": "true"           
        "filterTriggers": ["model.select"],
        "filter" : "model.select==item.category"
    },
    "titleMap": [
        {"value": 'value1', "name": 'text1', "category": "value1"},
        {"value": 'value2', "name": 'text2', "category": "value1"},
        {"value": 'value3', "name": 'long very very long label3'}
    ]
},

Note on filterTrigger and why not having a watch on the entire expression:

  • The expression is actually a one-to-many join, and mixes two scopes in the evaluation. This might not always be handled the same by $eval.
  • Adding watches for the expression would mean having to add one watch for each list item, long lists would mean a huge overhead.
  • Also, there might be use cases where triggering should be triggered by other conditions. Or not be triggered for some other reason.

The ASFDS controller scope

One usable property that is set by ASFDS is the options.scope-attribute.

Its value is the scope of the controller, which provides far-reaching control over ASFDS behavior.

In the example, the multiselectDynamicAsync's onChange event is implemented so that another ASFDS controller is told to repopulate its select list items when the value is changed. This is valuable, for example, when there is too much data or for some other reason, filters are inappropriate.

Defaults and enum

If a there is a form item that only has type "string" defined, but has an enum value, then a single select will be shown for that value.

{
    "key": 'select'
},

The schema declaration(the enum values will be both value and name for the options):

select: {
    title: 'Single Select Static',
    type: 'string',
    enum: ["value1", "value2", "value3"],
    description: 'Only single item is allowed. Based on schema enum and form default.(change here and observe how the select list below is filtered)'
},

inlineMaxLength and inlineMaxLengthHtml angularStrap parameters.

These settings affects only strapselect and controls the number of items that are shown in the selected list of items. If that list is full, the number of list items + the test in inlineMaxLengthHtml is shown. If, for example, inlineMaxLength is set to 2 and the number of selected items is 4, the text shown will be:

4 items are too many items to show....

Example(the same as in the example file):

    "key": 'multiselect_overflow',
    "type": 'strapselect',
    "placeholder": "Please select some items.",
    "options": {
        "multiple": "true",
        "inlineMaxLength": "2",
        "inlineMaxLengthHtml": " items are too many items to show...."
    },
    "titleMap": [
        {"value": 'value1', "name": 'text1'},
        {"value": 'value2', "name": 'text2'},
        {"value": 'value3', "name": 'text3'},
        {"value": 'value4', "name": 'text4'},
    ]

Positioning the angularStrap select.

The placement option can be used to position a strapselect. Possible values for placement are top, bottom, left, right, auto or any combination (e.g. bottom-right). Further details can be found in the angularStrap select documentation.

Example (the same as in the example file):

    "key": "select_placement",
	"placeholder": "Please select from the right.",
	"options": {
		"placement": "right"
	}

And then a submit button.

Not needed, of course, but is commonly used.

 {
   type: "submit",
   style: "btn-info",
   title: "OK"
 }

And ending the form element array:

];

Populating the list items

The form.titleMap property in a form holds the list items(also in the dynamic variants). The name titleMap is the same as the built-in angular-schema-form select.

Dynamically fetching list items

These types are dynamic and fetches their data from different back ends.

Callbacks in general

Callbacks can be defined either by name("loadGroups") or absolute reference ($scope.loadGroups).

The name is actually is an expression evaluated in the user scope that must return a function reference. This means that it can be "getLoadFunctions('Groups')", as long as that returns a function reference.

But the main reason for supporting referring to functions both by name and reference is that forms are often stored in a database and passed from the server to the client in pure JSON format, and there, callback: $scope.loadGroups is not allowed.

Callback results

The results of all callbacks can be remapped using the "map" property described above. All callbacks(also optionsCallback) has two parameters:

  • the options of the form,
  • if it is a UI-selects, the entered search value (see the UI-select example).

The two kinds of callback mechanisms are:

callback and asyncCallback

  • list items are fetched by a user-specified callback. The user implements the calling mechanism.
  • the callback receive the form options as a parameter and returns an array of list items(see the static strapselect)
  • asyncCallback implementations returns the data through a HttpPromise. NOT an array if items.

TIP: in an asyncCallback, you need to intercept and change an async server response before passing it on to the add-on, use the transformResponse function.

httpGet and httpPost

  • list items are fetched using a built in async http mechanism, so that the user doesn't have to implement that.
  • the url property defines the URL to use.
  • the optional optionsCallback can be used to add to or change the options with information known in runtime.
  • httpPost-options has a "parameter"-property, that contains the JSON that will be POST:ed to the server.

Handling errors from asynchronous callbacks

For asyncCallback, httpGet and httpPost, there is an option, onPopulationError.

If set to a callback function, and in case of a http error, the callback is called. Its parameters are: the form of the field(where they key and options are), the data and the status.

See app.js for an example of its usage. Try and rename test/testdata.js and you'll see it being called.

Statically setting the list items

This is done by either using the JSON-schema enum-property, or by manually setting form.titleMap.

UI-Select

The support for angular-ui-select was added in the 0.9.0-version, and is currently partial, but getting there.

The currently supported UI-select specific/native options are:

  • Single: tagging, taggingTokens, taggingLabel, refreshDelay, searchDescription, uiClass
  • Multiple select: refreshDelay, uiClass, groupBy(only multiple)

Installation

UI-select is not installed by default in ASFDS, even though it is featured in the demo and example, here is how to make it work:

Dependencies

Its dependencies aren't included in the package.json, and will hence have to be installed manually, here is a script:

 $  bower install angular-ui-select angular-underscore underscore angular-ui-utils angular-translate angular-ui-select angular-ui-utils angular-sanitize

HTML

Include all relevant files:

<link href="bower_components/angular-ui-select/dist/select.css" rel="stylesheet" />

<script src="https://code.jquery.com/jquery-2.1.4.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="bower_components/underscore/underscore-min.js"></script>
<script src="bower_components/angular-underscore/angular-underscore.js"></script>
<script src="bower_components/angular-ui-utils/ui-utils.js"></script>
<script src='bower_components/angular-ui-select/dist/select.js'></script>

Angular module configuration

UI-select have several additional dependencies that need to be added to your module configuration:

angular.module('yourModule', ['schemaForm', 'mgcrea.ngStrap', 'mgcrea.ngStrap.modal', 'pascalprecht.translate', 'ui.select', 'ui.highlight','mgcrea.ngStrap.select']);

Forms

It is used as strapselect, but by including the form types uiselect and uiselectmultiple instead.

    {
        "key": 'uiselectmultiple',
        "type": 'uiselectmultiple',
        "titleMap": [
          { value: 'one', name: 'option one'},
          { value: 'two', name: 'option two'},
          { value: 'three', name: 'option three'}
        ]
    },

It supports dynamically fetching items from a backend using callbacks and http-methods, but works a little bit different from AngularStrap internally, so filters, for example, aren't implemented yet.

See the example app in the source for more details on how to use it.

Recommendations

  • Choose httpGet and httpPost over the callback and asyncCallback methods if your don't specifically need the full freedom of callback and asyncCallback. There is no reason clutter client code with http-request handling unless you have to.
  • Given the asynchronous nature of javascript development, try use asynchronous alternatives before synchronous that block the UI.
  • The way the plug-ins works, they register themselves as defaults for all matching types.
    As long this is the case, all relevant fields must specify the "type"-property.
    If not, they will get the wrong editor. Either way, it is recommended to define the type.

Building

Building and minifying is done using gulp

Installing gulp and requrements

To install gulp, you need npm to be installer, however, we want a local bower install:

sudo npm install bower
node_modules/bower/bin/bower install

And then install the rest of the depencies

sudo npm install

The instructions are for Linux, to install under windows, the same commands adjusted for windows should work

Running the build

In the project root folder, run:

$ gulp default

Contributing

Pull requests are always very welcome. Try to make one for each thing you add, don't do like this author(me) did.

Remember that the next version is in the develop branch, so if you want to add new features, do that there.
If you want to fix a bug, do that against the master branch and it will be merged into the develop branch later.

Testing

Unit testing is done using Karma and Jasmine. The main configuration file for running tests is karma.conf.js, and test/tests.js holds the test code. First, make sure the relevant development dependencies are installed:

$ npm update

To run the tests:

$ node_modules/karma/bin/karma start karma.conf.js

Breaking change history

Important: Over the early minor versions, there has been considerable changes:

  • 0.3.0: all dynamic-select-related settings moved to the form.
  • 0.3.3: value/name-pairs for drop down data is deprecated.
    The correct way, and how the HTML select element actually works, is value/text.(note: Reverted in 0.8.0)
    The the add-on still supports both variants, but value/name will be removed.
  • 0.4.0: use the options.map functionality instead.
  • 0.5.0: Breaking changes:
    • http_post and http_get are renamed to httpPost and httpGet.
    • async.callback is removed and asyncCallback is used instead.
  • 0.6.0: earlier deprecated support for value/name-pairs is now removed
  • 0.7.0: meant a forced update of dependencies and some rewriting, since:
    • 2.2.1 of angular-strap has breaking changes making it impossible to keep backwards compatibility.
    • 0.8.0 of angular-schema-form, which also has breaking changes had to be updated to stay compatible with angular-straps' dependencies.
  • 0.8.0: Harmonization with angular-schema-form to be a drop-in replacement
    • Breaking change: The items array is now renamed to titleMap, as in ASF.
    • Value/name-pairs for drop-down data is now reintroduced (value/text is still supported)
  • 0.9.0: Breaking changes: strapselectdynamic, strapmultiselect and strapmultiselect was merged into strapselect.

Note: no further API changes are planned.

History

  1. This component was originally created by chengz.

  2. stevehu then added functionality to his project to connect to his light framework.

  3. This inspired nicklasb to merge stevehu:s code and rewrite the plugin in order to:

  • harmonize it with the current lookup handling in angular-schema-form
  • generalize it for it to be able to connect to any backend.

The rest is extremely recent history(i.e. > 0.3.0).