Developing Materia Widgets

Create your own widgets!

Developing Widgets for Materia

It’s easiest to imagine Materia as an app store for small educational apps. It’s the server that hosts and manages those individual apps (or widgets in Materia speak) in one centralized, integrated platform.

Widgets are packaged into a single file and easily installed with a single command. This page describes the widget development process and tools required to create a widget package that can be installed in any Materia distribution.

What Language are Widgets Written In?

Most widgets are built with HTML, CSS, and Javascript. However, just about anything viewable in a browser that talks to Javascript will work. This guide will focus on building Javascript widgets. If you’re using something more exotic, this will still be useful to understand how to wrap your fancy gadgets into a Materia widget.

Development Environment

The Materia Widget Development Kit provides the best environment to build and test widgets. It’s a combination of an Express.js server, a splash of javascript borrowed from the Materia Server, and a little bit of webpack to glue it all together. Check out the MWDK page to get set up and running.

What’s a Widget

A widget is a little packaged application that adheres to a few basic requirements. It has full control over what happens within it’s iframe, with a simple API to get data and save scores to the server. The simplest widgets can be simply static web pages.

The Player

A widget player is just an html page. You can place whatever you like on the page like graphics, Javascript, Flash, Unity, whatever. To load content and save scores, you’ll need to load a javascript file, materia.enginecore.js. The Engine Core abstracts communicating with Materia into a simple API.

Here’s an empty player.html file:

<!DOCTYPE html>
		<title>A Basic Widget Player</title>
		<script src="materia.enginecore.js"></script>
				start: (instance, qset, qsetVersion) => {
					// process the qset and start your app

See the Materia Engine Core API for details on all of it’s magical super powers.

A Typical Sequence of Events in The Player

  1. Call Materia.Engine.start({...}) to signal that your widget has loaded.
  2. The Engine Core will load instance content from the server and pass it to your start callback.
  3. Your widget processes the instance and qset data and do whatever amazing things your widget does.
  4. As the user progresses, send logs, scores, and data to the server.
  5. Finish by calling Materia.Engine.end(), Materia will show the score screen.

The Creator

A creator, like the player, is just an HTML page with your assets and code. Just like the player, there is a helper script to talk to the Materia API: materia.creatorcore.js.

A creator is totally optional, but if you want to allow users to create customized content for widgets, you’ll need one.

Here’s an empty creator.html file, it’s got a few special properties you’ll want to define:

<!DOCTYPE html>
		<title>A Basic Widget Creator</title>
		<script src="materia.creatorcore.js"></script>
			let _title = ''
			let _qset = {}

				initNewWidget: (widget, baseUrl, mediaUrl) => {
					// user is creating a new widget
					// display your custom input interface
				initExistingWidget: (widget, title, qset, qsetVersion, baseUrl, mediaUrl) => {
					// user is editing an existing widget
					_title = title
					_qset = qset
				onSaveClicked: (mode = 'save') => {
					// possible modes: 'publish', 'preview', 'save'
					// user clicked save, convert our data into a qset and save it, _qset)
				// onSaveComplete: (instanceName, widget, qset, qsetVersion) => {},
				// onMediaImportComplete: (arrayOfMedia) => {},
				// onQuestionImportComplete: (arrayOfQuestions) => {},

See the Materia Creatore Core API for details on creating and updating widgets.

A Typical Sequence of Events in The Creator

  1. Call Materia.CreatorCore.start({...}) to signal that your creator has loaded.
  2. The Creator Core will load content from the server and pass it one of your initWidget callbacks.
  3. Your creator processes the instance and qset data and lets the user customize the content.
  4. The user clicks Save
  5. Creator Core calls onSaveClicked where you build a qset and call

The Score Screen

Materia provides a standard score screen that will be fine for displaying scores and answer feedback in most cases.

However, for more advanced uses a custom score screen can be provided. It’s just another html page with some Javascript, this time using materia.scorecore.js to talk to the API.

<!DOCTYPE html>
		<title>A Basic Score Screen</title>
		<script src="materia.scorecore.js"></script>
			// hide the default results table
			// start & register a callback
				start: (instance, qset, scoreTable, isPreview, qsetVersion) => {
					// build a custom score display here

See the Materia Score Core API for score screen API details.

Typical Source Code File Structure

├── /src
│   ├── /_icons               # REQUIRED - unique icon in multiple sizes
│   ├── /_screen-shots        # REQUIRED - screenshots for widget detail page
│   ├── /_score
│   │   └── /score_module.php # server side score checking
│   ├── /assets               # js, css, assets for demo.json
│   ├── player.html           # REQUIRED
│   ├── creator.html
│   ├── scoreScreen.html
│   ├── demo.json             # REQUIRED - A qset used for the demo instance
│   └── install.yaml          # REQUIRED - Installation settings
├── package.json
└── webpack.config.js

Compiling with Webpack

Here is a basic example of a webpack config for a widget. See the materia widget development kit page for an explanation of the widgetWebpack functions called.

const path = require('path')
const srcPath = path.join(__dirname, 'src') + path.sep
const outputPath = path.join(__dirname, 'build') + path.sep
const widgetWebpack = require('materia-widget-development-kit/webpack-widget')

const rules = widgetWebpack.getDefaultRules()
const copy = widgetWebpack.getDefaultCopyList()

const entries = {
	'player.js': [
		path.join(srcPath, 'player.js')
	'player.css': [
		path.join(srcPath, 'player.html'),
		path.join(srcPath, 'player.scss')
	'creator.js': [
		path.join(srcPath, 'creator.js')
	'creator.css': [
		path.join(srcPath, 'creator.html'),
		path.join(srcPath, 'creator.scss')

const options = {
	entries: entries,
	moduleRules: rules,
	copyList: copy

const buildConfig = widgetWebpack.getLegacyWidgetBuildConfig(options)

module.exports = buildConfig