New subscriber view
diff --git a/views/ngXosLib/bower.json b/views/ngXosLib/bower.json
index 08a02e3..d2c7744 100644
--- a/views/ngXosLib/bower.json
+++ b/views/ngXosLib/bower.json
@@ -21,7 +21,8 @@
     "angular-animate": "1.4.7",
     "lodash": "~4.11.1",
     "angular-chart.js": "~0.10.2",
-    "d3": "~3.5.17"
+    "d3": "~3.5.17",
+    "angular-recursion": "~1.0.5"
   },
   "devDependencies": {
     "angular-mocks": "1.4.7",
diff --git a/views/ngXosLib/generator-xos/app/templates/bower.json b/views/ngXosLib/generator-xos/app/templates/bower.json
index 9dafc2b..0abca60 100644
--- a/views/ngXosLib/generator-xos/app/templates/bower.json
+++ b/views/ngXosLib/generator-xos/app/templates/bower.json
@@ -27,6 +27,7 @@
     "lodash": "~4.11.1",
     "bootstrap-css": "3.3.6",
     "angular-chart.js": "~0.10.2",
-    "d3": "~3.5.17"
+    "d3": "~3.5.17",
+    "angular-recursion": "~1.0.5"
   }
 }
diff --git a/views/ngXosLib/karma.conf.js b/views/ngXosLib/karma.conf.js
index 18c6f6f..9d880a0 100644
--- a/views/ngXosLib/karma.conf.js
+++ b/views/ngXosLib/karma.conf.js
@@ -94,7 +94,7 @@
     // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
     browsers: [
       'PhantomJS',
-      'Chrome'
+      // 'Chrome'
     ],
 
 
