@@ -242,6 +242,60 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s
242242 }
243243 }
244244 }
245+ } ,
246+ condition : function ( args ) {
247+ // Do we have a condition? Then we slap on an ng-if on all children,
248+ // but be nice to existing ng-if.
249+ if ( args . form . condition ) {
250+ var evalExpr = 'evalExpr(' + args . path +
251+ '.contidion, { model: model, "arrayIndex": $index})' ;
252+ if ( args . form . key ) {
253+ var strKey = sfPathProvider . stringify ( args . form . key ) ;
254+ evalExpr = 'evalExpr(' + args . path + '.condition,{ model: model, "arrayIndex": $index, ' +
255+ '"modelValue": model' + ( strKey [ 0 ] === '[' ? '' : '.' ) + strKey + '})' ;
256+ }
257+
258+ var children = args . fieldFrag . children ;
259+ for ( var i = 0 ; i < children . length ; i ++ ) {
260+ var child = children [ i ] ;
261+ var ngIf = child . getAttribute ( 'ng-if' ) ;
262+ child . setAttribute (
263+ 'ng-if' ,
264+ ngIf ?
265+ '(' + ngIf +
266+ ') || (' + evalExpr + ')'
267+ : evalExpr
268+ ) ;
269+ }
270+ }
271+ } ,
272+ array : function ( args ) {
273+ var items = args . fieldFrag . querySelector ( '[schema-form-array-items]' ) ;
274+ if ( items ) {
275+ state = angular . copy ( args . state ) ;
276+ state . keyRedaction = state . keyRedaction || 0 ;
277+ state . keyRedaction += args . form . key . length + 1 ;
278+
279+ // Special case, an array with just one item in it that is not an object.
280+ // So then we just override the modelValue
281+ if ( args . form . schema && args . form . schema . items &&
282+ args . form . schema . items . type &&
283+ args . form . schema . items . type . indexOf ( 'object' ) === - 1 &&
284+ args . form . schema . items . type . indexOf ( 'array' ) === - 1 ) {
285+ var strKey = sfPathProvider . stringify ( args . form . key ) . replace ( / " / g, '"' ) + '[$index]' ;
286+ state . modelValue = 'modelArray[$index]' ;
287+ } else {
288+ state . modelName = 'item' ;
289+ }
290+
291+ // Flag to the builder that where in an array.
292+ // This is needed for compatabiliy if a "old" add-on is used that
293+ // hasn't been transitioned to the new builder.
294+ state . arrayCompatFlag = true ;
295+
296+ var childFrag = args . build ( args . form . items , args . path + '.items' , state ) ;
297+ items . appendChild ( childFrag ) ;
298+ }
245299 }
246300 } ;
247301 this . builders = builders ;
@@ -279,12 +333,20 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s
279333 if ( ! field . replace ) {
280334 // Backwards compatability build
281335 var n = document . createElement ( snakeCase ( decorator . __name , '-' ) ) ;
282- n . setAttribute ( 'form' , path + '[' + index + ']' ) ;
336+ if ( state . arrayCompatFlag ) {
337+ n . setAttribute ( 'form' , 'copyWithIndex($index)' ) ;
338+ } else {
339+ n . setAttribute ( 'form' , path + '[' + index + ']' ) ;
340+ }
341+
283342 ( checkForSlot ( f , slots ) || frag ) . appendChild ( n ) ;
284343
285344 } else {
286345 var tmpl ;
287346
347+ // Reset arrayCompatFlag, it's only valid for direct children of the array.
348+ state . arrayCompatFlag = false ;
349+
288350 // TODO: Create a couple fo testcases, small and large and
289351 // measure optmization. A good start is probably a cache of DOM nodes for a particular
290352 // template that can be cloned instead of using innerHTML
@@ -330,17 +392,17 @@ angular.module('schemaForm').provider('sfBuilder', ['sfPathProvider', function(s
330392 } ;
331393
332394 return {
333- /**
334- * Builds a form from a canonical form definition
335- */
336- build : function ( form , decorator , slots , lookup ) {
337- return build ( form , decorator , function ( url ) {
338- return $templateCache . get ( url ) ;
339- } , slots , undefined , undefined , lookup ) ;
340-
341- } ,
342- builder : builders ,
343- internalBuild : build
395+ /**
396+ * Builds a form from a canonical form definition
397+ */
398+ build : function ( form , decorator , slots , lookup ) {
399+ return build ( form , decorator , function ( url ) {
400+ return $templateCache . get ( url ) ;
401+ } , slots , undefined , undefined , lookup ) ;
402+
403+ } ,
404+ builder : builders ,
405+ internalBuild : build
344406 } ;
345407 } ] ;
346408
@@ -1494,6 +1556,7 @@ angular.module('schemaForm').factory('sfValidator', [function() {
14941556
14951557/**
14961558 * Directive that handles the model arrays
1559+ * DEPRECATED with the new builder use the sfNewArray instead.
14971560 */
14981561angular . module ( 'schemaForm' ) . directive ( 'sfArray' , [ 'sfSelect' , 'schemaForm' , 'sfValidator' , 'sfPath' ,
14991562 function ( sfSelect , schemaForm , sfValidator , sfPath ) {
@@ -2087,6 +2150,218 @@ angular.module('schemaForm').directive('sfMessage',
20872150 } ;
20882151} ] ) ;
20892152
2153+ /**
2154+ * Directive that handles the model arrays
2155+ */
2156+ angular . module ( 'schemaForm' ) . directive ( 'sfNewArray' , [ 'sfSelect' , 'sfPath' , 'schemaForm' ,
2157+ function ( sel , sfPath , schemaForm ) {
2158+ return {
2159+ scope : false ,
2160+ link : function ( scope , element , attrs ) {
2161+ scope . min = 0 ;
2162+
2163+ scope . modelArray = scope . $eval ( attrs . sfNewArray ) ;
2164+
2165+ // We need to have a ngModel to hook into validation. It doesn't really play well with
2166+ // arrays though so we both need to trigger validation and onChange.
2167+ // So we watch the value as well. But watching an array can be tricky. We wan't to know
2168+ // when it changes so we can validate,
2169+ var watchFn = function ( ) {
2170+ //scope.modelArray = modelArray;
2171+ scope . modelArray = scope . $eval ( attrs . sfNewArray ) ;
2172+ // validateField method is exported by schema-validate
2173+ if ( scope . validateField ) {
2174+ scope . validateField ( ) ;
2175+ }
2176+ } ;
2177+
2178+ var onChangeFn = function ( ) {
2179+ if ( scope . form && scope . form . onChange ) {
2180+ if ( angular . isFunction ( scope . form . onChange ) ) {
2181+ scope . form . onChange ( scope . modelArray , scope . form ) ;
2182+ } else {
2183+ scope . evalExpr ( scope . form . onChange , { 'modelValue' : scope . modelArray , form : scope . form } ) ;
2184+ }
2185+ }
2186+ } ;
2187+
2188+ // We need the form definition to make a decision on how we should listen.
2189+ var once = scope . $watch ( 'form' , function ( form ) {
2190+ if ( ! form ) {
2191+ return ;
2192+ }
2193+
2194+ // Always start with one empty form unless configured otherwise.
2195+ // Special case: don't do it if form has a titleMap
2196+ if ( ! form . titleMap && form . startEmpty !== true && ( ! scope . modelArray || scope . modelArray . length === 0 ) ) {
2197+ scope . appendToArray ( ) ;
2198+ }
2199+
2200+ // If we have "uniqueItems" set to true, we must deep watch for changes.
2201+ if ( scope . form && scope . form . schema && scope . form . schema . uniqueItems === true ) {
2202+ scope . $watch ( attrs . sfNewArray , watchFn , true ) ;
2203+
2204+ // We still need to trigger onChange though.
2205+ scope . $watch ( [ attrs . sfNewArray , attrs . sfNewArray + '.length' ] , onChangeFn ) ;
2206+
2207+ } else {
2208+ // Otherwise we like to check if the instance of the array has changed, or if something
2209+ // has been added/removed.
2210+ if ( scope . $watchGroup ) {
2211+ scope . $watchGroup ( [ attrs . sfNewArray , attrs . sfNewArray + '.length' ] , function ( ) {
2212+ watchFn ( ) ;
2213+ onChangeFn ( ) ;
2214+ } ) ;
2215+ } else {
2216+ // Angular 1.2 support
2217+ scope . $watch ( attrs . sfNewArray , function ( ) {
2218+ watchFn ( ) ;
2219+ onChangeFn ( ) ;
2220+ } ) ;
2221+ scope . $watch ( attrs . sfNewArray + '.length' , function ( ) {
2222+ watchFn ( ) ;
2223+ onChangeFn ( ) ;
2224+ } ) ;
2225+ }
2226+ }
2227+
2228+ // Title Map handling
2229+ // If form has a titleMap configured we'd like to enable looping over
2230+ // titleMap instead of modelArray, this is used for intance in
2231+ // checkboxes. So instead of variable number of things we like to create
2232+ // a array value from a subset of values in the titleMap.
2233+ // The problem here is that ng-model on a checkbox doesn't really map to
2234+ // a list of values. This is here to fix that.
2235+ if ( form . titleMap && form . titleMap . length > 0 ) {
2236+ scope . titleMapValues = [ ] ;
2237+
2238+ // We watch the model for changes and the titleMapValues to reflect
2239+ // the modelArray
2240+ var updateTitleMapValues = function ( arr ) {
2241+ scope . titleMapValues = [ ] ;
2242+ arr = arr || [ ] ;
2243+
2244+ form . titleMap . forEach ( function ( item ) {
2245+ scope . titleMapValues . push ( arr . indexOf ( item . value ) !== - 1 ) ;
2246+ } ) ;
2247+ } ;
2248+ //Catch default values
2249+ updateTitleMapValues ( scope . modelArray ) ;
2250+
2251+ // TODO: Refactor and see if we can get rid of this watch by piggy backing on the
2252+ // validation watch.
2253+ scope . $watchCollection ( 'modelArray' , updateTitleMapValues ) ;
2254+
2255+ //To get two way binding we also watch our titleMapValues
2256+ scope . $watchCollection ( 'titleMapValues' , function ( vals , old ) {
2257+ if ( vals && vals !== old ) {
2258+ var arr = scope . modelArray ;
2259+
2260+ // Apparently the fastest way to clear an array, readable too.
2261+ // http://jsperf.com/array-destroy/32
2262+ while ( arr . length > 0 ) {
2263+ arr . pop ( ) ;
2264+ }
2265+ form . titleMap . forEach ( function ( item , index ) {
2266+ if ( vals [ index ] ) {
2267+ arr . push ( item . value ) ;
2268+ }
2269+ } ) ;
2270+
2271+ // Time to validate the rebuilt array.
2272+ // validateField method is exported by schema-validate
2273+ if ( scope . validateField ) {
2274+ scope . validateField ( ) ;
2275+ }
2276+ }
2277+ } ) ;
2278+ }
2279+
2280+ once ( ) ;
2281+ } ) ;
2282+
2283+ scope . appendToArray = function ( ) {
2284+
2285+ var empty ;
2286+
2287+ // Same old add empty things to the array hack :(
2288+ if ( scope . form && scope . form . schema ) {
2289+ if ( scope . form . schema . items ) {
2290+ if ( scope . form . schema . items . type === 'object' ) {
2291+ empty = { } ;
2292+ } else if ( scope . form . schema . items . type === 'array' ) {
2293+ empty = [ ] ;
2294+ }
2295+ }
2296+ }
2297+
2298+ var model = scope . modelArray ;
2299+ if ( ! model ) {
2300+ // Create and set an array if needed.
2301+ var selection = sfPath . parse ( attrs . sfNewArray ) ;
2302+ model = [ ] ;
2303+ sel ( selection , scope , model ) ;
2304+ scope . modelArray = model ;
2305+ }
2306+ model . push ( empty ) ;
2307+
2308+ return model ;
2309+ } ;
2310+
2311+ scope . deleteFromArray = function ( index ) {
2312+ var model = scope . modelArray ;
2313+ if ( model ) {
2314+ model . splice ( index , 1 ) ;
2315+ }
2316+ return model ;
2317+ } ;
2318+
2319+ // For backwards compatability, i.e. when a bootstrap-decorator tag is used
2320+ // as child to the array.
2321+ var setIndex = function ( index ) {
2322+ return function ( form ) {
2323+ if ( form . key ) {
2324+ form . key [ form . key . indexOf ( '' ) ] = index ;
2325+ }
2326+ } ;
2327+ } ;
2328+ var formDefCache = { } ;
2329+ scope . copyWithIndex = function ( index ) {
2330+ var form = scope . form ;
2331+ if ( ! formDefCache [ index ] ) {
2332+
2333+ // To be more compatible with JSON Form we support an array of items
2334+ // in the form definition of "array" (the schema just a value).
2335+ // for the subforms code to work this means we wrap everything in a
2336+ // section. Unless there is just one.
2337+ var subForm = form . items [ 0 ] ;
2338+ if ( form . items . length > 1 ) {
2339+ subForm = {
2340+ type : 'section' ,
2341+ items : form . items . map ( function ( item ) {
2342+ item . ngModelOptions = form . ngModelOptions ;
2343+ if ( angular . isUndefined ( item . readonly ) ) {
2344+ item . readonly = form . readonly ;
2345+ }
2346+ return item ;
2347+ } )
2348+ } ;
2349+ }
2350+
2351+ if ( subForm ) {
2352+ var copy = angular . copy ( subForm ) ;
2353+ copy . arrayIndex = index ;
2354+ schemaForm . traverseForm ( copy , setIndex ( index ) ) ;
2355+ formDefCache [ index ] = copy ;
2356+ }
2357+ }
2358+ return formDefCache [ index ] ;
2359+ } ;
2360+
2361+ }
2362+ } ;
2363+ } ] ) ;
2364+
20902365/*
20912366FIXME: real documentation
20922367<form sf-form="form" sf-schema="schema" sf-decorator="foobar"></form>
0 commit comments