Skip to content

Instantly share code, notes, and snippets.

@netzpirat
Created June 23, 2011 23:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save netzpirat/1043867 to your computer and use it in GitHub Desktop.
Save netzpirat/1043867 to your computer and use it in GitHub Desktop.
Model based form validation and row editor for Ext JS 4
# 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()
# 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()
}
]
# 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)
# 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()
# 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()
# 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
@netzpirat
Copy link
Author

This uses code uses the excellent i18n-js library.

@netzpirat
Copy link
Author

You should also have a look at the Sencha Rails Responder for which this code is written for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment