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;
}
}
}
});