Click here to Skip to main content
15,885,767 members
Articles / Mobile Apps

Creating a Quiz Application With Sapper and Prisma. Part 2: Frontend

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
23 Apr 2020CPOL7 min read 6.2K  
Find out how to easily create a single-page application using Sapper, Svelte and Prisma
This article shows that the combination of Sapper, Svelte and Prisma allows for quick full-stack prototyping. Using the Sapper framework and Prisma, we will create a full-fledged quiz application that allows users to pass quizzes, receive and share results, as well as create their own quizzes.

Introduction

In the first part of this article, we considered the problems of full-stack application development, along with the use of Prisma and Sapper as a potential solution.

Sapper is a framework that allows for writing applications of different sizes with Svelte. It internally launches a server that distributes a Svelte application with optimizations and SEO-friendly rendering.

Prisma is a set of utilities for designing and working with relational databases using GraphQL. In Part 1, we deployed the environment for Prisma-server and integrated it with Sapper.

In Part 2, we will continue developing the quiz application code on Svelte and Sapper, and integrate it with Prisma.

Disclaimer

We will intentionally not implement the authorization/registration of quiz participants, but we assume this may be important under some scenarios. Instead of registering/authorizing clients, we will use sessions. To implement session support, we need to update the src / server.js file.

For smooth operation, we also need to install additional dependencies:

Bash
```bash
yarn add graphql-tag express-session node-fetch
```

We add session middleware to the server. This must be done prior to calling Sapper middleware. Also, the sessionID must be passed to Sapper. Sessions will be used when loading the Sapper application. For the API routes, session processing may be abolished.

JavaScript
```js
server.express.use((req, res, next) => {
if (/^\/(playground|graphql).*/.test(req.url)) {
     return next();
}

session({
   secret: 'keyboard cat',
   saveUninitialized: true,
   resave: false,
})(req, res, next)
});

server.express.use((req, res, next) => {
  if (/^\/(playground|graphql).*/.test(req.url)) {
return next();
  }

  return sapper.middleware({
session: (req) => ({
   id: req.sessionID
})
  })(req, res, next);
});
```

In Part 1, we considered the data model for Prisma, which must be updated for further work. The final version is as follows:

JavaScript
```graphql
type QuizVariant {
  id: ID! @id
  name: String!
  value: Int!
}

type Quiz {
  id: ID! @id
  title: String!
  participantCount: Int!
  variants: [QuizVariant!]! @relation(link: TABLE, name: "QuizVariants")
}

type SessionResult {
id: ID! @id
sessionId: String! @id
quiz: Quiz!
variant: QuizVariant
}
```

In the updated data model, quiz results are shifted to the QuizVariant type. The SessionResult type stores results in a particular session’s quiz.

Next, we need to update the src / client / apollo.js file using server-side rendering. We forward the ApolloClient link to the fetch function, since fetch is not available in Node.js runtime.

JavaScript
```js
import ApolliClient from 'apollo-boost';
import fetch from "node-fetch";

const client = new ApolliClient({
uri: 'http://localhost:3000/graphql',
fetch,
});

export default client;
```

Since this is a frontend application, we will use TailwindCSS and FontAwesome, connected via CDN to simplify the markup.

To simplify support for resolver logic, we shift all the resolvers to a single file (src / resolvers / index.js).

JavaScript
```js
export const quizzes = async (parent, args, ctx, info) => {
return await ctx.prisma.quizzes(args.where);
}

export default {
Query: {
     quizzes,
},

Mutation: {}
}

```

Requirements

Earlier, we provided the application data model in the GraphQL schema. But it is very abstract and doesn't show how the application should work and what screens and components it consists of. To move on, we need to consider these issues in more detail.

The main purpose of the application is quizzing. Users can create quizzes, pass them, receive and share results. Based on this functionality, we need the following pages in the application:

  1. The main page with a list of quizzes and the number of participants for each
  2. A quiz page that can have two states (user voted / not voted)
  3. A quiz creation page

Let's get started.

Quiz Creation Page

We need to create quizzes to be displayed on the main page. For this reason, we start from the third point in our plan.

On the quiz creation page, a user should be able to enter answer options and a quiz name via a special form.

Let's assemble the model that will store the data entered via the form. We will use regular JavaScript for this. The model consists of two variables, newQuiz and newVariant:

  • newQuiz is the object sent to the server to be saved in the database
  • newVariant is the string storing new answer options
JavaScript
```js
const newQuiz = {
title: "",
variants: []
};

let newVariant = "";

let canAddQuiz = false;
// a variable that is recalculated only if `newQuiz.variants` or `newQuiz.title` changes
$: canAddQuiz = newQuiz.title === "" || newQuiz.variants.length === 0;
```

Next, we create two functions for adding and removing answer options in the quiz model. These are regular JavaScript functions that redefine the variants field of the newQuiz model.

