diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..68b9e27
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+bower_components/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..98682d9
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - "0.10"
+install: npm install && bower install
+script: grunt test
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..7363a2e
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,25 @@
+
+module.exports = function (grunt) {
+ 'use strict';
+ grunt.initConfig({
+ connect: {
+ server: {
+ options: {
+ port: 9001,
+ base: '.',
+ hostname: '0.0.0.0'
+ }
+ }
+ },
+ karma: {
+ unit: {
+ configFile: 'karma.conf.js',
+ singleRun: true
+ }
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-connect');
+ grunt.loadNpmTasks('grunt-karma');
+ grunt.registerTask('test', ['karma']);
+};
diff --git a/README.md b/README.md
index 528761d..9cb4ef9 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,114 @@ runtime. For example, this could be useful for building a generic database manip
includes a form for manipulating a row of data, with the database itself providing the schema
information at runtime.
-This is still a work in progress and is not feature-complete nor well tested. It's probably best
-not used in a production application without further work.
+Usage
+-----
+
+To make the directive available to your application, load the source file (or build it into your
+application's bundle) and declare a dependency on the module. For example:
+
+```js
+ var app = angular.module(['ng', 'ngRoute', 'dynamic-form']);
+```
+
+With this dependency declared you can use the directive in any of your templates:
+
+```html
+
+
+
+
+
+```
+
+The children of the `dynamic-table-form` element are field configurations, selected by the
+key given in the `dynamic-field-type` attribute. The `fields` attribute is an AngularJS
+expression evaluating to a field configuration as described below. The `data` attribute
+is an expression evaluating to an object whose properties will be edited by the form.
+
+A field configuration must be provided so that the directive knows which fields to show.
+This is just an array of field descriptions placed into the scope. Here's an example:
+
+```js
+
+$scope.fields = [
+ {
+ caption: 'Name',
+ model: 'name',
+ type: 'string',
+ maxLength: 25
+ },
+ {
+ caption: 'Age',
+ model: 'age',
+ type: 'number'
+ },
+ {
+ caption: 'State',
+ model: 'state',
+ type: 'choice',
+ options: [
+ {
+ caption: 'California',
+ value: 'CA'
+ },
+ {
+ caption: 'New York',
+ value: 'NY'
+ },
+ {
+ caption: 'Washington',
+ value: 'WA'
+ }
+ ]
+ }
+];
+````
+
+The core properties of a field description are `caption`, `model` and `type`:
+
+* `caption` is the literal string to show next to the field.
+* `model` is an Angular expression, relative to the form's data object, identifying where in the data structure this field's value belongs.
+* `type` is matched with the `dynamic-field-type` attributes in the directive to identify the appropriate UI to show for this field.
+
+Other type-specific properties may be added and accessed from the type's template element.
+
+The `dynamic-table-form` directive will then create one child element for each
+element in the field configuration, binding the appropriate field element to
+the field's value. The field elements are evaluated in a scope with the following
+members:
+
+* `value` is two-way bound to the result of the expression given in `model` in the field description.
+* `config` is the field description itself, from which type-specific properties may be retrieved.
+
+All field elements should interact with `value` in some way. The most common way is to reference
+it as the `ng-model` of a form element as shown in the HTML example earlier in this page.
+
+A more complete example is available in [example/](example/) in the repository, and you can also
+[see the example live](http://saymedia.github.io/angularjs-dynamic-form/example/).
+
+Collection Types
+----------------
+
+The original design for this library called for supporting special "collection types", which
+are fields representing arrays or objects. The intent was that fields could have specifiers
+like `array` which would cause the module to first look for a field type called
+`array` which could then transclude in a field for each item -- in this example the field
+element for `string`. This would allow a single field type to be created for editing arrays
+of any type, delegating to another field type for editing individual elements.
+
+This feature is not yet implemented, although the collection type syntax can be parsed
+and will be ignored. The practical implication of this for the moment is that it is not
+possible to have field types containing `<` and `>` symbols.
+
+The example linked above contains an ``array`` field element, but it is inoperable.
+Support for this may be added in a future version.
+
+In the mean time, arrays of specific types can be supported manually by the caller, by
+creating a type name like `arrayOfString` and then populating that type's field element
+with a UI for adding and removing strings to the array given in `value`.
+
+License
+-------
Copyright 2013 Say Media Ltd. All Rights Reserved. See the LICENSE file for distribution terms.
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..ec35536
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,14 @@
+{
+ "name": "angularjs-dynamic-form",
+ "version": "0.0.0",
+ "description": "AngularJS directive for creating dynamic forms based on schemas that aren't known until runtime",
+ "license": "MIT",
+ "main": "src/angulardynamicform.js",
+ "dependencies": {
+ "angular": "~1.2.7"
+ },
+ "devDependencies": {
+ "angular-mocks": "~1.2.7",
+ "jasmine": "~1.3.1"
+ }
+}
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..61334f2
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,55 @@
+// Karma configuration
+// http://karma-runner.github.io/0.10/config/configuration-file.html
+
+module.exports = function(config) {
+ config.set({
+ // base path, that will be used to resolve files and exclude
+ basePath: '',
+
+ // testing framework to use (jasmine/mocha/qunit/...)
+ frameworks: ['jasmine'],
+
+ plugins: [
+ 'karma-phantomjs-launcher',
+ 'karma-chrome-launcher',
+ 'karma-jasmine'
+ ],
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'bower_components/angular/angular.js',
+ 'bower_components/angular-mocks/angular-mocks.js',
+ 'src/*.js',
+ 'test/*.js'
+ ],
+
+ // list of files / patterns to exclude
+ exclude: [],
+
+ // web server port
+ port: 8080,
+
+ // level of logging
+ // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+
+ // Start these browsers, currently available:
+ // - Chrome
+ // - ChromeCanary
+ // - Firefox
+ // - Opera
+ // - Safari (only Mac)
+ // - PhantomJS
+ // - IE (only Windows)
+ browsers: ['PhantomJS'],
+
+ // Continuous Integration mode
+ // if true, it capture browsers, run tests and exit
+ singleRun: true
+ });
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4fcc0c0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "angularjs-dynamic-form",
+ "version": "0.0.0",
+ "devDependencies": {
+ "grunt-karma": "^0.8.2",
+ "grunt-cli": "^0.1.13",
+ "bower": "^1.3.1",
+ "karma": "^0.12.1",
+ "grunt-contrib-connect": "^0.7.1",
+ "karma-jasmine": "^0.1.5",
+ "karma-phantomjs-launcher": "^0.1.2"
+ }
+}
diff --git a/src/angulardynamicform.js b/src/angulardynamicform.js
index 4700012..06504f9 100644
--- a/src/angulardynamicform.js
+++ b/src/angulardynamicform.js
@@ -94,6 +94,14 @@
var fieldScope = $scope.$new(true);
fieldScope.config = field;
+ rowElem.addClass('control-group--' +
+ field.model.replace(/([a-z])([A-Z])/g,
+ function (m) {
+ return m[0] + '-' + m[1];
+ }
+ ).toLowerCase()
+ );
+
var modelGet = $parse(field.model);
var modelSet = modelGet.assign;
diff --git a/test/testBasic.js b/test/testBasic.js
new file mode 100644
index 0000000..9e4a1fe
--- /dev/null
+++ b/test/testBasic.js
@@ -0,0 +1,6 @@
+// let's have a really basic test to run
+describe('version', function () {
+ it('Should be equal to iteslf', function () {
+ expect('1').toEqual('1');
+ });
+});
\ No newline at end of file
diff --git a/test/testDynamicForm.js b/test/testDynamicForm.js
new file mode 100644
index 0000000..f3dda9d
--- /dev/null
+++ b/test/testDynamicForm.js
@@ -0,0 +1,151 @@
+describe('angularjs-dynamic-form-test', function () {
+
+ var element;
+ var $scope;
+ var compile;
+ var scopeFields = [
+ {
+ caption: 'Name',
+ model: 'name',
+ type: 'string',
+ maxLength: 25
+ },
+ {
+ caption: 'Street Address',
+ model: 'address.street',
+ type: 'string',
+ maxLength: 25
+ },
+ {
+ caption: 'State',
+ model: 'address.state',
+ type: 'select',
+ options: [
+ {
+ caption: 'California',
+ value: 'CA'
+ },
+ {
+ caption: 'New York',
+ value: 'NY'
+ },
+ {
+ caption: 'Washington',
+ value: 'WA'
+ }
+ ]
+ },
+ ];
+
+ var scopeData = {
+ name: 'John Smith',
+ nicknames: ['bill', 'dan', 'grumpy'],
+ address: {}
+ };
+ var html = '' +
+ '' +
+ '' +
+ '';
+
+
+
+ beforeEach(module('dynamic-form'));
+
+ beforeEach(inject(function ($compile, $rootScope) {
+ $scope = $rootScope.$new();
+ compile = $compile;
+ $scope.fields = [];
+ $scope.data= {};
+
+ element = angular.element(html);
+ }));
+
+ it('Should have added control groups for each element', function () {
+ $scope.fields = [
+ {
+ caption: 'Name',
+ model: 'name',
+ type: 'string',
+ },
+ {
+ caption: 'Street Address',
+ model: 'address.street',
+ type: 'string',
+ },
+ {
+ caption: 'State',
+ model: 'address.state',
+ type: 'select',
+ options: [
+ {
+ caption: 'California',
+ value: 'CA'
+ },
+ {
+ caption: 'New York',
+ value: 'NY'
+ },
+ {
+ caption: 'Washington',
+ value: 'WA'
+ }
+ ]
+ },
+ ];
+
+ compile(element)($scope);
+ $scope.$digest();
+
+ expect(element.children().length).toBe(scopeFields.length);
+ });
+
+ it('Should display a string field as an input field', function () {
+ $scope.fields = [
+ {caption: 'Name', model: 'name', type: 'string'}
+ ];
+
+ $scope.data = {
+ name: 'John Smith'
+ };
+
+ compile(element)($scope);
+ $scope.$digest();
+
+ expect(element.children().length).toBe(1);
+ expect(element.find('label').html()).toBe('Name');
+ expect(element.find('input').length).toBe(1);
+ expect(element.find('input').val()).toBe('John Smith');
+ });
+
+ it('Should display option fields as select with options', function () {
+ $scope.fields = [
+ {
+ caption: 'State',
+ model: 'address.state',
+ type: 'select',
+ options: [
+ {
+ caption: 'California',
+ value: 'CA'
+ },
+ {
+ caption: 'New York',
+ value: 'NY'
+ },
+ {
+ caption: 'Washington',
+ value: 'WA'
+ }
+ ]
+ }
+ ];
+ compile(element)($scope);
+ $scope.$digest();
+
+ expect(element.children().length).toBe(1);
+ expect(element.children().find('select').length).toBe(1);
+ // 1 extra default option
+ expect(element.children().find('option').length).toBe(4);
+ });
+});
\ No newline at end of file