var app = angular.module('app', ['setAttr', 'setRepeat']);

app.run(function($rootScope) {
  // simple component definitions
  var text1 = {
    id: 'text1',
    component: 'text',
    color: '#5bb75b',
    width: '60px',
    required: true
  },
    select1 = {
      id: 'select1',
      component: 'select',
      values: [{
        id: 'id1',
        text: 'option 1'
      }, {
        id: 'id2',
        text: 'option 2'
      }, {
        id: 'id3',
        text: 'option 3'
      }]
    };

  // form definition:
  // form has many tabs
  //  tab has many rows
  //   row has many cells
  //    cell has many components
  $rootScope.form = {
    tabs: [{
      rows: [{
        cells: [{
          size: 1,
          align: 'right',
          cmps: [{
            id: 'label1',
            component: 'label',
            text: 'Text'
          }]
        }, {
          size: 1,
          align: 'left',
          cmps: [text1]
        }]
      }, {
        cells: [{
          size: 2,
          align: 'left',
          cmps: [{
            id: 'integer1',
            component: 'integer',
            width: '40px'
          }]
        }]
      }, {
        cells: [{
          size: 2,
          align: 'left',
          cmps: [select1]
        }]
      }, {
        cells: [{
          size: 2,
          align: 'left',
          cmps: [{
            id: 'checkboxes1',
            component: 'checkboxes',
            required: true,
            values: [{
              id: 'id1',
              text: 'option 1'
            }, {
              id: 'id2',
              text: 'option 2'
            }, {
              id: 'id3',
              text: 'option 3'
            }]
          }, {
            id: 'radio1',
            component: 'radio',
            color: '#5bb75b',
            values: [{
              id: 'id1',
              text: 'option 1'
            }, {
              id: 'id2',
              text: 'option 2'
            }, {
              id: 'id3',
              text: 'option 3'
            }]
          }]
        }]
      }]
    }]
  };

  // list definition:
  // list has many cells
  //  cell has title and many components
  $rootScope.list = {
    cells: [{
      title: {
        id: 'listName',
        component: 'label',
        text: 'Name',
      },
      cmps: [text1]
    }, {
      title: {
        id: 'listOptions',
        component: 'label',
        text: 'Option',
      },
      cmps: [select1]
    }]
  };

  // array of models
  $rootScope.models = [{
    _id: 1,
    text1: 'John',
    integer1: 42,
    select1: 'id2',
    radio1: 'id3',
    checkboxes1: [{
      id: 'id2'
    }, {
      id: 'id3'
    }]
  }, {
    _id: 2,
    text1: 'Jimmy',
    integer1: 42,
    select1: 'id3',
    radio1: 'id2',
    checkboxes1: [{
      id: 'id1'
    }]
  }];

  // first model is selected
  // editable on form
  $rootScope.model = $rootScope.models[0];

  // list add button
  $rootScope.add = function() {
    var model = $rootScope.model = { _id:Math.random() };
    $rootScope.models.push(model);
  };

  // form state
  var state = $rootScope.state = {};

  // toggle between edit and detail mode
  // component can have edit and detail(readonly) modes
  $rootScope.toggleMode = function(scope) {
    if (state.mode == 'edit') {
      state.mode = 'detail';
      $rootScope.modeText = 'Edit mode';
    } else {
      state.mode = 'edit';
      $rootScope.modeText = 'Detail mode';
    }
  };
  // set to edit
  $rootScope.toggleMode();

  // helper for ngForm display
  $rootScope.JSONStringify = window.JSON.stringify;
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Dynamic form, list and components /w AngularJS</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"></script>
    <script src="helpers.js"></script>
    <script src="app.js"></script>
    <script src="components.js"></script>
  </head>
  <body ng-app="app">
    LIST:
    <table border="1">
      <tr>
        <td>Actions <input type="button" value="add" ng-click="add()"></td>
        <td ng-repeat="cell in list.cells">
          <span component="cell.title"></span>
        </td>
      </tr>
      <tr ng-repeat="model in models track by model._id" ng-init="state={mode:model==$root.model?'edit':'detail'}">
        <td>
          <input type="button" value="Edit" ng-click="$root.model=model" ng-disabled="$root.model==model">
        </td>
        <td set-repeat="cell in list.cells">
          <span set-repeat="c in cell.cmps">
            <span component="c" ng-model="model[c.id]"></span>
          </span>
        </td>
      </tr>
    </table>
    <br>
    FORM: <input type="button" value="{{modeText}}" ng-click="toggleMode()">
    <form name="ngForm" ng-init="selectedTab=form.tabs[0]">
      <table set-repeat="tab in form.tabs" ng-if="tab==selectedTab" border="1">
        <tr set-repeat="row in tab.rows">
          <td set-repeat="cell in row.cells" set-attr="{colspan:cell.size,align:cell.align}">
            <span ng-repeat="c in cell.cmps">
              <span component="c" ng-model="model[c.id]"></span>
            </span>
          </td>
        </tr>
      </table>
    </form>
    <hr>
    $scope.model=
    <pre>{{model | json}}</pre>
    <hr>
    $scope.ngForm=
    <pre>{{JSONStringify(ngForm, null, 4)}}</pre>
  </body>
</html>
// simple static label
app.directive('cmpLabel', function() {
  return {
    restrict: "A",
    template: '{{text}}'
  };
});

// text component with edit and detail mode
app.directive('cmpText', function() {
  return {
    restrict: "A",
    template: '<span ng-if="state.mode!=\'edit\'">{{ngModel}}</span><input ng-if="state.mode==\'edit\'" cmp-help-width ng-model="$parent.ngModel">'
  };
});

app.directive('cmpInteger', function() {
  return {
    restrict: "A",
    template: '<span ng-if="state.mode!=\'edit\'">{{ngModel}}</span><input type="number" ng-if="state.mode==\'edit\'" cmp-help-width ng-model="$parent.ngModel" ng-pattern="/^\\d+$/">',
  };
});

app.directive('cmpSelect', function() {
  return {
    restrict: "A",
    require: 'ngModel',
    template: '<select ng-if="state.mode==\'edit\'" ng-model="$parent.ngModel" ng-options="o.id as o.text for o in values"></select><span ng-if="state.mode!=\'edit\'">{{selected.text}}</span>',
    link: function(scope, element, attrs, ngModel) {
      ngModel.$render = function() {
        scope.selected = ngModel.$modelValue ? scope.values.filter(function(v) {
          return v.id === ngModel.$modelValue;
        })[0] : null;
      };
    }
  };
});

// no detail mode
app.directive('cmpRadio', function() {
  return {
    restrict: "A",
    template: '<label ng-repeat="v in values track by v.id"><input ng-model="$parent.ngModel" type="radio" value="{{v.id}}">{{v.text}}</label>'
  };
});

// no detail mode
app.directive('cmpCheckboxes', function() {
  return {
    restrict: "A",
    require: 'ngModel',
    template: '<label ng-repeat="v in values track by v.id"><input ng-model="checked[v.id]" type="checkbox" value="{{v.id}}">{{v.text}}</label>',
    link: function(scope, element, attrs, ngModel) {
      var isSelected = function(id) {
        var value, _i, _len;
        value = ngModel.$modelValue;
        if (value) {
          for (_i = 0, _len = value.length; _i < _len; _i++) {
            if (value[_i].id === id) {
              return true;
            }
          }
        }
        return false;
      };
      var checked = scope.checked = {};
      ngModel.$render = function() {
        var values = scope.values;
        for (var i = 0, len = values.length; i < len; i++) {
          var v = values[i];
          checked[v.id] = isSelected(v.id);
        }
        update();
      };
      ngModel.$isEmpty = function() {
        return !ngModel.$modelValue || !ngModel.$modelValue.length;
      };
      var selected = ngModel.$modelValue || [];

      function update() {
        selected.length = 0;
        scope.values.forEach(function(v) {
          if (checked[v.id]) {
            selected.push(v);
          }
        });
        scope.ngModel = selected.length === 0 ? null : selected;
      }
      scope.$watch('checked', update, true);
      scope.$watch('ngModel', update, true);
    }
  };
});

// helper to set input width
app.directive('cmpHelpWidth', function() {
  return {
    restrict: "A",
    link: function(scope, element) {
      if (scope.width) {
        element.css('width', scope.width);
      }
    }
  };
});

// abstract component directive responsible for dynamic component rendering
app.directive('component', ['$compile',
  function($compile) {
    return {
      restrict: 'A',
      require: 'component',
      scope: {
        ngModel: '='
      },
      controller: function($scope, $element, $attrs) {
        var component = $scope.$parent.$eval($attrs.component);
        // copy all properties from component to scope
        for (var k in component) {
          $scope[k] = component[k];
        }

        // set the name attribute so ngModel registers to form controller
        $attrs.$set('name', $scope.id);

        // common to all components
        this.init = function() {
          if ($scope.color) {
            $element.css('color', $scope.color);
          }
          this.render();
        };

        // this should be reusable re-render function
        // when(if) component definition changes
        // it should be idempotence
        this.render = function() {
          $compile($element)($scope);
        };
      },
      link: function(scope, element, attrs, ctrl) {
        // set state
        scope.state = scope.$parent.state;

        element
          // remove already compiled directives
          .removeAttr('component')
          .removeAttr('ng-model')
          // add dynamic component
          .attr('cmp-' + scope.component, '')
          .addClass('cmp cmp-' + scope.component);

        if (scope.required) {
          // 
          element.attr('required', true);
        }
        // $compile element with component attribute
        ctrl.init();
      }
    };
  }
]);

// https://gist.github.com/btm1/6802599
angular.module('setAttr',[]).directive('setAttr', function() {
  return {
    restrict: 'A',
    priority: 100,
    link: function(scope,elem,attrs) {
      if(attrs.setAttr.indexOf('{') != -1 && attrs.setAttr.indexOf('}') != -1) {
      //you could just angular.isObject(scope.$eval(attrs.setAttr)) for the above but I needed it this way
        var data = scope.$eval(attrs.setAttr);
 
        angular.forEach(data, function(v,k){
 
          if(angular.isObject(v)) {
            if(v.value && v.condition) {
                elem.attr(k,v.value);
                elem.removeAttr('set-attr');
            }
          } else {
            elem.attr(k,v);
            elem.removeAttr('set-attr');
          }
        });
      }
    }
  }
});

// https://gist.github.com/btm1/6746150
angular.module('setRepeat',[]).directive('setRepeat', function () {

  return {
    transclude: 'element',
    priority: 1000,
    compile: compileFun
  };

  function compileFun(element, attrs, linker) {
      var expression = attrs.setRepeat.split(' in ');
      expression = {
        child : expression[0],
        property : expression[1]
      };

      return {
        post: repeat
      };

      function repeat(scope, iele, iattrs /*, attr*/) {
        var template = element[0].outerHTML;
        var data = scope.$eval(expression.property);
        addElements(data,scope,iele);

        return;

        function makeNewScope (index, expression, value, scope, collection) {
          var childScope = scope.$new();
          childScope[expression] = value;
          childScope.$index = index;
          childScope.$first = (index === 0);
          childScope.$last = (index === (collection.length - 1));
          childScope.$middle = !(childScope.$first || childScope.$last);
          
          return childScope;
        }

        function addElements (collection, scope, insPoint) {
          var frag = document.createDocumentFragment();
          var newElements = [], element, idx, childScope;

          angular.forEach(data, function(v,i){
            childScope = makeNewScope(i,expression.child,v,scope,collection);
            element = linker(childScope, angular.noop);
            newElements.push(element);
            frag.appendChild(element[0]);
          });

          insPoint.after(frag);
          return newElements;
        }
      }
  }

});