JavaScript
```js
function addVariant() {
newQuiz.variants = [
     ...newQuiz.variants,
     { name: newVariant, value: 0 }
];

// after adding
newVariant = "";
}

function removeVariant(i) {
return () => {
     newQuiz.variants = newQuiz.variants.slice(0, i).concat(newQuiz.variants.slice(i + 1))
};
}
```

These functions transform the model according to a certain logic. But how does the representation level (Svelte) know about these changes?

All the Svelte component code passes the Ahead of Time compilation stage during assembly and is transpiled to optimized JS.

The variables in the Svelte component become "reactive," and any changes to the variables do not affect the direct value, but create an event for the change of this value. After that, everything that depends on the changed variables is recalculated or redrawn in the DOM.

The next step is to send the request to the Prisma-server. At this point, we will use the previously defined instance of apollo-client.

JavaScript
```js
import gql from 'graphql-tag';
import { goto } from '@sapper/app';
import client from "../client/apollo";

//  request template
const createQuizGQLTag = gql`
  mutation CreateQuiz($data: QuizCreateInput!) {
    createQuiz(data: $data) {
      id
    }
  }
`;

async function publishQuiz() {
await client.mutate({
     mutation: createQuizGQLTag,

     variables: {
         "data": {
             "title": newQuiz.title,
                "participantCount": 0,
             "variants": {
                 "create": newQuiz.variants
             },
             "result": {}
         }
     }
});

goto("/");
}
```

To send a request to the GraphQL server, we defined a request template and a function that causes mutation on the GraphQL server, using the request template and the data that is stored in the newQuiz variable as query variables.

If we try to send a request now, the attempt would be unsuccessful, since we haven't set up a quiz creation handler on the server.

We add the resolver for createQuiz mutation to src / resolvers / index.js:

JavaScript
```js
...
const createQuiz = async (parent, args, ctx) => {
return await ctx.prisma.createQuiz(args.data);
}

export default {
...

Mutation: {
     createQuiz
}
}
```

Everything is ready for the interface to work correctly, and it is only necessary to create the markup of the form.

HTML
```svelte
<form class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div class="mb-4">
     <label class="block text-gray-700 text-sm font-bold mb-2" for="quiz-title">
         Quiz title
     </label>
     <input
             class="shadow appearance-none border rounded w-full py-2 
             px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                id="quiz-title"
                type="text"
             placeholder="Quiz title"
                bind:value={newQuiz.title}
     >
</div>

<div class="mb-4">
     <label class="block text-gray-700 
     text-sm font-bold mb-2" for="new-variant">
         New Variant
     </label>
     <input
             class="shadow appearance-none border rounded w-full py-2 px-3 
             text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                id="new-variant"
                type="text"
             placeholder="New Variant"
                bind:value={newVariant}
     >
</div>

<div class="mb-4">
     <label class="block text-gray-700 text-sm font-bold mb-2">
         Variants
     </label>

     <!-- We display a list of quiz options or 
          specify that options have not yet been added -->
     {#each newQuiz.variants as { name }, i}
         <li>
             {name}
             <i class="cursor-pointer fa fa-close" on:click={removeVariant(i)} />
         </li>
     {:else}
         No options have been added
     {/each}
</div>

<div class="flex items-center justify-between">
     <button
             class="bg-blue-500 hover:bg-blue-700 text-white font-bold 
             py-2 px-4 rounded focus:outline-none focus:shadow-outline"
                type="button"
                on:click={addVariant}
     >
         <i class="fa fa-plus" /> Add variant
     </button>
     <button
                class="bg-green-500 hover:bg-blue-700 text-white font-bold 
                py-2 px-4 rounded focus:outline-none focus:shadow-outline"
                type="button"
                on:click={publishQuiz}
                bind:disabled={canAddQuiz}
     >
            Create New
     </button>
</div>
</form>
```

In general, this is simple HTML markup using the TailwindCSS utility class namespace. In this template, we defined the # each iterator, which renders a list of quiz options. If the list is empty, a notice that quiz options have not yet been added is displayed.

Svelte uses on: whatever attributes to process events. In this template, we handle clicks on the add option button and the quiz creation button. Respectively, the addVariant and publishQuiz functions work as handlers.

In the text input fields, we use the bind: value attribute to bind the reactive variables of our component and the value attribute of text fields. Also, bind: disabled is used to determine the disabled state of the quiz creation button.

Now, as the form and the code processing events are ready, we can fill out the form, click on the button, and send the request to the GraphQL server. Once the request is executed, a redirect to the main page will be made and the following text will be displayed: Quizzes in Service: 1. This means that we've successfully added a quiz.

Author's note: In this example, I intentionally avoid client validations, so as not to distract readers from the main topic of the article. Nevertheless, validations are necessary and very important for real applications.

Main Page

We need to display the created quizzes on the main page. With a large number of requests, each of them takes a long time to process and rendering is not nearly as fast as we would like. For this reason, pagination should be used on the main page.

This is easy to reflect in a GraphQL request:

