1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2009 Alex Iskander and TPSi 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 /*globals Forms */ 8 9 sc_require("mixins/emptiness"); 10 sc_require("mixins/edit_mode"); 11 sc_require("views/form_row"); 12 13 /** 14 @class 15 FormView lays out rows, manages their label widths, binds their 16 content properties, and sets up their contentValueKeys as needed. This class is 17 experimental and not available out of the box. 18 19 Usually, you will place rows into the FormView: 20 21 childViews: "fullName gender".w(), 22 contentBinding: 'MyApp.personController', 23 24 fullName: SC.FormView.row("Name:", SC.TextFieldView.extend({ 25 layout: {height: 20, width: 150} 26 })), 27 28 gender: SC.FormView.row("Gender:", SC.RadioView.design({ 29 layout: {width: 150, height: 40, centerY: 0}, 30 items: ["male", "female"] 31 })) 32 33 The name of the row (ie. 'fullName'), is passed down to the fields, and used as the key 34 to bind the value property to the content. In this case it will bind content.fullName to the 35 value property of the textFieldView. 36 37 38 @extends SC.View 39 @implements SC.FlowedLayout, SC.CalculatesEmptiness, SC.FormsEditMode 40 */ 41 42 SC.FormView = SC.View.extend(SC.FlowedLayout, SC.CalculatesEmptiness, SC.FormsEditMode, /** @scope SC.FormView.prototype */ { 43 // We lay out forms vertically. Each item gets its own "row". Wrapping makes 44 // no sense, as the FormView should grow with each row. 45 layoutDirection: SC.LAYOUT_VERTICAL, 46 canWrap: NO, 47 48 renderDelegateName: 'formRenderDelegate', 49 50 /** 51 The default padding around items in the form. By default, this comes from the theme. 52 You can supply your own directly, or override the formRenderDelegate: 53 54 // base it on the existing render delegate 55 MyTheme.formRenderDelegate = SC.AceTheme.formRenderDelegate.create({ 56 flowSpacing: { left: 5, top: 5, right: 5, bottom: 5 } 57 }); 58 */ 59 defaultFlowSpacing: SC.propertyFromRenderDelegate('flowSpacing', {}), 60 61 classNames: ["sc-form-view"], 62 63 /** 64 Whether to automatically start editing. 65 */ 66 editsByDefault: YES, 67 68 /** 69 The content to bind the form to. This content object is passed to all children. 70 71 All child views, if added at design time via string-based childViews array, will get their 72 contentValueKey set to their own key. Note that SC.RowView passes on its contentValueKey to its 73 child field, and if its isNested property is YES, uses it to find its own content object. 74 */ 75 content: null, 76 77 /** 78 Rows in the form do not have to be full SC.FormRowView at design time. They can also be hashes 79 that get loaded into rows. 80 */ 81 exampleRow: SC.FormRowView.extend({ 82 labelView: SC.FormRowView.LabelView.extend({ textAlign: SC.ALIGN_RIGHT }) 83 }), 84 85 /** 86 @private 87 */ 88 init: function() { 89 if (this.get("editsByDefault")) this.set("isEditing", YES); 90 sc_super(); 91 }, 92 93 /** 94 */ 95 createChildViews: function() { 96 var childViews = this.get('childViews'), 97 len = childViews.length, 98 idx, key, views, view, 99 attrs, exampleRow = this.get('exampleRow'); 100 101 this.beginPropertyChanges() ; 102 103 // swap the array 104 for (idx=0; idx<len; ++idx) { 105 if (key = (view = childViews[idx])) { 106 107 // is this is a key name, lookup view class 108 if (typeof key === SC.T_STRING) { 109 view = this[key]; 110 } else { 111 key = null ; 112 } 113 114 if (!view) { 115 SC.Logger.error ("No view with name "+key+" has been found in "+this.toString()); 116 // skip this one. 117 continue; 118 } 119 120 if(!view.isClass && SC.typeOf(view) === SC.T_HASH) { 121 attrs = view; 122 view = exampleRow; 123 } 124 else { 125 attrs = {}; 126 } 127 128 if((attrs.isFormField || view.prototype.isFormField) && (attrs.hasContentValueSupport || view.prototype.hasContentValueSupport)) { 129 attrs.contentValueKey = key; 130 } 131 132 attrs.formKey = key; 133 134 // createChildView creates the view if necessary, but also sets 135 // important properties, such as parentView 136 view = this.createChildView(view, attrs) ; 137 138 if(!view.get('content')) { 139 view.bind('content', this, 'content'); 140 } 141 142 // for form rows, set up label measuring and the label itself. 143 if (view.isFormRow) { 144 // set label (if possible). 145 if (SC.none(view.get('label'))) { 146 view.set("label", key.humanize().titleize()); 147 } 148 149 // set the label size measuring stuff 150 if (this.get('labelWidth') !== null) { 151 view.set("shouldMeasureLabel", NO); 152 } 153 } 154 155 if (key) { this[key] = view ; } // save on key name if passed 156 } 157 childViews[idx] = view; 158 } 159 160 this.endPropertyChanges() ; 161 162 this._hasCreatedRows = YES; 163 this.recalculateLabelWidth(); 164 165 return this ; 166 }, 167 168 169 /** 170 Allows rows to use this to track label width. 171 */ 172 isRowDelegate: YES, 173 174 /** 175 Supply a label width to avoid automatically calculating the widths of the labels 176 in the form. Leave null to let SproutCore automatically determine the proper width 177 for the label. 178 179 @type Number 180 @default null 181 */ 182 labelWidth: null, 183 184 /** 185 Tells the child rows whether they should measure their labels or not. 186 */ 187 labelWidthDidChange: function() { 188 var childViews = this.get('childViews'), i, len = childViews.length, 189 shouldMeasure = SC.none(this.get('labelWidth')); 190 191 for(i = 0; i < len; i++) { 192 childViews[i].set('shouldMeasureLabel', shouldMeasure); 193 } 194 195 this.recalculateLabelWidth(); 196 }.observes('labelWidth'), 197 198 /** 199 Propagates the label width to the child rows, finding the measured size if necessary. 200 */ 201 recalculateLabelWidth: function() { 202 if (!this._hasCreatedRows) { 203 return; 204 } 205 206 var ret = this.get("labelWidth"), children = this.get("childViews"), idx, len = children.length, child; 207 208 // calculate by looping through child views and getting size (if possible and if 209 // no label width is explicitly set) 210 if (ret === null) { 211 ret = 0; 212 for (idx = 0; idx < len; idx++) { 213 child = children[idx]; 214 215 // if it has a measurable row label 216 if (child.get("rowLabelMeasuredSize")) { 217 ret = Math.max(child.get("rowLabelMeasuredSize"), ret); 218 } 219 } 220 } 221 222 // now set for all children 223 if (this._rowLabelSize !== ret) { 224 this._rowLabelSize = ret; 225 226 // set by looping through child views 227 for (idx = 0; idx < len; idx++) { 228 child = children[idx]; 229 230 // if it has a measurable row label 231 if (child.get("hasRowLabel")) { 232 child.set("rowLabelSize", ret); 233 } 234 } 235 236 } 237 }, 238 239 /** 240 Rows call this when their label width changes. 241 */ 242 rowLabelMeasuredSizeDidChange: function(row, labelSize) { 243 this.invokeOnce("recalculateLabelWidth"); 244 } 245 246 247 }); 248 249 SC.mixin(SC.FormView, { 250 /** 251 Creates a form row. 252 253 Can be called in two ways: `row(optionalClass, properties)`, which creates 254 a field with the properties, and puts it in a new row; 255 and `row(properties)`, which creates a new row—and it is up to you to add 256 any fields you want in the row. 257 258 You can also supply some properties to extend the row itself with. 259 */ 260 row: function(optionalClass, properties, rowExt) 261 { 262 return SC.FormRowView.row(optionalClass, properties, rowExt); 263 } 264 }); 265