Project Archive

MySQL database files

Creating Surveys and Questions

The previous version of our Next.js app made it possible to view existing surveys and their questions. The next version we will create will include tools needed to create new surveys and their associated questions.

New data actions

The first new feature in this version will be a set of data actions that we can use to store newly created surveys and their questions in the database.

Here is a set of new data actions I created for this purpose:

export async function addQuestion(surveyid:number,question:string,responses:string) {
    const sql = 'INSERT INTO question(survey,question,responses) VALUES (?,?,?)';
    const values = [surveyid,question,responses];

    await conn.execute(sql,values);
}

export async function createSurvey(title:string,prompt:string) : Promise<number> {
    const sql = 'INSERT INTO survey(title,prompt) VALUES (?,?)';
    const values = [title,prompt];

    const [result] = await conn.execute<ResultSetHeader>(sql,values);
    const insertId = result.insertId;

    return insertId;
}

SQL select statements require us to use the connection's query() method. For SQL insert statements we will instead use execute().

One small technical problem I had to solve here was determining the id number of a newly created survey. To solve this problem I took advantage of the fact that execute() returns a ResultSetHeader object to us. That object contains an insertId property that will be set equal to the id number of the newly inserted item.

Forms and server actions

To create a new survey the user will need to supply a title and a prompt for that survey. The preferred way to gather that kind of information from the user in a Next.js application is to use a form. HTML form elements require an action attribute. Next.js allows us to set up special functions, called server actions, that can be passed as an action to a form.

Here is how all of this works in the case of creating a new survey. I set up a /create route and supplied it with the following page code:

import { createSurvey } from '@/app/lib/data';
import { redirect } from 'next/navigation';
import SurveyCreator from '@/app/ui/surveycreator';

export default function Page() {

  async function addSurvey(formData : FormData) { 
    'use server'
     const title = formData.get('title') as string;
     const prompt = formData.get('prompt') as string;
     const id = await createSurvey(title,prompt);
     redirect('/edit/'+id);
  }
     
  return (
    <SurveyCreator adder={addSurvey} />
  );
}

The page component I defined here is a client-side component. Embeded in the client component is a server action, addSurvey(), which is intended to be run on the server side. To indicate that addSurvey() is a server action I have added the special 'use server' declaration to the body of that function.

This code passes the addSurvey() function as a prop to another client-side component, the SurveyCreator component. Here is the code for that component:

export default function SurveyCreator({
    adder
  }: {
    adder : (data:FormData) => void
  }) {
    return (
      <form action={adder}>
        <p>Survey Title: <input type="text" name="title" /></p>
        <p>Survey Prompt: <input type="text" name="prompt" /></p>
        <p><button type="submit">Create Survey</button></p>
      </form>
    );
  }

This component will receive our server action function as a prop, and then turn right around and use that function as the action function for a form element.

To make our server action function as a proper action function for a form element, we have to equip our server action function with a parameter of type FormData that will receive the input data from the form. The FormData class has a get() method that will allow us to retrieve the values entered by the user by name.

Since our action function is designed to run on the server side, it is ok for it to access any data actions it needs to access. In particular, it can invoke the createSurvey() data action to insert the new survey into the database.

The last thing that our server action does is to call the Next.js redirect() function, which tells the application to navigate to another page where the user can edit the survey's questions.

Setting up questions for a survey

The /edit/[surveyid] route is a dynamic route that takes us to a page to edit the questions for a survey. Here is the code for that route's page.tsx:

import { Suspense } from 'react';
import Loading from '@/app/ui/loading';
import SurveyDetails from '@/app/ui/surveydetails';
import QuestionsEditor from '@/app/ui/questionseditor'
import QuestionCreator from '@/app/ui/questioncreator'
import { fetchQuestions, removeQuestion, addQuestion } from '@/app/lib/data';
import { redirect } from 'next/navigation';

export default async function SurveyPage({
    params,
  }: {
    params: Promise<{ surveyid: number }>
  }) {
    const { surveyid } = await params;
    const questions = await fetchQuestions(surveyid);

    async function handleRemove(questionid: number) {
        'use server'
        removeQuestion(questionid);
        redirect('/edit/'+surveyid);
    };

    async function handleCreate(formData: FormData){
        'use server'
        const question = formData.get('question') as string;
        const responses = formData.get('responses') as string;
        addQuestion(surveyid,question,responses);
        redirect('/edit/'+surveyid);
    }

    return (
          <Suspense fallback={<Loading/>}>
            <SurveyDetails surveyid={surveyid} />
            <QuestionsEditor questions={questions} remover={handleRemove}/>
            <QuestionCreator adder={handleCreate}/>
          </Suspense>
        );
  }

The QuestionCreator component displays a form where the user can enter a question for a survey. The handleCreate() server action defined here will serve as the action for that form.

I have set up a second server action here that will be used in the QuestionsEditor component. Here is the code for that component:

'use client'
import { Question } from '@/app/lib/data'
import QuestionDisplay from '@/app/ui/questiondisplay';

export default function QuestionsEditor({
    questions,
    remover
  }: {
    questions : Question[];
    remover : (id:number) => void
  }) {
    return (
        <div>
           { questions.map((q) => {
                return (
                <div>
                <QuestionDisplay question={q} />
                <button onClick={()=>remover(q.idquestion)}>Remove</button>
                </div>) }
           )
            }
        </div>
    );
}

This component receives two props, a list of question objects and a remover action that we can invoke to remove a question from the database. For each question object in the list we will display that question and then put a 'Remove' button next to the question. The onClick function for that button will invoke the remover action to remove the question from its survey.