diff --git a/views/ngXosLib/xosHelpers/spec/services/helpers/form.helpers.test.js b/views/ngXosLib/xosHelpers/spec/services/helpers/form.helpers.test.js
new file mode 100644
index 0000000..4c90cd6
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/services/helpers/form.helpers.test.js
@@ -0,0 +1,225 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 5/25/16.
+ */
+
+(function () {
+  'use strict';
+
+  describe('The xos.helper module', function(){
+
+    describe('The XosFormHelper service', () => {
+      let service;
+
+      let fields = [
+        'id',
+        'name',
+        'mail',
+        'active',
+        'created',
+        'custom'
+      ];
+
+      let modelField = {
+        id: {},
+        name: {},
+        mail: {},
+        active: {},
+        created: {},
+        custom: {}
+      };
+
+      let model = {
+        id: 1,
+        name: 'test',
+        mail: 'test@onlab.us',
+        active: true,
+        created: '2016-04-18T23:44:16.883181Z',
+        custom: 'MyCustomValue'
+      };
+
+      let customField = {
+        custom: {
+          label: 'Custom Label',
+          type: 'number',
+          validators: {}
+        }
+      };
+
+      let formObject = {
+        id: {
+          label: 'Id:',
+          type: 'number',
+          validators: {}
+        },
+        name: {
+          label: 'Name:',
+          type: 'text',
+          validators: {}
+        },
+        mail: {
+          label: 'Mail:',
+          type: 'email',
+          validators: {}
+        },
+        active: {
+          label: 'Active:',
+          type: 'boolean',
+          validators: {}
+        },
+        created: {
+          label: 'Created:',
+          type: 'date',
+          validators: {}
+        },
+        custom: {
+          label: 'Custom Label:',
+          type: 'number',
+          validators: {}
+        }
+      };
+
+      // load the application module
+      beforeEach(module('xos.helpers'));
+
+      // inject the cartService
+      beforeEach(inject(function (_XosFormHelpers_) {
+        // The injector unwraps the underscores (_) from around the parameter names when matching
+        service = _XosFormHelpers_;
+      }));
+
+      describe('the _isEmail method', () => {
+        it('should return true', () => {
+          expect(service._isEmail('test@onlab.us')).toEqual(true);
+        });
+        it('should return false', () => {
+          expect(service._isEmail('testonlab.us')).toEqual(false);
+          expect(service._isEmail('test@onlab')).toEqual(false);
+        });
+      });
+
+      describe('the _getFieldFormat method', () => {
+        it('should return text', () => {
+          expect(service._getFieldFormat('a random text')).toEqual('text');
+          expect(service._getFieldFormat(null)).toEqual('text');
+          expect(service._getFieldFormat('1')).toEqual('text');
+        });
+        it('should return mail', () => {
+          expect(service._getFieldFormat('test@onlab.us')).toEqual('email');
+        });
+        it('should return number', () => {
+          expect(service._getFieldFormat(1)).toEqual('number');
+        });
+        it('should return boolean', () => {
+          expect(service._getFieldFormat(false)).toEqual('boolean');
+          expect(service._getFieldFormat(true)).toEqual('boolean');
+        });
+
+        it('should return date', () => {
+          expect(service._getFieldFormat('2016-04-19T23:09:1092Z')).toEqual('text');
+          expect(service._getFieldFormat(new Date())).toEqual('date');
+          expect(service._getFieldFormat('2016-04-19T23:09:10.208092Z')).toEqual('date');
+        });
+
+        it('should return array', () => {
+          expect(service._getFieldFormat([])).toEqual('array');
+          expect(service._getFieldFormat(['a', 'b'])).toEqual('array');
+        });
+
+        it('should return object', () => {
+          expect(service._getFieldFormat({})).toEqual('object');
+          expect(service._getFieldFormat({foo: 'bar'})).toEqual('object');
+        });
+      });
+
+      it('should convert the fields array in an empty form object', () => {
+        expect(service.parseModelField(fields)).toEqual(modelField);
+      });
+
+      describe('when modelField are provided', () => {
+        it('should combine modelField and customField in a form object', () => {
+          expect(service.buildFormStructure(modelField, customField, model)).toEqual(formObject);
+        });
+      });
+
+      describe('when model field is an empty array', () => {
+        let empty_modelField = {
+          // 5: {}
+        };
+        let empty_customFields = {
+          id: {
+            label: 'Id',
+            type: 'number'
+          },
+          name: {
+            label: 'Name',
+            type: 'text'
+          },
+          mail: {
+            label: 'Mail',
+            type: 'email'
+          },
+          active: {
+            label: 'Active',
+            type: 'boolean'
+          },
+          created: {
+            label: 'Created',
+            type: 'date'
+          },
+          custom: {
+            label: 'Custom Label',
+            type: 'number'
+          }
+        };
+
+        let empty_formObject = {
+          id: {
+            label: 'Id:',
+            type: 'number',
+            validators: {}
+          },
+          name: {
+            label: 'Name:',
+            type: 'text',
+            validators: {}
+          },
+          mail: {
+            label: 'Mail:',
+            type: 'email',
+            validators: {}
+          },
+          active: {
+            label: 'Active:',
+            type: 'boolean',
+            validators: {}
+          },
+          created: {
+            label: 'Created:',
+            type: 'date',
+            validators: {}
+          },
+          custom: {
+            label: 'Custom Label:',
+            type: 'number',
+            validators: {}
+          }
+        };
+
+        let empty_model = {5: 'Nan'}
+
+        it('should create a form object', () => {
+          let res = service.buildFormStructure(empty_modelField, empty_customFields, empty_model)
+          expect(res.id).toEqual(empty_formObject.id);
+          expect(res.name).toEqual(empty_formObject.name);
+          expect(res.mail).toEqual(empty_formObject.mail);
+          expect(res.active).toEqual(empty_formObject.active);
+          expect(res.created).toEqual(empty_formObject.created);
+          expect(res.custom).toEqual(empty_formObject.custom);
+          expect(res).toEqual(empty_formObject);
+        });
+      });
+    });
+  });
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/spec/ui/field.test.js b/views/ngXosLib/xosHelpers/spec/ui/field.test.js
index 9e7a7f2..c308417 100644
--- a/views/ngXosLib/xosHelpers/spec/ui/field.test.js
+++ b/views/ngXosLib/xosHelpers/spec/ui/field.test.js
@@ -40,9 +40,7 @@
             type: 'number',
             validators: {}
           };
-          scope.ngModel = {
-            label: 1
-          };
+          scope.ngModel = 1;
           compileElement();
         }
         expect(errorFunctionWrapper).toThrow(new Error('[xosField] Please provide a field name'));
@@ -53,9 +51,7 @@
           // setup the parent scope
           scope = $rootScope.$new();
           scope.name = 'label';
-          scope.ngModel = {
-            label: 1
-          };
+          scope.ngModel = 1;
           compileElement();
         }
         expect(errorFunctionWrapper).toThrow(new Error('[xosField] Please provide a field definition'));
@@ -75,6 +71,121 @@
         }
         expect(errorFunctionWrapper).toThrow(new Error('[xosField] Please provide an ng-model'));
       }));
