The Form Generator DSL is used to create dynamic forms through a simple JSON-based text interface. The form generation script consists of a root object that contains an list of pages, a list of functions and a vars object. Each page represent a page in the form, which contains zero or more questions. The functions list contains of a list of functions available for generating dynamic question content. Finally the vars object is an object of variables accessible to all of the questions.
Form developers also have access to an API library of available functions which can be used to generate dynamic content for the questions.
All of the form responses generated by the quiz takers are stored in the Form State. The Form State can be accessed by the form developers to make use of quiz taker responses in other parts of the form and is used by the autograder to check for correct answers.
{
pages: [
{
id: "page-id",
header: pageHeaderText,
instructions: "These are some instructions",
goTo: "id-of-next-page",
displayQuestions: all,
questions: [
{
id: stringConcat("addition-q-", loopIndex),
type: textInput,
label: getAdditionQuestionLabel(num1, num2),
isRequired: true,
correctAnswer: ans,
loop: 5,
vars: {
num1: getRandomInt(1, 100),
num2: getRandomInt(1, 100),
ans: addNums(num1, num2)
}
}
]
}
],
functions: [
addNums(num1, num2) {
additionAns = (num1 + num2)
return additionAns
},
],
vars: {
pageHeaderText: "This is a page header"
}
}
- Clone repo from github
- Install all project dependencies with
npm install
. If you get a 401 error after running this, delete thepackage-lock.json
file and runnpm install
again. - Start the program with
npm run mountainlearn
- On the main page, click the
Upload
button to upload your own written dsl file. At this point, static checks will be run. An error will appear if any errors were detected in the uploaded file. Otherwise, aStart Quiz
button will appear on the page if all static checks passed. - Click on the
Start Quiz
button to begin the quiz!
A page object creates a new page and can have any of the following fields, of which only id
is required.
{
id*,
header,
instructions,
goTo,
questions,
displayQuestions
}
id: String
- Required unique page id (eg. "contact-page")header: String
- Page header text. Can be dynamically generated with a function or variableinstructions: String
- Instructions or introduction for the quiz takers. Can be dynamically generated with a function or variable.goTo: String
- ID of page that should follow. If the page that follows is dependent on the quiz taker's responses, it can be generated with a function or a variable.questions: List
- List of questions that will appear in the page. Refer to the Question section for more information about the questions.displayQuestions: Number
- Number of questions to display on the page. This can be used to randomly select a subset of defined questions.
A question object creates a new question inside of a page. The only required fields for the question are the id
, type
and label
.
{
id*
type*
label*
options
dependsOn
displayIf
loop
isRequired
correctAnswer
vars
}
-
id: String
- Required unique question id (eg. "first-name"). Can be dynamically generated with functions or variables. When using theloop
option, theloopindex
keyword can be used to pass the loop index into the id to generate a unique ID. -
label: String
- Required this is the question text that appears above an input. Can be dynamically generated with a function or variable. -
type: textInput | radio | checkbox | dropdown
- Required question type. textInput creates a simple text input field, radio creates a single select question, checkbox is a multiple select question and dropdown creates a single select question with a dropdown list. For the select questions, a list ofoptions
must be provided -
options: List
- Required when the question type isradio
,checkbox
ordropdown
. Can either be a list of strings, a list of numbers, a variable or a function that dynamically generates a list of options. -
dependsOn: String
- If this question is conditional on a different question, this should be the question-id of the other question. Use this if you want to show/hide a question based on the response to a different question or if the options available to select are dependent on another question. -
displayIf:
- If the question's display depends on a another question, this field should have the answer that should display the question. For example, if question 1 asks a quiz taker whether they identify as a visible minority, question 2 will only display if the quiz taker selects 'yes' to question 1.{ id: "visible-minority-q", label: "Do you identify as a visible minority?", type: radio, options: ["Yes", "No"], isRequired: true } { id: 'select-minority-q' label: "What visible minority do you identify with?" type: radio options: ["Afro Canadian", "Latin American", "East Asian"...] dependsOn: "visible-minority-q" displayIf: "Yes" }
-
loop: Number
- Repeats the questionloop
times. So ifloop: 5
, then the question will be repeated 5 times. Useful when generating multiple similar questions dynamically without having to write it multiple times. TheloopIndex
keyword can be used to generate unique IDs and identify the question in the Form State{ id: stringConcat("addition-q-",loopIndex), type: textInput, label: label: "What is " + num1 + "+" + num2 + "?", correctAnswer: ans, loop: 5, vars: { num1: Form.getRandomInt(1, 100), num2: Form.getRandomInt(1, 100), ans: num1 + num2 } }
-
isRequired: true | false
- Determines whether the quiz taker has to answer the question. Defaults to false. -
correctAnswer: String | Number | Function
- Used by the autograder to determine the correct answer. Use a function or variable to dynamically generate the correct answer. -
vars: Object
- An object consisting of key/value pairs used to declare and instantiate variables. These variables, as opposed to the variables in the root object are only accessible by the question.
The functions list is a list of C-style curly bracket functions. A few notes about functions
- Functions cannot declare their own variables. If a variable is needed, declare it in the global vars object.
- Statements in functions can only be composed of mathematical expressions, other function calls (including the API library) or conditional blocks.
- The allowable math operators are
+
,-
,*
,/
- The allowable math operators are
functions: [
condFunc(num1, num2) {
if(isGreater(num1 > num2)) {
return num1
}
return num2
},
algebraQuestionAns(num1, num2) {
return roundToInt( (((num4 - num3) / (num1 * num2) * 100) / 100) )
},
getAdditionQuestionLabel(num1, num2) {
return stringConcat(num1, " + ", num2, " = ")
}
]
The vars object is a list of key/value pairs where the key is the variable and the value is the value of the variable. Variables can be of type string, number, boolean, list, or function calls.
vars: {
myStr: "this is a string var",
mybool: true,
optionsList: ["Vancouver", "Montreal", "Toronto"],
randomNum: getRandomInt(1, 100),
answer: 5 + 8,
myVar: functionCall()
}
The Form State is a data store that stores all of the quiz taker's responses. These can be accessed by the form developers by entering the page-id and question-id that is needed in this format formState["page-id"]["question-id"]
.
If a question has a 'correctAnswer' listed, this field can be accessed through the Form State by appending "-correctAnswer"
to the end of the "question-id"
part of the path. formState["page-id"]["question-id-correctAnswer"]
.
Given the dynamic nature of the Form State, to access it it must be passed as a parameter to a function. A variable cannot be instantiated as or assigned a Form State value.
pages: [
{
id: "page1",
header: "Personal Information"
instructions: "Please enter your personal information",
goTo: "display-answer-pg",
questions: [
{
id: "first-name",
label: "What is your first name?",
type: testInput,
isRequired: true
}
],
displayQuestions
}
{
id: "display-answer-pg",
header: "Your name is:",
instructions: getFormState(FormState["page1"]["first-name"]),
}
]
functions: [
getFormSatate(state) {
if(isGreater(num1 > num2)) {
return num1
}
return num2
}
]
The API library provides a number of pre-defined functions that quiz developers can use for creating their quizzes.
- stringConcat(args...),
- isEqual(arg1, arg2),
- isGreater(num1, num2),
- isGreaterEqual(num1, num2),
- isLess(num1, num2),
- isLessEqual(num1, num2)
- getRandom(),
- getRandomInt(min, max)
- roundToInt(num)
Several examples of quizzes created using the DSL can be found in the src/AST/dsl/validInputs
directory in this repository.
The interactivity and dynamic aspects of the application are handled by the DSL itself coupled with a UI framework written using the React JavaScript library. The DSL allows for the quiz definition to be specified, complete with custom functions that execute after specific user input. React is used to render the output of the DSL, and execute the custom functions defined using the DSL.
After the quiz program has been uploaded to the main page, it is parsed into an Abstract Syntax Tree (AST). A Static Checker is run to determine if there are any errors in the program. An error is thrown by the Static Checker if this is the case, and the error is caught using React which then displays an error on the page. If the Static Checker runs successfully, the Evaluator
runs through the AST to compile a JavaScript object, which is then used by React to render the questions on the page. The Evaluator
includes pointers to AST function nodes in the compiled JavaScript object. React is used to maintain state in the UI, which includes variable values, user input.
At any point where React must execute a custom function defined using the DSL, it creates a FunctionEvaluator
. It calls the visit method of the FunctionEvaluator
, passing in the program state (with the addition of function arguments and pointers to all custom functions) as a context, and a pointer to the AST node representing the custom function. The FunctionEvaluator
then traverses the AST to evaluate the custom function, and returns the result back to the React framework to render the results on the screen for the user. If an error is thrown by the FunctionEvaluator
, it is caught in the React framework, which then displays a Runtime Error on the page to the user.