forked from json-schema-form/angular-schema-form
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathschema-form.js
203 lines (173 loc) · 7.43 KB
/
schema-form.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/*
FIXME: real documentation
<form sf-form="form" sf-schema="schema" sf-decorator="foobar"></form>
*/
angular.module('schemaForm')
.directive('sfSchema',
['$compile', '$http', '$templateCache', '$q','schemaForm', 'schemaFormDecorators', 'sfSelect', 'sfPath', 'sfBuilder',
function($compile, $http, $templateCache, $q, schemaForm, schemaFormDecorators, sfSelect, sfPath, sfBuilder) {
return {
scope: {
schema: '=sfSchema',
initialForm: '=sfForm',
model: '=sfModel',
options: '=sfOptions'
},
controller: ['$scope', function($scope) {
this.evalInParentScope = function(expr, locals) {
return $scope.$parent.$eval(expr, locals);
};
// Set up form lookup map
var that = this;
$scope.lookup = function(lookup) {
if (lookup) {
that.lookup = lookup;
}
return that.lookup;
};
}],
replace: false,
restrict: 'A',
transclude: true,
require: '?form',
link: function(scope, element, attrs, formCtrl, transclude) {
//expose form controller on scope so that we don't force authors to use name on form
scope.formCtrl = formCtrl;
//We'd like to handle existing markup,
//besides using it in our template we also
//check for ng-model and add that to an ignore list
//i.e. even if form has a definition for it or form is ["*"]
//we don't generate it.
var ignore = {};
transclude(scope, function(clone) {
clone.addClass('schema-form-ignore');
element.prepend(clone);
if (element[0].querySelectorAll) {
var models = element[0].querySelectorAll('[ng-model]');
if (models) {
for (var i = 0; i < models.length; i++) {
var key = models[i].getAttribute('ng-model');
//skip first part before .
ignore[key.substring(key.indexOf('.') + 1)] = true;
}
}
}
});
var lastDigest = {};
var childScope;
// Common renderer function, can either be triggered by a watch or by an event.
var render = function(schema, form) {
var asyncTemplates = [];
var merged = schemaForm.merge(schema, form, ignore, scope.options, undefined, asyncTemplates);
if (asyncTemplates.length > 0) {
// Pre load all async templates and put them on the form for the builder to use.
$q.all(asyncTemplates.map(function(form) {
return $http.get(form.templateUrl, {cache: $templateCache}).then(function(res) {
form.template = res.data;
});
})).then(function() {
internalRender(schema, form, merged);
});
} else {
internalRender(schema, form, merged);
}
};
var internalRender = function(schema, form, merged) {
// Create a new form and destroy the old one.
// Not doing keeps old form elements hanging around after
// they have been removed from the DOM
// https://github.com/Textalk/angular-schema-form/issues/200
if (childScope) {
// Destroy strategy should not be acted upon
scope.externalDestructionInProgress = true;
childScope.$destroy();
scope.externalDestructionInProgress = false;
}
childScope = scope.$new();
//make the form available to decorators
childScope.schemaForm = {form: merged, schema: schema};
//clean all but pre existing html.
element.children(':not(.schema-form-ignore)').remove();
// Find all slots.
var slots = {};
var slotsFound = element[0].querySelectorAll('*[sf-insert-field]');
for (var i = 0; i < slotsFound.length; i++) {
slots[slotsFound[i].getAttribute('sf-insert-field')] = slotsFound[i];
}
// if sfUseDecorator is undefined the default decorator is used.
var decorator = schemaFormDecorators.decorator(attrs.sfUseDecorator);
// Use the builder to build it and append the result
var lookup = Object.create(null);
scope.lookup(lookup); // give the new lookup to the controller.
element[0].appendChild(sfBuilder.build(merged, decorator, slots, lookup));
// We need to know if we're in the first digest looping
// I.e. just rendered the form so we know not to validate
// empty fields.
childScope.firstDigest = true;
// We use a ordinary timeout since we don't need a digest after this.
setTimeout(function() {
childScope.firstDigest = false;
}, 0);
//compile only children
$compile(element.children())(childScope);
//ok, now that that is done let's set any defaults
if (!scope.options || scope.options.setSchemaDefaults !== false) {
schemaForm.traverseSchema(schema, function(prop, path) {
if (angular.isDefined(prop['default'])) {
var val = sfSelect(path, scope.model);
if (angular.isUndefined(val)) {
sfSelect(path, scope.model, prop['default']);
}
}
});
}
scope.$emit('sf-render-finished', element);
};
var defaultForm = ['*'];
//Since we are dependant on up to three
//attributes we'll do a common watch
scope.$watch(function() {
var schema = scope.schema;
var form = scope.initialForm || defaultForm;
//The check for schema.type is to ensure that schema is not {}
if (form && schema && schema.type &&
(lastDigest.form !== form || lastDigest.schema !== schema) &&
Object.keys(schema.properties).length > 0) {
lastDigest.schema = schema;
lastDigest.form = form;
render(schema, form);
}
});
// We also listen to the event schemaFormRedraw so you can manually trigger a change if
// part of the form or schema is chnaged without it being a new instance.
scope.$on('schemaFormRedraw', function() {
var schema = scope.schema;
var form = scope.initialForm ? angular.copy(scope.initialForm) : ['*'];
if (schema) {
render(schema, form);
}
});
scope.$on('$destroy', function() {
// Each field listens to the $destroy event so that it can remove any value
// from the model if that field is removed from the form. This is the default
// destroy strategy. But if the entire form (or at least the part we're on)
// gets removed, like when routing away to another page, then we definetly want to
// keep the model intact. So therefore we set a flag to tell the others it's time to just
// let it be.
scope.externalDestructionInProgress = true;
});
/**
* Evaluate an expression, i.e. scope.$eval
* but do it in parent scope
*
* @param {String} expression
* @param {Object} locals (optional)
* @return {Any} the result of the expression
*/
scope.evalExpr = function(expression, locals) {
return scope.$parent.$eval(expression, locals);
};
}
};
}
]);