+
+      describe('when a text input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'text',
+            validators: {}
+          };
+          scope.ngModel = 'label';
+          compileElement();
+        });
+
+        it('should print a text field', () => {
+          expect($(element).find('[name="label"]')).toHaveAttr('type', 'text');
+        });
+      });
+
+      describe('when a number input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'number',
+            validators: {}
+          };
+          scope.ngModel = 10;
+          compileElement();
+        });
+
+        it('should print a number field', () => {
+          expect($(element).find('[name="label"]')).toHaveAttr('type', 'number');
+        });
+      });
+
+      describe('when a boolean input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'boolean',
+            validators: {}
+          };
+          scope.ngModel = true;
+          compileElement();
+        });
+
+        let setFalse, setTrue;
+
+        beforeEach(() => {
+          setFalse= $(element).find('.boolean-field > button:first-child');
+          setTrue = $(element).find('.boolean-field > button:last-child');
+        });
+
+        it('should print two buttons', () => {
+          expect($(element).find('.boolean-field > button').length).toEqual(2)
+        });
+
+        it('should change value to false', () => {
+          expect(isolatedScope.ngModel).toEqual(true);
+          setFalse.click()
+          expect(isolatedScope.ngModel).toEqual(false);
+        });
+
+        it('should change value to true', () => {
+          isolatedScope.ngModel = false;
+          scope.$apply();
+          expect(isolatedScope.ngModel).toEqual(false);
+          setTrue.click()
+          expect(isolatedScope.ngModel).toEqual(true);
+        });
+      });
+
+      describe('when an object input is passed', () => {
+        beforeEach(() => {
+          scope = rootScope.$new();
+          scope.name = 'label';
+          scope.field = {
+            label: 'Label',
+            type: 'object',
+            validators: {}
+          };
+          scope.ngModel = {
+            baz: true,
+            foo: 'bar',
+            foz: 1,
+          };
+          compileElement();
+        });
+
+        it('should print a panel to contain object property field', () => {
+          expect($(element).find('.panel.object-field')).toExist()
+        });
+
+        it('should print the right input type for each property', () => {
+          expect($(element).find('input').length).toBe(2);
+          expect($(element).find('.boolean-field > button').length).toEqual(2);
+        });
+
+        describe('and the model is empty', () => {
+          beforeEach(() => {
+            scope.ngModel = {
+            };
+            compileElement();
+          });
+
+          it('should not print the panel', () => {
+            console.log($(element).find('.panel.object-field'));
+            expect($(element).find('.panel.object-field')).not.toExist()
+          });
+        });
+      });
     });
   });
 })();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/spec/ui/form.test.js b/views/ngXosLib/xosHelpers/spec/ui/form.test.js