JavaScript
```graphql
query GetQuizes($skip: Int, $limit: Int) {
  quizzes(skip: $skip, first: $limit) {
id,
title,
participantCount
  }
}
```

In this request, we define two variables: $ skip and $ limit. They are sent to the quizzes request. These variables are already defined there, in the scheme generated by `Prisma`.

Next, we need to pass these variables to the src / resolvers / index.js request resolver.

JavaScript
```js
export const quizzes = async (parent, args, ctx) => {
return await ctx.prisma.quizzes(args);
}
```

Now requests from the application can be made. We update the preload logic in the `src / routes / index.svelte` route.

JavaScript
```svelte
<script context="module">
  import gql from 'graphql-tag';
  import client from "../client/apollo";

  const GraphQLQueryTag = gql`
query GetQuizes($skip: Int, $limit: Int) {
   quizzes(skip: $skip, first: $limit) {
     id,
     title,
     participantCount
   }
}
  `;

  const PAGE_LIMIT = 10;

  export async function preload({ query }) {
const { page } = query;

const queriedPage = page ? Number(page) : 0;

const {
   data: { quizzes }
} = await client.query({ query: GraphQLQueryTag, variables: {
     limit: PAGE_LIMIT,
     skip: PAGE_LIMIT * Number(queriedPage)
   }
});

return {
   quizzes,
   page: queriedPage,
};
  }
</script>
```

In the preload function, we define an argument with the query field. The request parameters will be sent there. We can determine the current page through a URL in the following way: http://localhost:3000?Page=123.

The page limit can be changed by overriding the PAGE_LIMIT variable.

JavaScript
```svelte
<script>
  export let quizzes;
  export let page;
</script>
```

The last step for this page is the markup description.

JavaScript
```svelte
<svelte:head>
  <title>Quiz app</title>
</svelte:head>

{#each quizzes as quiz, i}
  <a href="quiz/{quiz.id}">
<div class="flex flex-wrap bg-white border-b border-blue-tial-100 p-5">
   {i + 1 + (page * PAGE_LIMIT)}.
   <span class="text-blue-500 hover:text-blue-800 ml-3 mr-3">
     {quiz.title}
   </span>
   ({quiz.participantCount})
</div>
  </a>
{:else}
<div class="text-2xl">No quizzes was added :(</div>

<div class="mt-3">
   <a
           class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 
           px-4 rounded focus:outline-none focus:shadow-outline"
              href="create"
   >
     Create new Quiz
   </a>
</div>
  {/each}

{#if quizzes.length === PAGE_LIMIT}
  <div class="mt-3">
<a
         class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 
         px-4 rounded focus:outline-none focus:shadow-outline"
         href="?page={page + 1}"
>
   more...
</a>
  </div>
{/if}
```

Navigation between pages is implemented using regular links. The only difference is that the route name or parameters are passed to the href attribute instead of the source address.

Quiz Page

Now we need to develop the most important page of the application. The quiz page has to provide answer options. Also, results and selected options have to be displayed to those who have already passed the quiz.

Following the Sapper convention, we create the src / routes / quiz / [id] .svelte route file.

In order for the quiz to be displayed correctly, we need to create a GraphQL request that will return:

  • request
  • the results by each option of the quiz
  • id of the answer option if the client has already participated in the quiz
JavaScript
```svelte
<script context="module">
import gql from 'graphql-tag';
import client from "../../client/apollo";

const GraphQLQueryTag = gql`
query GetQuiz($id: ID!, $sessionId: String!) {
   quiz(where: { id: $id }) {
     id,
     title,
     participantCount,
     variants {
         id,
         name,
         value
     }
   }

   sessionResults(where: { quiz: { id: $id }, sessionId: $sessionId}, first: 1) {
     variant {
         id
     }
   }
}
`;

export async function preload({ params }, session) {
const { id } = params;

const {
     data: { quiz, sessionResults: [sessionResult] }
} = await client.query({ query: GraphQLQueryTag, variables: {
         id,
         sessionId: session.id
     }
});

return {
     id,
     quiz,
     sessionId: session.id,
     sessionResult
}
}
</script>
```

Conclusion

This article shows that the combination of Sapper, Svelte and Prisma allows for quick full-stack prototyping. Using the Sapper framework and Prisma, we created a full-fledged quiz application that allows users to pass quizzes, receive and share results, as well as create their own quizzes.

After future refinements, this application could support real-time mechanics, since Prisma supports notifications about real-time database changes. And these are not all possibilities of the stack.

The article was written in collaboration with software developers at Digital Clever Solutions and Opporty.

History

  • 22nd April, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Fuente Labs LLC
United States United States
Sergii Grybniak works on Research and Development on multiple levels in the field of Distributed Ledger Technologies. Some of the publications here are a result of joint efforts of R&D teams of Fuente Labs LCC and Digital Clever Solutions LLC.

Comments and Discussions

 
-- There are no messages in this forum --