Created
June 23, 2011 23:15
-
-
Save netzpirat/1043867 to your computer and use it in GitHub Desktop.
Model based form validation and row editor for Ext JS 4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# The form adds model based validation to the form | |
# and it updates the 'reset' and 'save' buttons from the owning | |
# form panel according to the dirty and validity status of the form. | |
# | |
# @author Michael Kessler | |
# | |
Ext.define 'Ext.ux.form.Basic', | |
extend: 'Ext.form.Basic' | |
config: | |
modelRecord: null | |
# Initialize the form | |
# | |
initialize: -> | |
@callParent() | |
for index, field of @getFields().items | |
field.on 'change', @validateFieldByModel, @, { buffer: 250 } | |
@on 'validitychange', (form, valid) => @updateButtonStatus() | |
@on 'dirtychange', (form, dirty) => @updateButtonStatus() | |
# Applies the model to the form | |
# | |
# @param model [Ext.data.Model] the model record | |
# | |
applyModelRecord: (model) -> | |
@loadRecord model | |
model | |
# No fields will be marked as invalid as a result of calling this. | |
# To trigger marking of fields use {#isValid} instead. | |
# | |
# @return [Boolean] the invalid field status | |
# | |
hasInvalidField: -> | |
@modelRecord.set @getFieldValues() | |
@modelRecord.validate().length != 0 | |
# Test if the form is valid. If you only want to determine overall form | |
# validity without marking anything, use {#hasInvalidField} instead. | |
# | |
# @return [Boolean] true if client-side validation of the model on the record is successful | |
# | |
isValid: -> | |
@modelRecord.set @getFieldValues() | |
@clearInvalid() | |
errors = @modelRecord.validate() | |
@markInvalid errors | |
errors.length == 0 | |
# This method validates a given field by the model declared in the form | |
# and mark the field as valid or invalid. | |
# | |
# @param field [Object] the field to validate. | |
# | |
validateFieldByModel: (field) -> | |
# Validate the whole model | |
@modelRecord.set field.getName(), field.getValue() | |
modelErrors = @modelRecord.validate() | |
fieldErrors = new Ext.data.Errors() | |
# Selectively get the fields error | |
modelErrors.each (item, index, length) -> | |
fieldErrors.add(item) if item.field == field.getName() | |
# Send validity change event | |
fieldIsValid = fieldErrors.length == 0 | |
if fieldIsValid != field.wasValid | |
field.wasValid = fieldIsValid | |
field.fireEvent 'validitychange', field, fieldIsValid | |
# Mark invalid field | |
field.clearInvalid() | |
@markInvalid fieldErrors | |
# Update the reset and save button according to the | |
# validation and dirty status. | |
# | |
updateButtonStatus: -> | |
reset = @owner.down 'button#reset' | |
save = @owner.down 'button#save' | |
if reset && save | |
if @isDirty() | |
reset.enable() | |
if @isValid() then save.enable() else save.disable() | |
else | |
reset.disable() | |
save.disable() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Better model based form panel which handles the | |
# model <-> form converting and handling the REST | |
# responses correctly to update dirty tracking status. | |
# | |
# @example Ext form panel | |
# Ext.define 'App.view.SamplePanel' | |
# extend: 'Ext.ux.form.Panel' | |
# model: 'User' | |
# | |
# @author Michael Kessler | |
# | |
Ext.define 'Ext.ux.form.Panel', | |
extend: 'Ext.form.Panel' | |
config: | |
modelRecord: null | |
# Construct a form panel | |
# | |
# @param config [Object] the component configuration | |
# | |
constructor: (config) -> | |
config = config || {} | |
config.trackResetOnLoad = true | |
@callParent([config]) | |
# Initialize the form panel | |
# | |
initComponent: -> | |
@modelRecord = Ext.ModelManager.create {}, @model | |
@bbar = [ | |
{ | |
xtype: 'component' | |
itemId: 'message' | |
height: 18 | |
padding: '0 0 0 20' | |
} | |
{ | |
xtype: 'tbfill' | |
} | |
{ | |
xtype: 'button' | |
itemId: 'reset' | |
iconCls: 'icons-16-refresh' | |
text: I18n.t("js.actions.reset.#{ @labels?.reset }", { defaultValue: I18n.t('js.actions.reset.default') }) | |
handler: => @reset() | |
disabled: true | |
} | |
{ | |
xtype: 'button' | |
itemId: 'save' | |
iconCls: 'icons-16-floppy_disk' | |
text: I18n.t("js.actions.save.#{ @labels?.save }", { defaultValue: I18n.t('js.actions.save.default') }) | |
type: 'submit' | |
handler: => @save() | |
disabled: true | |
} | |
] | |
@callParent() | |
@on 'afterrender', @loadKeyMap, @ | |
# Initialize the form panel items | |
# | |
initItems: -> | |
@fieldDefaults.validateOnChange = false | |
@fieldDefaults.validateOnBlur = false | |
for index, item of @initialConfig.items | |
delete item.vtype | |
@callParent() | |
# Create the model validated form | |
# | |
# @return [Ext.ux.form.Basic] the form | |
# | |
createForm: -> | |
Ext.create 'Ext.ux.form.Basic', @, Ext.applyIf({ listeners: {}, modelRecord: @modelRecord }, @initialConfig) | |
# Applies the model record to the underlying form | |
# | |
# @param model [Ext.data.Model] the model record | |
# | |
applyModelRecord: (model) -> | |
@getForm().setModelRecord model | |
# Save the associated model instance and update | |
# the form with the responded record. | |
# | |
save: -> | |
record = @getForm().getModelRecord() | |
if record.isValid() && record.dirty | |
record.proxy.on 'exception', (proxy, response, operation) => | |
@down('button#save').enable() | |
@clearMessage() | |
new RestResponse().adapt(response, @) | |
@down('button#save').disable() | |
@showProgress() | |
record.save | |
success: (record) => | |
@applyModelRecord record | |
@showSuccess I18n.t 'js.form.success' | |
# Reset the form and its dirty tracking state | |
# | |
reset: -> | |
@getForm().reset() | |
# Shows a form error message | |
# | |
# @param msg [String] the error message | |
# | |
showError: (msg) -> | |
@showMessage msg, false | |
# Shows a form success message | |
# | |
# @param msg [String] the success message | |
# | |
showSuccess: (msg) -> | |
@showMessage msg, true | |
# Show that form processing is on the way | |
# | |
showProgress: -> | |
message = @down 'component#message' | |
if message | |
message.removeCls 'e-form-error' | |
message.removeCls 'e-form-notice' | |
message.update "<p>#{ I18n.t 'js.form.progress' }</p>" | |
message.addCls 'e-form-progress' | |
message.show() | |
# Shows a form message | |
# | |
# @param msg [String] the message | |
# @param success [Boolean] whether the message is a success or not | |
# | |
showMessage: (msg, success) -> | |
message = @down 'component#message' | |
if message | |
message.removeCls 'e-form-progress' | |
message.removeCls 'e-form-notice' | |
message.removeCls 'e-form-error' | |
message.update "<p>#{ msg }</p>" | |
if success | |
message.addCls 'e-form-notice' | |
else | |
message.addCls 'e-form-error' | |
@up('window')?.sayNo() | |
message.show() | |
Ext.defer @clearMessage, 2000, @ | |
# Clears the notice and error message | |
# | |
clearMessage: -> | |
@down('component#message')?.hide() | |
# Initializes the key mappings for the form panel. | |
# | |
loadKeyMap: -> | |
@keyMap or= new Ext.util.KeyMap @getEl(), | |
[ | |
{ | |
key: [10, 13] | |
shift: false | |
ctrl: false | |
alt: false | |
fn: => @save() | |
} | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# The RestResponse is the opposite of the Rails RestResponder class and | |
# handles form related REST message exchange. | |
# | |
# @author Michael Kessler | |
# | |
Ext.define 'Ext.ux.data.RestResponse' | |
# Adapt the REST response to the form. This involves | |
# adding the message to the `message` component and | |
# append validation errors to the corresponding field. | |
# | |
# If the response is successful, hide the `information` | |
# component. | |
# | |
# @param response [XMLHttpRequest] the REST response | |
# @param form [Ext.form.Panel] the form panel | |
# @param hide [Boolean] Hide information also when not successful | |
# @return [Number] the HTTP code | |
# | |
adapt: (response, form, hide = false) -> | |
data = Ext.decode(response.responseText) | |
@addMessage(data, form) | |
@addValidationErrors(data, form) | |
if data['success'] || hide | |
form.down('#information')?.hide() | |
response.status | |
# Adds the REST response message to the form. The message itself must | |
# be present under the `message` key and the style of the response | |
# depends on the boolean value of the key `success`. | |
# | |
# In addition the window that encloses the given form panel says either | |
# yes or no. | |
# | |
# @param data [Object] the REST response data | |
# @param form [Ext.form.Panel] the form panel | |
# | |
addMessage: (data, form)-> | |
if data['message'] | |
message = form.getComponent 'message' | |
if message | |
message.update "<p>#{ data['message'] }</p>" | |
if data['success'] | |
message.addCls 'response-notice' | |
message.removeCls 'response-error' | |
form.up('window')?.sayYes() | |
else | |
message.addCls 'response-error' | |
message.removeCls 'response-notice' | |
form.up('window')?.sayNo() | |
message.show() | |
# Add validation errors from the REST response to the form. | |
# The response errors must be present at the `errors` key | |
# and must contain the name of the field and its associated | |
# message array. | |
# | |
# @example Errors response | |
# errors: { email: ["can't be blank"], username: ["is too long", "is already taken"] } | |
# | |
# @param data [Object] the REST response data | |
# @param form [Ext.form.Panel] the form panel | |
# | |
addValidationErrors: (data, form)-> | |
form.getForm().clearInvalid() | |
if data['errors'] | |
for fieldName, errors of data['errors'] | |
form.getForm().findField(fieldName)?.markInvalid(errors) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Row editing plugin that uses a model validating form panel. | |
# | |
# @author Michael Kessler | |
# | |
Ext.define 'Ext.ux.grid.plugin.RowEditing', | |
extend: 'Ext.grid.plugin.RowEditing' | |
# Initialize the row editor | |
# | |
# @return [Ext.ux.grid.RowEditor] the editor | |
# | |
initEditor: -> | |
Ext.create 'Ext.ux.grid.RowEditor', | |
autoCancel: @autoCancel | |
errorSummary: @errorSummary | |
fields: @grid.headerCt.getGridColumns() | |
hidden: true | |
editingPlugin: @ | |
renderTo: @view.el | |
model: @model | |
labels: @labels | |
# Start editing, but cancels any row editing before. | |
# | |
# @param record [Model] The Store data record which backs the row to be edited. | |
# @param columnHeader [Model] The Column object defining the column to be edited. @override | |
# | |
startEdit: (record, columnHeader) -> | |
@cancelEdit() if @editing | |
@callParent(arguments) | |
# Cancels the edit and removes phantom records. | |
# | |
cancelEdit: -> | |
@wasEditing = @editing | |
# Must be called before remove the record from the store! | |
@callParent(arguments) | |
if @wasEditing && @context?.record | |
if @context.record.phantom | |
@grid.getStore().remove(@context.record) | |
else | |
@context.record.reject() | |
@getEditor()?.cancelEdit() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Row editor that uses a model validating form panel. | |
# | |
# @author Michael Kessler | |
# | |
Ext.define 'Ext.ux.grid.RowEditor', | |
extend: 'Ext.grid.RowEditor' | |
config: | |
modelRecord: null | |
# Initialize the row editor | |
# | |
initComponent: -> | |
@modelRecord = Ext.ModelManager.create {}, @model | |
@callParent(arguments) | |
# Initialize the row editor fields | |
# | |
initItems: -> | |
@fieldDefaults.validateOnChange = false | |
@fieldDefaults.validateOnBlur = false | |
for index, field of @initialConfig.fields | |
delete field.vtype | |
@callParent(arguments) | |
# Create the model validated form | |
# | |
# @return [Ext.ux.form.Basic] the form | |
# | |
createForm: -> | |
Ext.create 'Ext.ux.form.Basic', @, Ext.applyIf({ listeners: {}, modelRecord: @modelRecord }, @initialConfig) | |
# Get all model validation errors | |
# | |
# @return [String] the error message | |
# | |
getErrors: -> | |
errors = [] | |
@getForm().getFields().each (item, index, length) -> | |
if item.activeErrors | |
for error in item.activeErrors | |
errors.push "<li>#{ Ext.String.capitalize(item.getName()) } #{ error }</li>" | |
"<ul>#{ errors.join('') }</ul>" | |
# Completes the model edit by saving directly to the backend | |
# and set the backend errors in the form if any. | |
# | |
completeEdit: -> | |
form = @getForm() | |
record = @context.record | |
form.updateRecord(record) if form.isDirty() | |
if record.isValid() && record.dirty | |
record.proxy.on 'exception', (proxy, response, operation) => | |
@clearMessage() | |
new Ext.ux.data.RestResponse().adapt(response, @) | |
@showProgress() | |
record.save | |
success: (r) => | |
record.commit() | |
@showSuccess I18n.t 'js.form.success' | |
@hide() | |
failure: => | |
@editingPlugin.editing = true | |
else | |
@hide() | |
# Shows a form error message | |
# | |
# @param msg [String] the error message | |
# | |
showError: (msg) -> | |
@showMessage msg, false | |
# Shows a form success message | |
# | |
# @param msg [String] the success message | |
# | |
showSuccess: (msg) -> | |
@showMessage msg, true | |
# Show that form processing is on the way | |
# | |
showProgress: -> | |
message = @editingPlugin.grid.up('component').down('#message') | |
if message | |
message.removeCls 'e-form-error' | |
message.removeCls 'e-form-notice' | |
message.addCls 'e-form-progress' | |
message.update "<p>#{ I18n.t 'js.form.progress' }</p>" | |
message.show() | |
# Shows a form message | |
# | |
# @param msg [String] the message | |
# @param success [Boolean] whether the message is a success or not | |
# | |
showMessage: (msg, success) -> | |
message = @editingPlugin.grid.up('component').down('#message') | |
if message | |
message.removeCls 'e-form-progress' | |
message.removeCls 'e-form-error' | |
message.removeCls 'e-form-notice' | |
message.update "<p>#{ msg }</p>" | |
if success | |
message.addCls 'e-form-notice' | |
else | |
message.addCls 'e-form-error' | |
@up('window')?.sayNo() | |
message.show() | |
Ext.defer @clearMessage, 2000, @ | |
# Clears the notice and error message | |
# | |
clearMessage: -> | |
@editingPlugin.grid.up('component').down('#message')?.hide() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Ext.Window override to add shaking effects to windows. | |
# | |
# @author Michael Kessler | |
# | |
Ext.override Ext.Window, | |
# Let the window say no | |
# | |
sayNo: -> | |
Ext.create 'Ext.fx.Animator' | |
target: @ | |
duration: 800 | |
keyframes: | |
0: | |
x: @x - 32 | |
20: | |
x: @x + 16 | |
40: | |
x: @x - 8 | |
60: | |
x: @x + 4 | |
80: | |
x: @x - 2 | |
100: | |
x: @x | |
# Let the window say yes | |
# | |
sayYes: -> | |
Ext.create 'Ext.fx.Animator' | |
target: @ | |
duration: 1200 | |
keyframes: | |
0: | |
y: @y - 32 | |
20: | |
y: @y + 16 | |
40: | |
y: @y - 8 | |
60: | |
y: @y + 4 | |
80: | |
y: @y - 2 | |
100: | |
y: @y |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This uses code uses the excellent i18n-js library.