index 7673fc8..5f73f30 100644
--- a/views/ngXosLib/xosHelpers/spec/ui/form.test.js
+++ b/views/ngXosLib/xosHelpers/spec/ui/form.test.js
@@ -9,221 +9,6 @@
 
   describe('The xos.helper module', function(){
 
-    // TODO move in separate file
-    describe('The XosFormHelper service', () => {
-      let service;
-
-      let fields = [
-        'id',
-        'name',
-        'mail',
-        'active',
-        'created',
-        'custom'
-      ];
-
-      let modelField = {
-        id: {},
-        name: {},
-        mail: {},
-        active: {},
-        created: {},
-        custom: {}
-      };
-
-      let model = {
-        id: 1,
-        name: 'test',
-        mail: 'test@onlab.us',
-        active: true,
-        created: '2016-04-18T23:44:16.883181Z',
-        custom: 'MyCustomValue'
-      };
-
-      let customField = {
-        custom: {
-          label: 'Custom Label',
-          type: 'number',
-          validators: {}
-        }
-      };
-
-      let formObject = {
-        id: {
-          label: 'Id:',
-          type: 'number',
-          validators: {}
-        },
-        name: {
-          label: 'Name:',
-          type: 'string',
-          validators: {}
-        },
-        mail: {
-          label: 'Mail:',
-          type: 'email',
-          validators: {}
-        },
-        active: {
-          label: 'Active:',
-          type: 'boolean',
-          validators: {}
-        },
-        created: {
-          label: 'Created:',
-          type: 'date',
-          validators: {}
-        },
-        custom: {
-          label: 'Custom Label:',
-          type: 'number',
-          validators: {}
-        }
-      };
-
-      // load the application module
-      beforeEach(module('xos.helpers'));
-
-      // inject the cartService
-      beforeEach(inject(function (_XosFormHelpers_) {
-        // The injector unwraps the underscores (_) from around the parameter names when matching
-        service = _XosFormHelpers_;
-      }));
-
-      describe('the _isEmail method', () => {
-        it('should return true', () => {
-          expect(service._isEmail('test@onlab.us')).toEqual(true);
-        });
-        it('should return false', () => {
-          expect(service._isEmail('testonlab.us')).toEqual(false);
-          expect(service._isEmail('test@onlab')).toEqual(false);
-        });
-      });
-
-      describe('the _getFieldFormat method', () => {
-        it('should return string', () => {
-          expect(service._getFieldFormat('string')).toEqual('string');
-          expect(service._getFieldFormat(null)).toEqual('string');
-        });
-        it('should return mail', () => {
-          expect(service._getFieldFormat('test@onlab.us')).toEqual('email');
-        });
-        it('should return number', () => {
-          expect(service._getFieldFormat(1)).toEqual('number');
-          // this is skipped because not realistic and js Date sucks
-          // expect(service._getFieldFormat('1')).toEqual('number');
-        });
-        it('should return boolean', () => {
-          expect(service._getFieldFormat(false)).toEqual('boolean');
-          expect(service._getFieldFormat(true)).toEqual('boolean');
-        });
-
-        it('should return date', () => {
-          expect(service._getFieldFormat('2016-04-19T23:09:1092Z')).toEqual('string');
-          expect(service._getFieldFormat(new Date())).toEqual('date');
-          expect(service._getFieldFormat('2016-04-19T23:09:10.208092Z')).toEqual('date');
-        });
-
-        it('should return array', () => {
-          expect(service._getFieldFormat([])).toEqual('array');
-          expect(service._getFieldFormat(['a', 'b'])).toEqual('array');
-        });
-
-        it('should return object', () => {
-          expect(service._getFieldFormat({})).toEqual('object');
-          expect(service._getFieldFormat({foo: 'bar'})).toEqual('object');
-        });
-      });
-
-      it('should convert the fields array in an empty form object', () => {
-        expect(service.parseModelField(fields)).toEqual(modelField);
-      });
-
-      describe('when modelField are provided', () => {
-        it('should combine modelField and customField in a form object', () => {
-          expect(service.buildFormStructure(modelField, customField, model)).toEqual(formObject);
-        });
-      });
-
-      describe('when model field is an empty array', () => {
-        let empty_modelField = {
-          // 5: {}
-        };
-        let empty_customFields = {
-          id: {
-            label: 'Id',
-            type: 'number'
-          },
-          name: {
-            label: 'Name',
-            type: 'string'
-          },
-          mail: {
-            label: 'Mail',
-            type: 'email'
-          },
-          active: {
-            label: 'Active',
-            type: 'boolean'
-          },
-          created: {
-            label: 'Created',
-            type: 'date'
-          },
-          custom: {
-            label: 'Custom Label',
-            type: 'number'
-          }
-        };
-
-        let empty_formObject = {
-          id: {
-            label: 'Id:',
-            type: 'number',
-            validators: {}
-          },
-          name: {
-            label: 'Name:',
-            type: 'string',
-            validators: {}
-          },
-          mail: {
-            label: 'Mail:',
-            type: 'email',
-            validators: {}
-          },
-          active: {
-            label: 'Active:',
-            type: 'boolean',
-            validators: {}
-          },
-          created: {
-            label: 'Created:',
-            type: 'date',
-            validators: {}
-          },
-          custom: {
-            label: 'Custom Label:',
-            type: 'number',
-            validators: {}
-          }
-        };
-
-        let empty_model = {5: 'Nan'}
-
-        it('should create a form object', () => {
-          let res = service.buildFormStructure(empty_modelField, empty_customFields, empty_model)
-          expect(res.id).toEqual(empty_formObject.id);
-          expect(res.name).toEqual(empty_formObject.name);
-          expect(res.mail).toEqual(empty_formObject.mail);
-          expect(res.active).toEqual(empty_formObject.active);
-          expect(res.created).toEqual(empty_formObject.created);
-          expect(res.custom).toEqual(empty_formObject.custom);
-          expect(res).toEqual(empty_formObject);
-        });
-      });
-    });
-
     describe('The xos-form component', () => {
 
       let element, scope, isolatedScope;
@@ -304,7 +89,7 @@
           expect(isolatedScope.excludedField).toEqual(expected);
         });
 
-        xit('should render 8 input field', () => {
+        it('should render 10 input field', () => {
           // boolean are in the form model, but are not input
           expect(Object.keys(isolatedScope.formField).length).toEqual(9);
           var field = element[0].getElementsByTagName('input');
@@ -312,7 +97,6 @@
         });
 
         it('should render 1 boolean field', () => {
-          // console.log($(element).find('.boolean-field'));
           expect($(element).find('.boolean-field > button').length).toEqual(2)
         });
 
@@ -410,10 +194,6 @@
             expect(isolatedScope.testForm.age.$error.min).toBeTruthy();
           });
         });
