My First Moment

In this tutorial we are going to create a Treasure Hunt application to show the process of developing a simple Stagecast Moment from scratch.

You find the demo code on Github: https://github.com/stagecast/moment-examples

Before we start

In the first stage we define what our moment should do. Based on these requirements we create a screen design which then will be the foundation of our JavaScript application.

The Stagecast SDK provides developers the possibility to develop custom Moments with various JavaScript frameworks/libraries. For this tutorial we chose Vue.js to demonstrate the process. Vue.js is an extremely lightweight JavaScript framework (Weighing only ~ 7kb gzipped) and it will boost our development process immensely. Feel free to use any other JavaScript framework like React, Angular or Ember.

Things to consider

Our Moment will be rendered inside a webview frame which makes the integration easy for event organizers. An event organizer could include the Moment directly into his own native application, his website or use it through the Stagecast app.

As a Moment developer we have to take care of creating mainly two things:

  • The Event Visitor View: The Moment launched by the event visitor. A web application consisting of HTML/CSS/JavaScript. Therefore you can use every library you want, as long as it compiles to HTML/CSS/JavaScript.

  • The Result Page: A result screen, which can be broadcasted on a screen by the event organizer.

To demonstrate this process, we will develop a demo Moment called “Treasure Hunt”: it encourages event visitors to go to different locations in order to find answers to questions the event organizer defines in the Stagecast backend. The backend also enables the event organizer to define further variables like the background-image and all sort of text.

Event visitors then enter the answers into an input field which are evaluated by the app. Once all questions are answered correctly by the user, the user can claim a prize. At the same time information about the treasure hunt are displayed on the “Result Page”. It can be displayed on screens inside the event venue and will update live how many questions have been correctly answered in total to encourage other people to take part in the game.

Let’s start our project by setting up the basics.

Creating a Project

Folder Structure

Before we can start to code, we have to create a project with a certain folder structure for our Moment.

- manifest.json
- config
  - mobile.config.json
  - results.config.json
- src
  - mobile
    - index.html
    - ... other css, js or image assets
  - results
    - index.html
    - ... other css, js or image assets

Let’s explain the files we created step by step:

manifest.json: You may know manifest.json files from progressive web apps. This file contains general information about the moment. For example the current version, the name, the developer, a short description and other information about the moment. For further details take a look at the Moments SDK documentation

config/mobile.config.json: This file defines the configuration form that enables event organizers to customize the Moment (e.g. the background-image or the questions of the Moment). The Moments backend will create a form to configure the moment from this JSON file automatically. The format must comply with the ngx-formly format standard.

config/results.config.json: This file works just like the mobile.config.json file but instead defines the configuration form for the results screen.

Creating a Development Sandbox

When a webview (for example the one integrated in the Stagecast app) launches a Moment, it will pass certain information to the Moment. Because we will develop and test our Moment on our locale machine first, before pushing it to a staging server, we have to create a development sandbox, which takes care of passing test data to our Moment.

To mimic the data being passed to the moment we have to retrieve an API token and some information about our moment first. Currently you have to make two POST requests to the Stagecast API manually. Feel free to use a HTTP client like Postman to make your life easier.

Get the token

POST https://stagecast.se/api/users/login

First we will have to get our API token by making a login request to Stagecast’s API. Only the token property of the response will be relevant for us.

Request Body

{
  ...
  "token": "<YOUR_API_TOKEN>"
  ...
}

Next we have to retrieve the momentID and the momentClassID from the Stagecast Sandbox API. You will need your eventID for this request. You can find the eventID in the URL when opening an event in the Stagecast Web Platform: https://stagecast.se/dashboard/events/<YOUR_EVENT_ID>/launchpad

Create the Sandbox

POST https://stagecast.se/api/sandbox/<YOUR_EVENT_ID>

{
  "momentID": "<YOUR_MOMENT_ID>",
  "momentClassID": "<YOUR_MOMENT_CLASS_ID>",
  "eventID": "<YOUR_EVENT_ID>",
  "creationTime": 1584007941692
}

After retrieving those values from the API we can start setting up our sandbox.

Our sandbox/adapter is a simple HTML/JS application itself, which will launch the Moment inside an iframe.

For this we will create a few files in the directory src/adapters/tests (you have to create this directory manually).

The content of the folder is the following:

<!DOCTYPE html>
<!DOCTYPE html>
<!-- Use this file to test your Moment -->
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
    <title>Treasure Hunt Moment Tester</title>
<style>body {
    position:fixed;
    padding: 0px;
    margin: 0px;
    overflow:hidden;
    height:100vh;
    width:100vw;
}

#frame {
    position: absolute;
    width: 100%;
    height: 100%;
    top:0;
    left:0;
    margin:0;
    padding:0;
    overflow: hidden;
    box-sizing: border-box;
    border-style: none;
}
</style>
</head>
<body>
<!-- iframe where the treasure hunt moment will be loaded. -->
<iframe id="frame" title="Treasure Hunt Moment" onload="iframeLoaded()" src="../../mobile/index.html"></iframe>
<script src="./test-data.js"></script>
<script src="./loader.js"></script>
</body>
</html>

src/adapters/tests/mobile.adapter.html: With this file we create an iframe, and link it to the index.html inside our src/mobile folder (So our Moment will launch inside this iframe).

src/adapters/tests/test-data.js: This is the data we will seed the Moment with. This JavaScript object is normally passed to the Moment via the Stagecast App/Webview integration. Now we have to inject the test data we’ve just created into the iframe in our mobile.adapter.html. This is done in the loader.js file we include right after the test-data.js.

src/adapters/tests/loader.js: Inside this file we define the functioniframeLoaded() which will be called as soon as the iframe finishes loading. Once loaded, we will pass the variable momentData we’ve just created to our iframe with the postMessage() method. That’s it. From now on we can launch our moment by opening the src/adapters/tests/mobile.adapter.html file. Right now you won’t see much, because our moment is still an empty HTML file, but let’s change that!

Creating a new Vue.js project

1. Set up

