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 index 8e4092c..ec35536 100644 --- a/bower.json +++ b/bower.json @@ -2,5 +2,13 @@ "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" + "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