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
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.
```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:
```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.
```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).
```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:
- The main page with a list of quizzes and the number of participants for each
- A quiz page that can have two states (user voted / not voted)
- 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
```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.
```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.
```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:
```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.
```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>
<!--
{#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:
```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.
```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.
```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.
```svelte
<script>
export let quizzes;
export let page;
</script>
```
The last step for this page is the markup description.
```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
```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