-
-        describe('the object field', () => {
-          
-        });
       });
     });
   });
diff --git a/views/ngXosLib/xosHelpers/src/services/helpers/ui/form.helpers.js b/views/ngXosLib/xosHelpers/src/services/helpers/ui/form.helpers.js
index ce52272..c850f48 100644
--- a/views/ngXosLib/xosHelpers/src/services/helpers/ui/form.helpers.js
+++ b/views/ngXosLib/xosHelpers/src/services/helpers/ui/form.helpers.js
@@ -53,19 +53,14 @@
         return 'boolean';
       }
 
-      // check if a string is a number
-      if(!isNaN(value) && value !== null){
-        return 'number';
-      }
-
       // check if a string is an email
       if(this._isEmail(value)){
         return 'email';
       }
 
       // if null return string
-      if(value === null){
-        return 'string';
+      if(typeof value === 'string' || value === null){
+        return 'text';
       }
 
       return typeof value;
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/field/field.component.js b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/field/field.component.js
index 923def6..c2aa62d 100644
--- a/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/field/field.component.js
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/field/field.component.js
@@ -27,7 +27,7 @@
     * ```
     * @param {mixed} ngModel The field value
     */
-  .directive('xosField', function(){
+  .directive('xosField', function(RecursionHelper){
     return {
       restrict: 'E',
       scope: {
@@ -36,7 +36,7 @@
         ngModel: '='
       },
       template: `
-        <label>{{vm.field.label}}</label>
+        <label ng-if="vm.field.type !== 'object'">{{vm.field.label}}</label>
             <input
               ng-if="vm.field.type !== 'boolean' && vm.field.type !== 'object'"
               type="{{vm.field.type}}"
@@ -62,27 +62,41 @@
             </span>
             <div
               class="panel panel-default object-field"
-              ng-if="vm.field.type == 'object' "
+              ng-if="vm.field.type == 'object' && !vm.isEmptyObject(vm.ngModel)"
               >
-              <div class="panel-heading">Panel heading without title</div>
+              <div class="panel-heading">{{vm.field.label}}</div>
               <div class="panel-body">
-                Panel content
+                <div ng-repeat="(k, v) in vm.ngModel">
+                  <xos-field
+                    name="k"
+                    field="{label: k, type: vm.getType(v)}"
+                    ng-model="v">
+                  </xos-field>
+                </div>
               </div>
             </div>
       `,
       bindToController: true,
       controllerAs: 'vm',
-      controller: function(){
-        // console.log('Field: ', this.name, this.field.type, this.ngModel);
+      // the compile cicle is needed to support recursion
+      compile: function (element) {
+        return RecursionHelper.compile(element);
+      },
+      controller: function(XosFormHelpers){
+        // console.log('Field: ', this.name, this.field, this.ngModel);
         if(!this.name){
           throw new Error('[xosField] Please provide a field name');
         }
         if(!this.field){
           throw new Error('[xosField] Please provide a field definition');
         }
-        if(!this.ngModel){
+        if(!angular.isDefined(this.ngModel)){
           throw new Error('[xosField] Please provide an ng-model');
         }
+
+        this.getType = XosFormHelpers._getFieldFormat;
+
+        this.isEmptyObject = o => Object.keys(o).length === 0;
       }
     }
   });
diff --git a/views/ngXosLib/xosHelpers/src/xosHelpers.module.js b/views/ngXosLib/xosHelpers/src/xosHelpers.module.js
index 4af3680..4ffc99d 100644
--- a/views/ngXosLib/xosHelpers/src/xosHelpers.module.js
+++ b/views/ngXosLib/xosHelpers/src/xosHelpers.module.js
@@ -25,6 +25,7 @@
         'ngAnimate',
         'bugSnag',
         'xos.uiComponents',
+        'RecursionHelper'
       ])
       .config(config)