Please see Getting Started with Custom Questions and Features before starting with your Custom Question project.
We recommend using the Custom Question Skeleton project to build your Custom Question.
This page describes how to set up default schemas rules as well as the desired editor layout for your custom Question or Feature with Learnosity Question Editor API.
Overview
In order to build a Custom Question / Feature, you need to do the following:
- Define a schema for the Question Editor form (JSON data),
- Provide a corresponding HTML layout, which contains the hooks which Question Editor uses to insert the schema attributes (HTML file),
- Provide the custom question logic (JS file) which contains logic to parse the Question JSON and render the Question, and
- Provide the custom scoring logic (JS file) which contains logic to score the learner's response.
In this article, we'll look into some different examples for the schema and the HTML layout (points one and two above) - covering a simple static form, a dynamic example and a more advanced dynamic example.
Please refer to Creating Custom Questions for information on the custom Question and scoring logic (points 3 and 4 above).
Custom Question / Feature Schemas
In order for the Question Editor to understand what desired input types you want to use when authoring your custom Question or Feature, you need to define the schema for each input.
The rule will be inserted through the custom_question_types and custom_feature_types initialization options.
Each entry in the array will define some properties including the JavaScript and the layout, see more information on the docs pages linked above.
The schemas are defined in the property editor_schema.
Understanding editor_schema
The editor_schema defines the authoring interface for a Custom Question or Feature. It specifies the attributes that authors can configure, how those attributes are displayed in the Question Editor, and how their values are stored in the Question JSON.
The Question Editor uses the editor_schema to:
- Generate the authoring interface
- Validate author input
- Store attribute values in the Question JSON
The editor_schema defines the configurable attributes for the Custom Question or Feature. The editor_layout, when supplied, defines where those attributes are rendered in the Question Editor. For more information, see Adding a Custom Editor Layout for a Question Type.
Note: The Question Editor supports both attributes and properties within editor_schema. They are functionally equivalent because properties is translated internally by the Question Editor. New implementations should use attributes, which is the canonical form used throughout the Learnosity documentation.
Supported schema attribute types
Every attribute defined within editor_schema.attributes (or editor_schema.properties) must specify a type.
| Type | Description |
|---|---|
string |
Simple text input. |
number |
Numeric input. Only numeric values are accepted. |
boolean |
Checkbox/toggle control. |
editor |
Rich text editor. |
question |
Renders the current Custom Question using the supplied white_list attributes. This is commonly used when authoring validation responses. |
array |
Repeatable collection of attributes. Requires the items property. |
object |
Groups related attributes together and supports nested attribute structures. |
The type determines the data model for the attribute. You can optionally specify an element to change how that attribute is rendered in the Question Editor.
Supported editor elements
The optional element property overrides the default editor control used for an attribute.
| Element | Description |
|---|---|
select |
Displays a dropdown list. Requires the options property. |
colorPicker |
Displays a colour picker. Supported only with type: "string". |
question |
Renders the current Question using the supplied white_list. |
array |
Used only when type is array. No other element values are supported for arrays. |
For example, a dropdown is defined as:
{
"type": "string",
"element": "select",
"options": [
{
"label": "Display text",
"value": "stored_value"
}
]
}Common editor_schema properties
| Property | Required | Description |
|---|---|---|
type |
Yes | Specifies the attribute type. |
name |
Recommended | Label displayed in the Question Editor. |
description |
No | Additional help text displayed to authors. |
element |
No | Overrides the default editor control. |
required |
No | Requires the author to enter a value before saving. |
default |
No | Default value used when creating a Question. |
group |
No | Groups attributes in automatically generated layouts. |
options |
Required when element is select. |
|
items |
Required when type is array. |
|
attributes |
Required when type is object. |
|
white_list |
Required when type or element is question. |
|
min |
No | Minimum value for number attributes. |
editorV3 |
No | Question Editor V3-specific overrides. |
Validation rules
The Question Editor validates the editor_schema before allowing a Question to be saved. Depending on the schema configuration, the following validation rules apply:
-
type: "number"accepts only numeric values. -
required: truerequires a value before the Question can be saved. -
min: "number"enforces the minimum numeric value. -
element: "select"requires theoptionsproperty. -
type: "array"requires theitemsproperty.
Static attributes
Static forms are easily achievable using the schemas rules mentioned above. To see an example of this, along with the schema and the layout, please see our demo for the custom Question, using a box & whisker chart.
Dynamic attributes
It is possible to create dynamic forms using conditional_attributes. That is, you can have attributes which only appear when another attributes value is a specific value.
If you want to have nested conditionals, that is, attributes which are shown based on the value if another attribute, which itself is shown based on the attribute of another, then the properties must be nested themselves.
For single level conditionals the output JSON can remain flat, but if if you have nested, then the JSON will have nested attributes.
To understand further, see the following two examples.
Referencing nested attributes
Nested attributes are defined using type: "object" and referenced using dot notation.
Example schema:
{
"validation": {
"type": "object",
"attributes": {
"valid_response": {
"type": "object",
"attributes": {
"score": {
"type": "number"
}
}
}
}
}
}Nested attributes are referenced using their full attribute path.
Example:
<span data-lrn-qe-input="validation.valid_response.score"></span>
For array attributes, use the [*] wildcard within data-lrn-qe-loop.
Example:
<div data-lrn-qe-loop="validation.alt_responses[*]"> <span data-lrn-qe-input="validation.alt_responses[*].score"></span> </div>
Basic Example
In this example, we have a dropdown menu to select types of triangle. When one is selected, different fields show based on that value. For example, for equilateral, as all sides are the same length, there's just one field for "Side length" which appears. Whereas for scalene, as all three sides are different lengths, three fields (array) are shown. See the video below which demonstrates this, and below that are the code examples for the schema, HTML and output JSON.
Video 1: basic dynamic form
Schema data
{
"editor_schema": {
"hidden_question": false,
"attributes": {
"stimulus": {
"type": "string",
"name": "Stimulus",
"description": "The question for the student to answer",
"required": true,
"element": "editor",
"default": "Calculate the area of the shape below"
},
"triangle_type": {
"type": "string",
"name": "Triangle Type",
"description": "Select a type of triangle.",
"required": true,
"element": "select",
"options": [{
"value": "equilateral",
"label": "Equilateral"
}, {
"value": "isosceles",
"label": "Isosceles"
}, {
"value": "scalene",
"label": "Scalene"
}, {
"value": "right",
"label": "Right Angled"
}],
"default": "equilateral"
},
"color": {
"type": "string",
"name": "Color",
"description": "Fill color of the shape.",
"required": true,
"element": "colorPicker",
"default": "#0f3d6d"
}
},
"conditional_attributes": {
"attribute_key": "triangle_type",
"conditions": [{
"value": "equilateral",
"attributes": {
"side_length": {
"type": "number",
"name": "Side Length (cm)",
"description": "Length of the side of the triangle.",
"required": true
}
}
}, {
"value": "isosceles",
"attributes": {
"base": {
"type": "number",
"name": "Base",
"description": "Base length of the triangle.",
"required": true
},
"height": {
"type": "number",
"name": "Side Length",
"description": "Length of the equal sides of the triangle.",
"required": true
}
}
}, {
"value": "scalene",
"attributes": {
"side_lengths": {
"items": {
"type": "number",
"name": "Side length "
},
"name": "Side Lengths",
"description": "Lengths of the sides of the triangle.",
"required": true,
"type": "array",
"count": {
"minimum": 3,
"maximum": 3
}
}
}
}, {
"value": "right",
"attributes": {
"base": {
"type": "number",
"name": "Base",
"description": "Base length of the triangle.",
"required": true
},
"height": {
"type": "number",
"name": "Height",
"description": "Height of the triangle.",
"required": true
}
}
}]
}
}
}
Source code example 1: the schema data
HTML layout
<div class="lrn-qe-edit-form">
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="stimulus"></span><span data-lrn-qe-input="stimulus"></span>
</div>
<!-- triangle -->
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="triangle_type"></span><span data-lrn-qe-input="triangle_type"></span>
</div>
<!-- equilateral -->
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="side_length"></span><span data-lrn-qe-input="side_length"></span>
</div>
<!-- isosceles -->
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="base"></span><span data-lrn-qe-input="base"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper><span data-lrn-qe-label="height"></span>
<span data-lrn-qe-input="height"></span>
</div>
<!-- scalene -->
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<div data-lrn-qe-loop="side_lengths[*]">
<div data-lrn-qe-label="side_lengths[*]"></div>
<div data-lrn-qe-input="side_lengths[*]"></div>
</div>
</div>
<!-- color -->
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="color"></span><span data-lrn-qe-input="color"></span>
</div>
</div>Source code example 2: the HTML layout
Example JSON (for a scalene triangle)
{
"type": "custom",
"stimulus": "Calculate the area of the shape below",
"js": {
"question": "shapes_q.js",
"scorer": "shapes_s.js"
},
"valid_response": 1.9843,
"score": 1,
"custom_type": "custom",
"css": "custom.css",
"version": "v1.0.0",
"triangle_type": "scalene",
"color": "#0f3d6d",
"side_lengths": [2, 3, 2]
}Source code example 3: the JSON source for the scalene example
Advanced Example
In this example, we have a select dropdown where the shape dimensions is picked first (2D/3D), then based on that another drop-down menu shows different classifications of shapes (Triangle, Quadrilateral, etc.), and then based on that different shape types are shown (Square, Scalene, and so on).
See the video below which demonstrates this, and below that are the code examples for the schema, HTML and output JSON.
Video 2: advanced dynamic form
Schema data
{
"editor_schema": {
"hidden_question": false,
"attributes": {
"stimulus": {
"type": "string",
"name": "Stimulus",
"description": "The question for the student to answer",
"required": true,
"element": "editor",
"default": "Calculate the [area/volume] of the shape below"
},
"dimensions": {
"type": "string",
"name": "Shape Dimensions",
"description": "Select the type of shape.",
"required": true,
"element": "select",
"options": [{
"value": "2d",
"label": "2D"
}, {
"value": "3d",
"label": "3D"
}],
"default": "3d"
},
"color": {
"type": "string",
"name": "Color",
"description": "Fill color of the shape.",
"required": true,
"element": "colorPicker",
"default": "#0cb0d8"
}
},
"conditional_attributes": {
"attribute_key": "dimensions",
"conditions": [{
"value": "2d",
"attributes": {
"2d": {
"type": "object",
"attributes": {
"shape": {
"type": "string",
"name": "2D Shape",
"description": "Select a 2D shape.",
"required": true,
"element": "select",
"options": [{
"value": "quadrilateral",
"label": "Quadrilateral"
}, {
"value": "triangle",
"label": "Triangle"
}, {
"value": "polygon",
"label": "Polygon"
}],
"default": "triangle"
}
},
"conditional_attributes": {
"attribute_key": "shape",
"conditions": [{
"value": "quadrilateral",
"attributes": {
"quadrilateral": {
"type": "object",
"attributes": {
"type": {
"type": "string",
"name": "Quadrilateral Type",
"description": "Select a type of quadrilateral.",
"required": true,
"element": "select",
"options": [{
"value": "square",
"label": "Square"
}, {
"value": "rectangle",
"label": "Rectangle"
}, {
"value": "rhombus",
"label": "Rhombus"
}],
"default": "square"
}
},
"conditional_attributes": {
"attribute_key": "type",
"conditions": [{
"value": "square",
"attributes": {
"square_options": {
"type": "object",
"name": "Square Options",
"description": "Options for squares.",
"required": true,
"attributes": {
"side_length": {
"type": "number",
"name": "Side Length",
"description": "Length of the side of the square.",
"required": true
}
}
}
}
}, {
"value": "rectangle",
"attributes": {
"rectangle_options": {
"type": "object",
"name": "Rectangle Options",
"description": "Options for rectangles.",
"required": true,
"attributes": {
"length": {
"type": "number",
"name": "Length",
"description": "Length of the rectangle.",
"required": true
},
"width": {
"type": "number",
"name": "Width",
"description": "Width of the rectangle.",
"required": true
}
}
}
}
}, {
"value": "rhombus",
"attributes": {
"rhombus_options": {
"type": "object",
"name": "Rhombus Options",
"description": "Options for rhombuses.",
"required": true,
"attributes": {
"side_length": {
"type": "number",
"name": "Side Length",
"description": "Length of the side of the rhombus.",
"required": true
},
"angle": {
"type": "number",
"name": "Angle",
"description": "Angle of the rhombus.",
"required": true
}
}
}
}
}]
}
}
}
}, {
"value": "triangle",
"attributes": {
"triangle": {
"type": "object",
"attributes": {
"type": {
"type": "string",
"name": "Triangle Type",
"description": "Select a type of triangle.",
"required": true,
"element": "select",
"options": [{
"value": "equilateral",
"label": "Equilateral"
}, {
"value": "isosceles",
"label": "Isosceles"
}, {
"value": "scalene",
"label": "Scalene"
}, {
"value": "right",
"label": "Right Angled"
}],
"default": "equilateral"
}
},
"conditional_attributes": {
"attribute_key": "type",
"conditions": [{
"value": "equilateral",
"attributes": {
"equilateral_options": {
"type": "object",
"name": "Equilateral Options",
"description": "Options for equilateral triangles.",
"required": true,
"attributes": {
"side_length": {
"type": "number",
"name": "Side Length (cm)",
"description": "Length of the side of the triangle.",
"required": true
}
}
}
}
}, {
"value": "isosceles",
"attributes": {
"isosceles_options": {
"type": "object",
"name": "Isosceles Options",
"description": "Options for isosceles triangles.",
"required": true,
"attributes": {
"base": {
"type": "number",
"name": "Base",
"description": "Base length of the triangle.",
"required": true
},
"side_length": {
"type": "number",
"name": "Side Length",
"description": "Length of the equal sides of the triangle.",
"required": true
}
}
}
}
}, {
"value": "scalene",
"attributes": {
"scalene_options": {
"type": "object",
"name": "Scalene Options",
"description": "Options for scalene triangles.",
"required": true,
"attributes": {
"side_lengths": {
"items": {
"type": "number",
"name": "Side length "
},
"name": "Side Lengths",
"description": "Lengths of the sides of the triangle.",
"required": false,
"type": "array",
"count": {
"minimum": 3,
"maximum": 3
}
}
}
}
}
}, {
"value": "right",
"attributes": {
"right_options": {
"type": "object",
"name": "Right Options",
"description": "Options for right triangles.",
"required": true,
"attributes": {
"base": {
"type": "number",
"name": "Base",
"description": "Base length of the triangle.",
"required": true
},
"height": {
"type": "number",
"name": "Height",
"description": "Height of the triangle.",
"required": true
}
}
}
}
}]
}
}
}
}, {
"value": "polygon",
"attributes": {
"type": "object",
"polygon": {
"attributes": {
"type": {
"type": "string",
"name": "Polygon Type",
"description": "Select a type of polygon.",
"required": true,
"element": "select",
"options": [{
"value": "pentagon",
"label": "Pentagon"
}, {
"value": "hexagon",
"label": "Hexagon"
}, {
"value": "octagon",
"label": "Octagon"
}],
"default": "pentagon"
}
},
"conditional_attributes": {
"attribute_key": "type",
"conditions": [{
"value": "pentagon",
"attributes": {
"pentagon_options": {
"type": "object",
"name": "Pentagon Options",
"description": "Options for pentagons.",
"required": true,
"attributes": {
"side_length": {
"type": "number",
"name": "Side Length",
"description": "Length of the side of the pentagon.",
"required": true
}
}
}
}
}, {
"value": "hexagon",
"attributes": {
"hexagon_options": {
"type": "object",
"name": "Hexagon Options",
"description": "Options for hexagons.",
"required": true,
"attributes": {
"side_length": {
"type": "number",
"name": "Side Length",
"description": "Length of the side of the hexagon.",
"required": true
}
}
}
}
}, {
"value": "octagon",
"attributes": {
"octagon_options": {
"type": "object",
"name": "Octagon Options",
"description": "Options for octagons.",
"required": true,
"attributes": {
"side_length": {
"type": "number",
"name": "Side Length",
"description": "Length of the side of the octagon.",
"required": true
}
}
}
}
}]
}
}
}
}]
}
}
}
}, {
"value": "3d",
"attributes": {
"3d": {
"type": "object",
"attributes": {
"shape": {
"type": "string",
"name": "3D Shape",
"description": "Select a 3D shape.",
"required": true,
"element": "select",
"options": [{
"value": "polyhedron",
"label": "Polyhedron"
}, {
"value": "sphere",
"label": "Sphere"
}, {
"value": "cylinder",
"label": "Cylinder"
}, {
"value": "cone",
"label": "Cone"
}],
"default": "polyhedron"
}
},
"conditional_attributes": {
"attribute_key": "shape",
"conditions": [{
"value": "polyhedron",
"attributes": {}
}]
}
}
}
}]
}
}
}Source code example 4: schema data for the advanced example
HTML layout (not all included for brevity)
<div class="lrn-qe-edit-form">
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="stimulus"></span>
<span data-lrn-qe-input="stimulus"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="dimensions"></span>
<span data-lrn-qe-input="dimensions"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="2d.shape"></span>
<span data-lrn-qe-input="2d.shape"></span>
</div>
<!-- 2D --><!-- quadrilateral -->
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="2d.quadrilateral.type"></span>
<span data-lrn-qe-input="2d.quadrilateral.type"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="2d.quadrilateral.square_options.side_length"></span>
<span data-lrn-qe-input="2d.quadrilateral.square_options.side_length"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="2d.quadrilateral.square_options.color"></span>
<span data-lrn-qe-input="2d.quadrilateral.square_options.color"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="2d.quadrilateral.rectangle_options.length"></span>
<span data-lrn-qe-input="2d.quadrilateral.rectangle_options.length"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="2d.quadrilateral.rectangle_options.width"></span>
<span data-lrn-qe-input="2d.quadrilateral.rectangle_options.width"></span>
</div>
<div class="lrn-qe-col-lg-12" data-lrn-qe-layout-wrapper>
<span data-lrn-qe-label="2d.quadrilateral.rectangle_options.color"></span>
<span data-lrn-qe-input="2d.quadrilateral.rectangle_options.color"></span>
</div>
....
</div>Source code example 5: HTML layout for the advanced example
JSON source (for the 3D -> polyhedron -> cube)
{
"type": "custom",
"js": {
"question": "shapes_q.js",
"scorer": "shapes_s.js"
},
"custom_type": "custom",
"css": "custom.css",
"version": "v1.0.0",
"dimensions": "3d",
"color": "#0cb0d8",
"3d": {
"polyhedron": {
"cube_options": {
"edge_length": 3
}
}
}
}Source code example 6: JSON for the 3D polyhedron cube
JSON source (for the 2D -> polyhedron -> cube)
{
"type": "custom",
"stimulus": "Calculate the [area/volume] of the shape below",
"js": {
"question": "shapes_q.js",
"scorer": "shapes_s.js"
},
"custom_type": "custom",
"css": "custom.css",
"version": "v1.0.0",
"dimensions": "2d",
"color": "#0cb0d8",
"2d": {
"shape": "quadrilateral",
"quadrilateral": {
"type": "rectangle",
"rectangle_options": {
"length": 1,
"width": 4
}
}
}
}Source code example 7: JSON for the 2D polyhedron cube
Schema limitations
The following limitations apply when defining an editor_schema:
- Object nesting has no practical depth limit.
- Arrays of objects are supported.
- Nested arrays are supported.
-
white_listsupports only top-level attribute names. Nested attribute paths (for example,validation.valid_response.score) cannot be specified inwhite_list. -
conditional_attributescan only be defined at the root level of theeditor_schema.
How to Create Custom Question / Feature Custom Tile Items
Once the custom Question / Feature is defined through custom_question_types and custom_feature_types, we can now reference those custom Questions / Features in question_type_templates through their custom_type unique IDs.
Defining Custom Question / Feature Tile Items
const initOptions = {
custom_question_types: [{
"custom_type": "custom_short_text",
"type": "custom",
"name": "Custom Shorttext (DRAFT)",
"js": {
"question": "//demos.learnosity.com/usecases/customquestions/custom_shorttext_q.js",
"scorer": "//demos.learnosity.com/usecases/customquestions/custom_shorttext_s.js"
},
"css": "//demos.learnosity.com/usecases/customquestions/custom_shorttext.css",
"editor_layout": "//demos.learnosity.com/usecases/customquestions/custom_shorttext.html",
"version": "v0.1.0",
"editor_schema": {
"hidden_question": false,
"properties": {
"valid_response": {
"type": "string",
"name": "Valid response",
"description": "Correct answer for the question.",
"required": false
},
"score": {
"type": "number",
"name": "Score",
"description": "Score for a correct answer.",
"required": false,
"default": 0
}
}
}
}],
custom_feature_types: [{
"custom_type": "name_of_custom_type",
"name": "Display Name of Custom Feature",
"js": "//[your.domain]/myfeaturescript.js",
"css": "//[your.domain]/myfeaturestyles.css",
"version": "1.0.0",
"editor_layout": "//[your.domain]/myquestioneditorlayout.html",
"editor_schema": {
"hidden_question": false,
"properties": {
"placeholder_text": {
"type": "string",
"required": false
}
}
}
}],
question_type_templates: {
custom_short_text: [{
"name": "Custom Question - Short text",
"description": "A custom question type - short text",
"group_reference": "other",
"image": "custom_image_link_of_your_custom_question_tile_item",
"defaults": {
"type": "custom",
"js": {
"question": "//demos.learnosity.com/usecases/customquestions/custom_shorttext_q.js",
"scorer": "//demos.learnosity.com/usecases/customquestions/custom_shorttext_s.js"
},
"css": "//demos.learnosity.com/usecases/customquestions/custom_shorttext.css",
"valid_response": "",
"score": 1
}
}],
custom_text_input_feature: [{
"name": "Custom Feature - Simple textinput",
"description": "A custom feature type - simple textinput",
"image": "custom_image_link_of_your_custom_feature_tile_item",
"defaults": {
"type": "customfeature",
"js": "//learnosity.github.io/docs-learnosity-apis/examples/custom_feature_types/textinput/index.js",
"css": "//learnosity.github.io/docs-learnosity-apis/examples/custom_feature_types/textinput/index.css",
"placeholder": "Default placeholder"
}
}]
}
};Source code example 8: Defining Custom Question / Feature Tile Items
Best practices
When defining an editor_schema:
- Use
attributesinstead ofproperties. Although both are supported,attributesis the canonical form. - Use snake_case when naming custom attributes to remain consistent with Learnosity Question schemas.
- Avoid custom attribute names that conflict with reserved attributes such as
type,custom_type,js,css, andversion. - Use
groupto organise automatically generated layouts. - Use
type: "object"to organise related attributes into logical groups. - Use
type: "array"together withitemsfor repeatable collections of attributes. - Use
conditional_attributesto dynamically show or hide attributes based on the value of another attribute. - Reuse supported native Question attributes (for example,
stimulus,validation, andinstant_feedback) when your Custom Question requires the same platform functionality.