As mentioned before, we will use Vue.js for our Moment. In addition to the Vue framework we will use the handy Vue CLI (https://cli.vuejs.org) to create and compile our Vue project. Take a look at the Vue CLI documentation for the installation instructions.

Once Vue is installed, we create our Vue project inside the src directory by running the following command in our terminal directly inside the src directory:

$ vue create vue

This will open a wizard guiding you through the creation of the new Vue project. The default configuration will be fine for our purposes.

By default, Vue CLI compiles its project into the dist folder. In order to change the output directory to the root of our Moment (src/mobile/) we have to edit the default Vue CLI configuration.

We do this by creating a file vue.config.js in the root of our vue project (src/vue/).

// src/vue/vue.config.js
module.exports = {
  outputDir: '../mobile',
  publicPath: './'
};

This changes the output directory from the default dist directory to thesrc/mobile/directory. In addition we set the public path (which is used for compiled assets) to the current directory. This will make all our asset paths relative to the output directory.

Now we can run npm run build inside our Vue root directory to check if everything is working as expected. Wait until the command has finished, and then open the mobile adapter HTML file in your browser. You should be able to see a page generated from the Vue project.

To start with a blank page we will delete the HelloWorld.vue component from our project (https://github.com/moritzschrom/treasure_hunt_moment/commit/80c25bf961f02dcc7bf8377e0162ba66fcbe95b2).

2. Include the Stagecast SDK

In order to integrate our application inside the Stagecast System we need include the SDK first inside src/vue/public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
+   <script src="https://stagecast.se/media/lib/mdk/stagecast.min.js"></script>
    <!-- built files will be auto injected -->
  </body>
</html>

3. Make Stagecast available for Vue

Now we need to make the Stagecast SDK available for Vue inside our moment.

We achieve that by creating a new instance from the Stagecast library in src/vue/src/main.jsand creating a global $SDK variable by adding it to Vue.prototype. From now on we can call the API from everywhere inside the project.

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

if (window.Stagecast) {
  Vue.prototype.$SDK = new window.Stagecast()

  new Vue({
    render: h => h(App)
  }).$mount('#app')
}

4. Defining Default Data

For developing purposes we define a default data set which will be used if no other data is received. Therefore we will define a data-variable in our src/vue/src/App.vue component called “defaultData” with all options necessary for our test application.

We will pass the data as a comma separated string and the image as an asset ID.

5. Connect to the SDK to get the Moment Class

In the next step we define a method to initialize the Stagecast Moments SDK. Inside the method we call the getMomentClass from the Moments SDK.

In the next step we define a method called initSettings. We call it and pass the data we received from the getMomentClass.

Inside initSettings we check if the keys of the object are received and if not fill it with default values. (https://github.com/moritzschrom/treasure_hunt_moment/blob/e8170e77b4122a720ae1793e98248ec166f0bca4/src/vue/src/App.vue)

6. Creating the “IntroBox” - our first Vue Component

First we create a component which will introduce users to the Moment. We create a file for the component and call it IntroBox.vue src/vue/src/components/IntroBox.vue

We define the HTML template in the beginning of the file.

We are going to need:

  • an intro text

  • the button description

  • a notification text

  • an image of the prize

  • a heading

  • a describing text

  • a text for the claim-button

Quite a lot - therefore we are going to define a property called “data'' which is going to be an object.

In the beginning we only use the button text and the intro text and take care of the rest later.

In the last step we define the CSS which is specific to this component.https://github.com/moritzschrom/treasure_hunt_moment/commit/55675d1afa12bc6a77045af5bc81f18776caedf1

7. Creating the “Quest” Component

In the next step we define the "Quest-Buttons", which link to the individual questions. In the backend, the Event Organizer should be able to create any number of quests. For each new quest one such button will be generated. Clicking on the button should open a pop-up with :

  • the corresponding question

  • a picture

  • an input for the answer

  • a button to check the answer

For this purpose we create the component Quest.vue and define the property "quest" there directly. With this property you can pass a quest object to use it in the Quest component / Quest Template.

In the mentioned template we first define the button, which will later open the corresponding pop-up. The button text should take the name of the transferred quest object.

https://github.com/moritzschrom/treasure_hunt_moment/commit/a18fce59af4e3577aabf0e8107d18f9e8e7ee065

8. Creating a “Pop-Up” Component

This component creates an empty popup with a "slot" placeholder (https://vuejs.org/v2/guide/components-slots.html) , in which the contents of the popup will be inserted later.

Additionally our popup needs a close button.

With "v-show" we can make the popup visible or hide it by a variable (https://vuejs.org/v2/guide/conditional.html)

To do this, we first define in the script area of the component that the component should not be visible at the beginning (default value).

To show or hide the popup, we still need the methods "show" and "hide".

Afterwards the component specific CSS Part follows.

9. Using the Popup Component inside the Quest Component

To make sure that when you click on the Quest button a pop-up is displayed, we have to include the pop-up component we just created.

For this reason the Quest Button gets a click-event which calls the method "showQuest". Since we need to be able to control the pop-up component via the question component, the included pop-up component gets a ref attribute with the value "questPopup".

We define the method "showQuest" in the methods section of the quest component.

With the ref attribute the methods show() and hide() of the popup component can be called via $refs in the showQuest and hideQuest method to control the visibility of our popups.

Now we fill the popup component with additional HTML elements and insert the values like the name, the page and the image including the link.

Then we create the input field. The input field gets a v-model attribute, so that the component can access the input field.

After the input, the user should now be able to check the entered answer by clicking on a button. For this we create the button "Confirm". If the answer is correct, the quest is marked as "Correct". Additionally we hide the check button and show a "Close" button instead. To control this logic we use the v-show attribute.

https://github.com/moritzschrom/treasure_hunt_moment/commit/170fc206a02799abbee5d24c1743ba8d0b87f2ea

10. Form Validation and Processing

The Confirm button of the Quest component triggers the method submitAnswer, which checks the entered answer. For this we use the attribute @click.

<button @click="submitAnswer" class="button-black quest-submit-button" v-show="!quest.correct">{{ $t('Confirm') }}</button>
<button @click="hideQuest" class="button-black quest-close-button" v-show="quest.correct">{{ $t('Close') }}</button>

For our Moment it is important that we can define several correct answers per question (for example "turquoise" as well as "blue").

To be able to validate typos and inaccurate entries anyway, we use the JavaScript library fuzzyset.js. This library compares the user's answer with the answers we accept and returns a score for the match. If the score is within the tolerance range defined by us, we accept the answer as correct.

We install fuzzyset.js via NPM by executing the following code in the vue folder

npm i -D fuzzyset.js https://github.com/Glench/fuzzyset.js/

To use fuzzyset, we create the variable fs in the data object.

For initialization, once the component is created, we create a new fuzzyset in the method created() with the answers we accept (https://vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks)

In the submitAnswer method we check the input.

For this we call the get method from the fuzzyset and pass three values: the entered answer, false (because there is no default value) and0.75 as tolerance range.

Now we check if the entered answer is within the tolerance range of an accepted solution and take the first best answer we get back from fuzzyset. In this case we set "inputValid" to "true".

Additionally we set the "correct" property of the Quest object to true.

For this we use the method this.$set, because properties in Vue should not be mutated directly.

If the entered answer does not match, we set "inputValid" to false.

To inform the user about the validation of the input, we set either the CSS class "is-valid" or "is-invalid" in the template depending on "inputValid" and display a div with success message or error message. "Is-valid" is also set if a quest has already been answered correctly.

The input field for entering the answer gets an @input handler, which resets the validation on every input by setting this.inputValid to "undefined", so that it can be checked again by clicking on the button.

As soon as an input was correct, we deactivate the input field for the answer.

11. Creating the “Prize-Popup” inside the IntroBox Component

If you click the button “prize” on the homescreen a popup should appear that shows information about the prize. If all quests have been solved successfully another popup appears that displays a button which is linked to a website where the winner can claim the prize. For this functionality we use again the defined component “popup-overlay”, define the ref attribute, which cann be refered to the component.

In the first step we create a html structure for the popup without focusing to much about the logic. We define a success message “win-info”, info about the prize “prize-info” and the button to claim the prize “claim-prize”. A new method to display the popups has to be defined and created as well.

12. Creating Quest Data for Testing Purposes

We add two quest test data in App.vue and then code the Win-Logic.

For this we define the variable "won", which is by default "false" and will be set to "true" when all quests are answered correctly.

In IntroBox.vue we define a new entry point / props named "won" which tells the IntroBox whether it should display the success message (Congratulations, you have won...). The success message should only be displayed if "won" is true. Therefore we check with v-if for "won" == true; only then the element will be rendered.

Apply the same condition to the claim-prize button.

13. Win Detection Logic

To transmit a correct answer within the Quest component to the App component, we need an event. This is created with this.$with in the submitAnswer method within the Quest component.

We will respond to the emitted event in the App component. There we call the method markQuestAsCorrect, which we pass the index of the current quest within the Quest Array.

Of course we now have to define the function markQuestAsCorrect. This function sets the key correct of the object quest at the position i to true. Then we check if we have already successfully completed all quests by calling the method checkWin.

Here we look if:

  • this.data is already initialized

  • whether this.data.quest already exists

  • whether all quests have already been answered

    • Therefore we filter the quest array for quests where the property "correct" is not true. If the length of the filtered array is 0, we know that all quests have already been answered correctly.

https://github.com/moritzschrom/treasure_hunt_moment/commit/5a4c40b1cf98fee537d5d534ae9c75a47048405d

14. Notifying the Backend about a successful Quest

As soon as a quest is answered correctly, we want to inform the backend via API. This way, live statistics can be generated from the data later, which can be displayed on a large screen at the event.

We pass the object userState to the API with an array of the quests completed so far, which contains the name of the quest and the entered answer.

In order to pass the selected answer, we pass the selected answer at this.$with in the Quest component.

In App.vue, where we respond to the emit event, we pass the selected response at $event to the method markQuestAsCorrect.

In the method markQuestAsCorrect we add the chosen answer to the property "answer" of the object Quest at the position i.

Now we have to transform our quest object so that we can pass it to the Moment API. For this we filter out all already answered quests from the array and map the result into the desired format. This way we don't have to pass all data per user to the Stagecast API, which reduces the size of globalState (which contains the user data of all users). Always try to keep the global state as small as possible (see: Tips and Notes).

Then we set the userState with the object we just created, for this we call the method connection.setMomentUserState(data) and pass the array.

15. Loading the saved user state, after Moment is reopened

For this we write the method initUserState in app.vue.

First we request the user state (quest ID, quest name, quest answer) using connection.getMomentUserState and loop through all returned quests. Thereby we check if the respective quest matches the quest from the data object, to be on the safe side we also check the name. If this is the case, we fill the matching quest with the already selected answer from the UserState and mark it with quest.correct = true as already successfully answered.

Then we save the modified quest back into the Data object.

We also add the variable "loading", which can be used later to show visually that the matching of the quests with the UserState is done in the background. If the check is successful, we set the variable "loading" to false. Then we check whether all quests have already been successfully completed by calling the checkWin method.

We add to the method checkWin, that in case of winning, it automatically displays the popup after a few seconds. (https://github.com/moritzschrom/treasure_hunt_moment/commit/d3a2b441dc82806b511cfc00c4e0e1d73bb6e510)

16. Generate the JSON File to automatically generate the Backend Form

To automatically create the backend including all input fields, we need a JSON file, which is used when uploading the moment into the Moment-Store.

For this we need the accepted answers to each quest as a string. So far we have saved them as an array. Accordingly, we need to change the answers with the split method in App.vue.

17. Creating the Result Page

The result page has a similar structure, but instead of a local state it uses the global state. For simplicity we write the result page in plain HTML/JS instead of in Vue.

The top and bottom banner are generated automatically by the Stagecast backend so we don't need to take care of them.

We create a HTML page (src/results/index.html) and include the Stagecast Library again as well as a file called main.js.

Inside this file we create a new instance of the Stagecast SDK, call the onConfigReveiced() method and wait until the Moment SDK has received the configuration.

When the configuration is received, we call the handleOnConfigReceived() method. We then retrieve the Moment class from the SDK and import our custom data using the handleCustomData() method.

After processing the custom data, we call the displayCustomDate() method, which inserts the variables (background, header, etc) into the HTML template.

We then check the global state, which contains the user states of all users who have used the moment. Therefore we call connection.getMomentGlobalState().

We receive an object with the ID as key and its value corresponds to the user state.

We now count the number of quests from each local state we get back and sum them up by determining the length of the returned array of each user. We can do this because only successfully completed quests are written into the user array.

This method is called once by handleOnConfigReceived() and then called every 5 seconds to constantly update the number of quests.

You are done!

At this point the moment should work just like intended. Congratulations on creating your first moment from scratch! 😀

Last updated