UNPKG

comindware.ui

Version:

Comindware Core UI provides the basic components like editors, lists, dropdowns, popups that we so desperately need while creating Marionette-based single-page applications.

369 lines (326 loc) 11.9 kB
/** * Developer: Stepan Burguchev * Date: 11/19/2014 * Copyright: 2009-2016 Comindware® * All Rights Reserved * Published under the MIT license */ import FieldView from '../fields/FieldView'; import helpers from '../../utils/helpers'; const Form = Marionette.Object.extend({ /** * Constructor * * @param {Object} [options.schema] * @param {Backbone.Model} [options.model] */ initialize(options) { this.options = options = options || {}; this.__regionManager = new Marionette.RegionManager(); this.schema = _.result(options, 'schema'); this.model = options.model; this.fields = {}; _.each(this.schema, function(fieldSchema, key) { const FieldType = fieldSchema.field || options.field || FieldView; const field = new FieldType({ form: this, key, schema: fieldSchema, model: this.model }); this.listenTo(field.editor, 'all', this.__handleEditorEvent); this.fields[key] = field; }, this); const $target = this.options.$target; const usedFields = {}; //Render standalone editors $target.find('[data-editors]').each((i, el) => { const $editorRegion = $(el); const key = $editorRegion.attr('data-editors'); const regionName = `${key}Region`; if (usedFields[key]) { helpers.throwFormatError(`Duplicated field '${key}'.`); } this.__regionManager.addRegion(regionName, { el: $editorRegion }); this.__regionManager.get(regionName).show(this.fields[key].editor); usedFields[key] = true; }); //Render standalone fields $target.find('[data-fields]').each((i, el) => { const $fieldRegion = $(el); const key = $fieldRegion.attr('data-fields'); const regionName = `${key}Region`; if (usedFields[key]) { helpers.throwFormatError(`Duplicated field '${key}'.`); } this.__regionManager.addRegion(regionName, { el: $fieldRegion }); this.__regionManager.get(regionName).show(this.fields[key]); usedFields[key] = true; }); }, /** * Update the model with all latest values. * * @param {Object} [options] Options to pass to Model#set (e.g. { silent: true }) * * @return {Object} Validation errors */ commit(options) { // Validate options = options || {}; const errors = this.validate({ skipModelValidate: !options.validate }); if (errors) { return errors; } // Commit let modelError = null; const setOptions = Object.assign({ error(model, e) { modelError = e; } }, options); this.model.set(this.getValue(), setOptions); return modelError; }, /** * Returns the editor for a given field key * * @param {String} key * * @return {Editor} */ getEditor(key) { const field = this.fields[key]; if (!field) { throw new Error(`Field not found: ${key}`); } return field.editor; }, onDestroy() { this.__regionManager.destroy(); }, /** * Get all the field values as an object. * @param {String} [key] Specific field value to get */ getValue(key) { // Return only given key if specified if (key) { return this.fields[key].getValue(); } // Otherwise return entire form const values = {}; _.each(this.fields, field => { values[field.key] = field.getValue(); }); return values; }, /** * Update field values, referenced by key * * @param {Object|String} prop New values to set, or property to set * @param val Value to set */ setValue(prop, val) { let data = {}; if (typeof prop === 'string') { data[prop] = val; } else { data = prop; } Object.keys(this.schema).forEach(key => { if (data[key] !== undefined) { this.fields[key].setValue(data[key]); } }); }, __handleEditorEvent(event, editor, field) { switch (event) { case 'change': this.trigger('change', this, editor); this.trigger(`${editor.key}:change`, this, editor); break; case 'focus': if (!this.hasFocus) { this.hasFocus = true; this.trigger('focus', this); } break; case 'blur': if (this.hasFocus) { const focusedField = _.find(this.fields, f => f.editor.hasFocus); if (!focusedField) { this.hasFocus = false; this.trigger('blur', this); } } break; default: break; } }, setErrors(errors) { _.each(errors, (value, key) => { const field = this.fields[key]; if (field) { field.setError(value); } }); }, /** * Validate the data * @return {Object} Validation errors */ validate(options) { const fields = this.fields; const model = this.model; const errors = {}; options = options || {}; //Collect errors from schema validation _.each(fields, field => { const error = field.validate(options); if (error) { errors[field.key] = error; } }); //Get errors from default Backbone model validator if (!options.skipModelValidate && model && model.validate) { const modelErrors = model.validate(this.getValue()); if (modelErrors) { const isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors); //If errors are not in object form then just store on the error object if (!isDictionary) { errors._others = errors._others || []; errors._others.push(modelErrors); } //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' }) if (isDictionary) { _.each(modelErrors, (val, key) => { //Set error on field if there isn't one already if (fields[key] && !errors[key]) { fields[key].setError(val); errors[key] = val; } else { //Otherwise add to '_others' key errors._others = errors._others || []; errors._others.push({ [key]: val }); } }); } } } const result = _.isEmpty(errors) ? null : errors; this.trigger('form:validated', !result, result); return result; }, /** * Gives the first editor in the form focus */ focus() { if (this.hasFocus) { return; } const field = this.fields[0]; if (!field) { return; } field.editor.focus(); }, /** * Removes focus from the currently focused editor */ blur() { if (!this.hasFocus) { return; } const focusedField = _.find(this.fields, field => field.editor.hasFocus); if (focusedField) { focusedField.editor.blur(); } } }); const constants = { RENDER_STRATEGY_RENDER: 'render', RENDER_STRATEGY_SHOW: 'show', RENDER_STRATEGY_MANUAL: 'manual' }; /** * Marionette.Behavior constructor shall never be called manually. * The options described here should be passed as behavior options (look into Marionette documentation for details). * @name BackboneFormBehavior * @memberof module:core.form.behaviors * @class This behavior turns any Marionette.View into Backbone.Form. To do this Backbone.Form scans this.$el at the moment * defined by <code>options.renderStrategy</code> and puts field and editors defined in <code>options.schema</code> into * DOM-elements with corresponding Backbone.Form data-attributes. * It's important to note that Backbone.Form will scan the whole this.$el including nested regions that might lead to unexpected behavior. * Possible events:<ul> * <li><code>'form:render' (form)</code> - the form has rendered and available via <code>form</code> property of the view.</li> * </ul> * @constructor * @extends Marionette.Behavior * @param {Object} options Options object. * @param {Object|Function} options.schema Backbone.Form schema as it's listed in [docs](https://github.com/powmedia/backbone-forms). * @param {Object|Function} [options.model] Backbone.Model that the form binds it's editors to. <code>this.model</code> is used by default. * @param {String} [options.renderStrategy='show'] Defines a moment when the form is applied to the view. May be one of:<ul> * <li><code>'render'</code> - On view's 'render' event.</li> * <li><code>'show'</code> - On view's 'show' event.</li> * <li><code>'manual'</code> - Form render method (<code>renderForm()</code>) must be called manually.</li> * </ul> * @param {Backbone.Form.Field} [options.field] Backbone.Form.Field that will be used to render fields of the form. * The field <code>core.form.fields.Field</code> is used by default. * @param {Marionette.View} view A view the behavior is applied to. * */ export default Marionette.Behavior.extend(/** @lends module:core.form.behaviors.BackboneFormBehavior.prototype */{ initialize(options, view) { view.renderForm = this.__renderForm.bind(this); }, defaults: { renderStrategy: constants.RENDER_STRATEGY_SHOW, model() { return this.model; }, schema() { return this.schema; } }, onRender() { if (this.options.renderStrategy === constants.RENDER_STRATEGY_RENDER) { this.__renderForm(); } }, onShow() { if (this.options.renderStrategy === constants.RENDER_STRATEGY_SHOW) { this.__renderForm(); } }, onDestroy() { if (this.form) { this.form.destroy(); } }, __renderForm() { let model = this.options.model; if (_.isFunction(model)) { model = model.call(this.view); } let schema = this.options.schema; if (_.isFunction(schema)) { schema = schema.call(this.view); } const form = new Form({ model, schema, $target: this.$el, field: this.options.field }); this.view.form = this.form = form; if (this.view.initForm) { this.view.initForm(); } this.view.triggerMethod('form:render', form); } });