When writing the prototype, I did partially build the data classes that were required, so now let's fully flesh them out:
// packages/wizard/src/model/Questionnaire.js Ext.define('Wizard.model.Questionnaire', { extend: 'Ext.data.Model', fields: [ { name: 'title' }, { name: 'introduction' }, { name: 'conclusion' } ], proxy: { type: 'rest', url: 'http://localhost:3000/questionnaire' }, toJSON: function() { return this.getData(true); } });
Standard stuff here with one exception is that a toJSON
method that consumes applications can override in order to obtain JSON in a format they can use for further processing. The default implementation returns the data of the questionnaire object along with its association data. Alternatively, they can override the proxy configuration to save the questionnaire data to their own server.
Let's take a look at the model I used to represent a step in the questionnaire:
// packages/wizard/src/model/Step.js Ext.define('Wizard.model.Step', { extend: 'Ext.data.Model', fields: [ { name: 'title' }, { name: 'introduction' }, { name: 'questionnaireId', reference: { type: 'Wizard.model.Questionnaire', inverse: 'steps' } } ], isValid: function() { var valid = true; this.questions().each(function(q) { if(q.isValid() === false) { valid = false; } }); this.set('valid', valid); return valid; } });
There are a couple of things to note on the Step
model; firstly, the use of associations, which I realized would provide a really easy way to load the nested data for the whole questionnaire in a single action.
The association is created using the reference
option on the field with the type
option specifying the full class name of the parent model and the inverse is the name of the association store that will be created on this parent Questionnaire
. Ext JS 5 associations are a little confusing at first as they're always defined on the child, not the parent.
Secondly, the isValid
method enumerates the questions belonging to this step and sets the step's own valid field according to the validity of its questions.
Finally, here's the Question
model I built:
// packages/wizard/src/model/Question.js Ext.define('Wizard.model.Question', { extend: 'Ext.data.Model', fields: [ { name: 'name' }, { name: 'required', type: 'boolean' }, { name: 'questionText' }, { name: 'type' }, { name: 'answer' }, { name: 'stepId', reference: { type: 'Wizard.model.Step', inverse: 'questions' } } ], validators: { answer: 'presence' }, getValidation: function() { if(this.get('required')) { return this.callParent(); } else { return new Ext.data.Validation(); } } });
Again, I have child-side
of the Step -> Questions
association defined in the same way as Questionnaire -> Steps
. Using the validators
config, I specify that answer should always be present, but I stumbled on a catch here that I could never have known about when just sitting down with a pencil and paper.
I really wanted to be able to add validators at runtime so that I could check the required field of the Step
model and add the presence
check to the answer. This enables the end user to toggle whether a particular question is required or not.
Unfortunately, after some intimate time with the Ext JS source code, it turns out that validators can only be defined on model class instances when they're defined and not on each instance of that class. Hopefully, this will be allowed in a later version—which at time of writing this book was 5.0.1—but I managed to come up with workarounds that enable this functionality.
We need to override the Question
class's getValidation
method. In the event that required is true
, I call the getValidation
on the superclass to proceed with validation normally. However, if it's false
, I return a new Ext.data.Validation
instance, but don't actually run its validation in effect, providing the same result as if the validation had passed.
While this works, and it's simple, it's one of these things that should be revisited with each new Ext JS version to see whether there's a more elegant way of solving the issue. I recommend code like this should be commented to let others know exactly why the workaround is needed and which version it applies to.