Skip to content

Instantly share code, notes, and snippets.

@potatogim
Forked from netzpirat/form_basic.coffee
Created September 14, 2019 17:55
Show Gist options
  • Select an option

  • Save potatogim/0694925705c7d33410dda2b8701431c7 to your computer and use it in GitHub Desktop.

Select an option

Save potatogim/0694925705c7d33410dda2b8701431c7 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment