NAV Navbar
javascript

Introduction

Welcome to the Graasp Get Started with React App! An initial project structure. This Get Started React App has been developed for starters adoption. You can use it as much as you need, add small to big modifications. All links in the sections and this documentation will help you get started.

You can view code examples in the dark area to the right.

This guide is divided into the following sections:

Applications

There are two types of applications within the Graasp ecosystem: Apps and Labs. Both are HTML5 applications.

Apps

Apps refer to the scaffolding applications that support learning no matter what the subject matter might be. For example, one could co

Examples

Input

The Input app is a simple app that allows a viewer to submit text so that the editors of the space in which the app is embedded can review. This is particularly useful when you consider the viewer to be a student and the editor to be a teacher. The teacher can then submit feedback to the student as well as visualise all of the responses either in a table or a word cloud.

This app uses all of the functionalities of the API, as well as the different modes and views. It also supports the offline viewers.

Demo

Repository

Badges

The Badges app allows educators to assign badges to learners within the Graasp ecosystem. The editors of the space can view the list of learners that are using their space and assign medals, points, trophies or badges that the learners will then see when visiting the space.

This app uses all of the functionalities of the API, including different modes and views. However, it does not support the offline viewers.

Demo

Repository

Labs

Labs are a special type of application that power the core learning component of Graasp. Labs usually refer to subject-specific simulations or virtual experiments that can be run on the browser or within Graasp's offline viewers. Labs usually have pedagogical requirements, which are out of the scope of this guide, that should be clear before one starts developing a lab. For example, if you want to develop a lab to teach students about pH, you should have a good idea about how this is taught in a typical Chemistry class. Technically speaking, however, labs are simple web applications, usually involving only HTML5, CSS and JavaScript. In this section we explain how to get you started developing labs using JavaScript frameworks such as React and Angular. Choose your preferred framework and start developing labs for education with this short guide.

React

Forking the Starter Repository

This is the quickest way to get started! First, open the graasp/react-get-started-app link in a new tab. On the top right of the repository, click fork. Then clone or download the cloned app from your own account.

You can also just clone using SSH or HTTPS

  git clone git@github.com:your-username/graasp-app-starter-react.git

Installing Dependencies

To install the application dependencies

1- Open your Terminal

2- Navigate to the cloned of downloaded project directory or folder

3- Run yarn install. But make sure you already have yarn installed on your development environment for this.

Starting the application

To start your development server, run yarn start when all installations is completed and you're done. This script executes a development server with force reloading enabled. Then your server keeps listening on all your modified files and reload your browser as well to apply changes an asynchronous way running on the port 3000 by default.

Then navigate to localhost:3000 to preview your application running.

Code Style

ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. Normally, it is configured by default (you can see the configuration here). Once you installed it, by restarting your server, you will see all synthax issues rendered in the terminal as well as the browser console.

In case you want to enforce a coding style, you can just use this tho disable eslint controls for a particular section or lines of code.

  // eslint-disable-next-line no-param-reassign <here include the eslint rule you want to disable, e.g. no-param-reassign>

Testing

To run tests just execute yarn test. Note that the tests are also asynchronous. Then every time you save a file, the tests are re-run. You can read more about React testing here.

Internationalization

The starter app supports internationalization using the react-i18next library.

To add a new language to your app:

1- Add a language json file under src/langs. For example de.json for German.

2- Add your language translations using a key value pairs format. The key being the expression you use in your source code and the value beings its corresponding translation. For example: "Welcome to the Graasp App Starter Kit": "Willkommen im Graasp App Starter Kit".

3- Import and add you language json to the react-i18next library under src/config/i18n.js by adding your language to the resources.

4- Add a language query string to your links to show the translated version of the app. For example: http://localhost:3000/?mode=student&lang=de.

5- To allow teachers and students to be able to change the language, a select box is available by default in the header.

You could remove this select box or include it in either the student or the teacher components by copying it along with the changeLanguage and colorStyles methods and editing the options array under src/constants/langs to add your new languages.

  <Select
    styles={this.colorStyles}
    className="LanguageSelector"
    defaultValue={options[0]}
    options={options}
    value={selectedLanguage}
    onChange={this.changeLanguage}
  />

API

Authentication

The user authentication is handled by Graasp's ecosystem in a very transparent and familiar way: using session cookies.

Apps hosted on graasp.eu domain need to use/send the session cookie when calling the API endpoints. It is mandatory for the usage of the API.

There are two types of users to consider in the ecosystem, and each uses its own session cookie for authentication purposes.

 Graasp Users

Graasp users are those authenticated using their email and password via the graasp.eu platform. These are usually the teachers or authors in spaces.

If one opens a Graasp page using the viewer.graasp.eu link, Graasp users will be enforced.

Light Users

Light users are those authenticated using a nickname, nickname and password combination, or none (anonymous), when opening a space using the Pages app. Light users usually represent the learners, students or consumers of a space.

If one opens a Graasp page using the cloud.graasp.eu link, light users will be enforced.

App Running Context

When an embedded app is loading in an ILS, a set of query parameters is passed down to it to provide the appropriate running context. These parameters can be necessary to call the API.

The most important parameter is appInstanceId. In most calls to the API, this ID needs to be specified.

Other parameters:

Entities: AppInstance

An AppInstance is a document that represents, as the name implies, an instance of an app that will run inside a space.

The main property of an AppInstance is the settings. This property's purpose is to store app configuration data. As an app developer, you should only care about modifying or reading this property.

Each AppInstance is also linked to a Graasp item where the app's url is stored along with other metadata related to Graasp.

Get

Example of returned AppInstance JSON object:

{
  "_id" : "<app-instance id>",
  "settings" : "<valid JSON - object/array/string/value/number>",
  "item" : "<graasp item id>",
  "createdAt" : "2018-12-06T10:00:00.000Z",
  "updatedAt" : "2018-12-06T10:00:00.000Z"
}

In order to read an AppInstance one needs to make a GET request to /app-instances/<id>.

A JSON structure similar to the one on the right will be returned.

Update

Example of a PATCH request body:

{
  "settings": { "min": 1, "max": 10, "step": 0.5 }
}

In order to update an app-instance one needs to make a PATCH request to /app-instances/<id>.

The request's body should contain a JSON object with the settings property defined. An app-instance update can only change the settings property - any other properties will be discarded.

The resulting updated app-instance JSON object will be returned after a successful update request.

Create/Delete

The creation and deletion of this type of entity is not available in the API - app-instances are created/deleted when the corresponding item/app is created/deleted in Graasp.

Entities: AppInstanceResource

An AppInstanceResource is a document that holds data linked to a specific app and user pair. It can be anything, be it some user input or some metadata generated by the app and related to that user.

For the same user and app pair there can be any number of AppInstanceResource objects.

AppInstanceResource objects can be potentially accessed by other users with more permissions.

Both Graasp Users and Light Users can use the API endpoints for this entitiy.

Create

Example of a POST request body:

{
  "appInstance": "5bd072b3532c3e058807decb",
  "data": {
    "offset": -34,
    "progress": 43
  },
  "type": "user-input",
  "format": "my-format-v1",
  "visibility": "public"
}

In order to create an AppInstanceResource one needs to make a POST request to /app-instance-resources.

The request's body should contain a JSON object similar to the one provided in the example.

JSON object's properties:

The resulting AppInstanceResource JSON object will be returned after a successful create request.

Get

To fetch app-instance-resources one needs to make a GET request to /app-instance-resources, and there's two options:

GET /app-instance-resources/<id>

Fetching a single AppInstanceResource, by appending its ID in the endpoint's path:

   

GET /app-instance-resources?
  appInstanceId=<app-instance id>
  [&userId=<user id>]
  [&type=<type string>]
  [&format=<format string>]

Fetching multiple AppInstanceResource objects, matching a specific filter passed as a query string:

Query parameters:

Update

To update an AppInstanceResource one should issue a PATCH request to /app-instance-resources/<id>

{
  "data": {
    "offset": -34,
    "progress": 73
  }
}

The request's body should contain a JSON object with a data property defined (see the example). Any other properties will be discarded.

The resulting updated AppInstanceResource JSON object will be returned after a successful update request.

Delete

To delete an AppInstanceResource, one can send a DELETE request to /app-instance-resources/<id>, passing in the path the ID of the AppInstanceResource to be deleted.

The deleted AppInstanceResource JSON object will be returned after a successful delete request.

Entities: User

Get

Example of a returned User JSON object:

{
  "id" : "<user id>",
  "name": "<user's name>",
  "email": "<user's email>"
  "type": "<graasp|light>"
}

In order to get the current User (linked to the current cookie session) one needs to make a GET request to /users/current.

In order to get some User one needs to make a GET request to /users/<id>. In this case the email information will be ommitted.

The type field can be either graasp (normal graasp user) or light (users created on-the-fly when viewing/using a space/ILS, based on the authentication scheme chosen by the owner of the space/ILS: username, username with password, or anonymous)

Entities: Spaces

An Space document represents a Graasp space.

Both Graasp Users and Light Users can use the API endpoints for this entitiy, except if signaled otherwise.

Get users

Example of a list of users belonging to a space:

[
  {
    "id" : "<user1 id>",
    "name": "<user1's name>",
    "type": "graasp"
  },
  {
    "id" : "<user2 id>",
    "name": "<user2's name>",
    "type": "light"
  }
]

To get the users that belong to a Space one should make a GET request to /spaces/<id>/users.

Get light-users

Example of a list of light-users belonging to a space:

[
  {
    "id" : "<user1 id>",
    "name": "<user1's name>"
  },
  {
    "id" : "<user2 id>",
    "name": "<user2's name>"
  },
]

To get the light-users that belong to a space, the ones with authentication schemes based on a username, username with password, or anonymous, one should make a GET request to /spaces/<id>/light-users.

Testing

You can test your apps' connection to our API locally using our development server. In order to do this, you need to configure your localhost to point to apps.dev.graasp.eu.

On most Linux and macOS machines you would have to edit the /etc/hosts file. On Windows machines this is achieved you edit the hosts file inside C:\Windows\System32\drivers\etc.

In both cases, simply add the following line to the respective file:

127.0.0.1   apps.dev.graasp.eu

Offline

Apps and labs that want to be able to run offline can still make calls to local APIs provided by the Graasp Desktop and Graasp App offline clients. These calls will tell the client to write or fetch information from their local databases. Not all of the calls are supported, though. In this section we outline how to setup the appropriate listeners and how to run the calls.

Downloading

When an app is downloaded for offline use, its AppInstance object and all of the public AppInstanceResource objects will be downloaded along with it. These objects will be available for the application to get from within its offline context.

Context

An application running inside an offline client will be sent the ?offline=true flag in its query string. This flag should be used by offline-ready applications to show a different offline UI, if needed, and fall back to calling the offline API rather than the online one.

For example, in order to reduce the number of calls to the offline API and thus the amount of disk I/O, the Graasp Input App will disable auto-saving functionality if offline, and instead show a "Save" button to the user.

// simplified example of rendering a button only when offline, using the react framework
renderButton() {
  const { offline } = this.props;

  // button is only visible offline
  if (!offline) {
    return null;
  }
  return (
    <Button
      variant="contained"
      color="primary"
      onClick={this.handleClickSaveText}
    >
      Save
    </Button>
  );
}

// simplified example of an onchange event handler that calls the api only when online
handleChangeText = ({ target }) => {
  const { value } = target;
  const { offline } = this.props;
  this.setState({
    text: value,
  });
  // only save automatically if online
  if (!offline) {
    this.saveToApi({ data: value });
  }
};

These are just some example of all of the logic that can be dependent on whether or not an app is running within an offline context.

Types of Messages

The offline API, when replying, will always send a message with a type and payload.

{
  "type": <TYPE>,
  "payload": <PAYLOAD>
}

The payload will be similar to the response that you would expect, for a given call, from the online API. However, because the responses are being sent through the messaging channel, we need to be able to distinguish what type of message is being sent/received. We achieve this by having a standard for PostMessageType and for ReceiveMessageType. You should define these in your application.

// PostMessageType
export const GET_APP_INSTANCE = 'GET_APP_INSTANCE';
export const GET_APP_INSTANCE_RESOURCES = 'GET_APP_INSTANCE_RESOURCES';
export const PATCH_APP_INSTANCE_RESOURCE = 'PATCH_APP_INSTANCE_RESOURCE';
export const POST_APP_INSTANCE_RESOURCE = 'POST_APP_INSTANCE_RESOURCE';

// ReceiveMessageType
export const GET_APP_INSTANCE_SUCCEEDED = 'GET_APP_INSTANCE_SUCCEEDED';
export const GET_APP_INSTANCE_RESOURCES_SUCCEEDED = 'GET_APP_INSTANCE_RESOURCES_SUCCEEDED';
export const PATCH_APP_INSTANCE_RESOURCE_SUCCEEDED = 'PATCH_APP_INSTANCE_RESOURCE_SUCCEEDED';
export const POST_APP_INSTANCE_RESOURCE_SUCCEEDED = 'POST_APP_INSTANCE_RESOURCE_SUCCEEDED';

API

By leveraging the offline flag provided by in the query string, the app can fall back on using the offline API. The offline API uses the postMessage functionality to send messages to the offline client and the window.addEventListener functionality to receive messages from the client.

To register to receive messages from the client, you should first have a function that will run every time a message is received. Messages sent from the offline client will always have a type and a payload. The types will be one of the ones defined above.

const receiveMessage = event => {
  const { data } = event;
  try {
    const message = JSON.parse(data);

    const { type, payload } = message;

    // do something with the payload given the type
    // ...

  } catch (err) {
    console.error(err);
  }
}

Once you have your receiveMessage handler, you need to register it to listen to the 'message' event. Hence the following code should run once in the app.

// if offline, we need to set up the listeners here
if (offline) {
  window.addEventListener('message', receiveMessage);
}

Every time the app receives a message, the receiveMessage handler will be called and it can update itself accordingly.

Sending a message is a very similar process. Wherever your app is requesting information from the online API, you can include a conditional so that if the app is in an offline context, it can request from the offline API instead by posting a message using postMessage.

For this it needs to include the type, which is one of the PostMessageType strings outlined above, as well as a payload, which is similar to that of the online API, but might include some extra details.

Currently, the following postMessage calls are supported.

To get the AppInstance, you post a message of type GET_APP_INSTANCE, along with the ID of the AppInstance, Space and SubSpace, which have been passed down through the url.

// example offline api call to get the app instance
if (offline) {
  return postMessage({
    type: GET_APP_INSTANCE,
    payload: {
      id: appInstanceId,
      spaceId,
      subSpaceId,
    },
  });
}

To get the all AppInstanceResource objects, you post a message of type GET_APP_INSTANCE_RESOURCES, along with the optional type of AppInstanceResource, as well as the IDs of the AppInstance, Space and SubSpace, which have been passed down through the url.

// example offline api call to get the app instance resources
if (offline) {
  return postMessage({
    type: GET_APP_INSTANCE_RESOURCES,
    payload: {
      type,
      spaceId,
      subSpaceId,
      appInstanceId,
    },
  });
}

To create an AppInstanceResource, you post a message of type POST_APP_INSTANCE_RESOURCE, along with the data, optional type and format of AppInstanceResource, as well as mandatory IDs of the Space, SubSpace, AppInstance and User, which have been passed down through the url.

// example offline api call to create an app instance resource
if (offline) {
  return postMessage({
    type: POST_APP_INSTANCE_RESOURCE,
    payload: {
      data,
      type,
      format,
      spaceId,
      subSpaceId,
      appInstanceId,
      userId,
    },
  });
}

To update an AppInstanceResource, you post a message of type PATCH_APP_INSTANCE_RESOURCE, along with the data and the IDs of the AppInstanceResource, Space, SubSpace, AppInstance, which have been passed down through the url.

// example offline api call to patch an app instance resource
if (offline) {
  return postMessage({
    type: PATCH_APP_INSTANCE_RESOURCE,
    payload: {
      id: appInstanceResourceId,
      spaceId,
      subSpaceId,
      appInstanceId,
      data,
    },
  });
}