mcpField

Form Builder

github.com/eddiebe147ドキュメント46 回インストール
リポジトリ

Form Builder について

Form Builderは、入力可能なフォームやアンケート、インタラクティブな文書を作成するスキルです。PDFやHTML出力に対応し、入力検証、条件分岐、回答処理を扱います。

主な機能

  1. 01入力可能なPDFフォームの作成
  2. 02HTMLフォームの生成
  3. 03条件分岐付きアンケートの作成
  4. 04フィールドの入力検証
  5. 05送信データの処理とエクスポート
  6. 06進捗表示付き複数ページフォーム

活用シーン

  1. 01登録フォームや申請フォームの作成
  2. 02フィードバック調査や質問票の作成
  3. 03フォーム回答の収集と処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/eddiebe147/claude-settings --skill form-builder

インストール後、Claude Code で/form-builderを入力して読み込まれたか確認します。

詳細

フォームビルダー

フォームビルダーのスキルは、入力可能なフォーム、アンケート、質問票、インタラクティブなドキュメントの作成を自動化します。登録フォーム、フィードバックアンケート、申請フォーム、受付フォーム、評価ツールなど、さまざまな種類のフォームに対応しています。検証ルール、条件分岐、データ収集機能に加え、複数の出力形式(入力可能なフィールドを備えたPDF、HTMLフォーム、Google Formsとの連携)をサポートしています。

あらゆるデータ収集ニーズに対応する、適切なフィールドタイプ、検証、スタイル、送信処理を備えたプロフェッショナルなフォームを生成します。

主なワークフロー

ワークフロー 1:入力可能なPDFフォームの作成

目的: インタラクティブな入力可能フィールドを備えたPDFを生成します

手順:

  1. フォームの構成とフィールドを定義する
  2. フィールドタイプを設定する(テキスト、チェックボックス、ラジオボタン、ドロップダウン、日付)
  3. 各フィールドに検証ルールを追加する
  4. フィールドのプロパティを設定する(必須、maxLengthなど)
  5. スタイルとレイアウトを適用する
  6. 説明とヘルプテキストを追加する
  7. 入力可能フィールドを備えたPDFを生成する
  8. フォームの機能をテストする

実装:

const { PDFDocument, PDFTextField, PDFCheckBox, PDFDropdown, rgb } = require('pdf-lib');
const fs = require('fs');

async function createFillablePDFForm(formData, outputPath) {
  const pdfDoc = await PDFDocument.create();
  const page = pdfDoc.addPage([612, 792]); // Letter size

  const { width, height } = page.getSize();
  const fontSize = 11;
  const labelFontSize = 10;

  // Add title
  page.drawText(formData.title, {
    x: 50,
    y: height - 50,
    size: 18,
    color: rgb(0.2, 0.2, 0.2)
  });

  // Add instructions
  if (formData.instructions) {
    page.drawText(formData.instructions, {
      x: 50,
      y: height - 80,
      size: 10,
      color: rgb(0.4, 0.4, 0.4),
      maxWidth: width - 100
    });
  }

  let currentY = height - 120;

  // Add form fields
  for (const field of formData.fields) {
    // Draw field label
    page.drawText(field.label + (field.required ? ' *' : ''), {
      x: 50,
      y: currentY,
      size: labelFontSize,
      color: rgb(0, 0, 0)
    });

    currentY -= 25;

    // Create field based on type
    switch (field.type) {
      case 'text':
      case 'email':
      case 'number':
        const textField = pdfDoc.getForm().createTextField(field.name);
        textField.addToPage(page, {
          x: 50,
          y: currentY,
          width: 300,
          height: 20,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        if (field.placeholder) {
          textField.setText(field.placeholder);
        }
        if (field.maxLength) {
          textField.setMaxLength(field.maxLength);
        }
        break;

      case 'textarea':
        const textArea = pdfDoc.getForm().createTextField(field.name);
        textArea.addToPage(page, {
          x: 50,
          y: currentY - 40,
          width: 500,
          height: 60,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        textArea.enableMultiline();
        currentY -= 40;
        break;

      case 'checkbox':
        field.options.forEach((option, index) => {
          const checkbox = pdfDoc.getForm().createCheckBox(`${field.name}_${index}`);
          checkbox.addToPage(page, {
            x: 50,
            y: currentY - (index * 20),
            width: 12,
            height: 12,
            borderColor: rgb(0.5, 0.5, 0.5),
            borderWidth: 1
          });

          page.drawText(option, {
            x: 70,
            y: currentY - (index * 20) + 2,
            size: 10,
            color: rgb(0, 0, 0)
          });
        });
        currentY -= field.options.length * 20;
        break;

      case 'radio':
        const radioGroup = pdfDoc.getForm().createRadioGroup(field.name);
        field.options.forEach((option, index) => {
          radioGroup.addOptionToPage(option, page, {
            x: 50,
            y: currentY - (index * 20),
            width: 12,
            height: 12,
            borderColor: rgb(0.5, 0.5, 0.5),
            borderWidth: 1
          });

          page.drawText(option, {
            x: 70,
            y: currentY - (index * 20) + 2,
            size: 10,
            color: rgb(0, 0, 0)
          });
        });
        currentY -= field.options.length * 20;
        break;

      case 'dropdown':
        const dropdown = pdfDoc.getForm().createDropdown(field.name);
        dropdown.addOptions(field.options);
        dropdown.addToPage(page, {
          x: 50,
          y: currentY,
          width: 200,
          height: 20,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        break;

      case 'date':
        const dateField = pdfDoc.getForm().createTextField(field.name);
        dateField.addToPage(page, {
          x: 50,
          y: currentY,
          width: 150,
          height: 20,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        dateField.setText('MM/DD/YYYY');
        break;
    }

    // Add help text if provided
    if (field.helpText) {
      page.drawText(field.helpText, {
        x: 370,
        y: currentY + 5,
        size: 8,
        color: rgb(0.6, 0.6, 0.6),
        maxWidth: 200
      });
    }

    currentY -= 40;

    // Add new page if running out of space
    if (currentY < 100) {
      const newPage = pdfDoc.addPage([612, 792]);
      currentY = height - 50;
    }
  }

  // Add submit button placeholder (text)
  page.drawText('Please save and email this completed form to: ' + formData.submitEmail, {
    x: 50,
    y: 50,
    size: 9,
    color: rgb(0.3, 0.3, 0.3)
  });

  const pdfBytes = await pdfDoc.save();
  fs.writeFileSync(outputPath, pdfBytes);

  return outputPath;
}
ワークフロー 2:HTMLフォームの生成

目的: クライアント側の検証機能を備えたインタラクティブなHTMLフォームを作成します

手順:

  1. フォームの構成とフィールドを定義する
  2. HTMLマークアップを生成する
  3. プロフェッショナルな外観になるようCSSスタイルを追加する
  4. JavaScriptによる検証を実装する
  5. フォームの送信を処理する
  6. アクセシビリティ機能を追加する(ARIAラベル、キーボード操作)
  7. モバイルデバイスに対応するレスポンシブデザインにする
  8. 各ブラウザーでテストする

実装:

function generateHTMLForm(formData, outputPath) {
  const html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${formData.title}</title>
  <style>
    * { box-sizing: border-box; }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      max-width: 700px;
      margin: 40px auto;
      padding: 20px;
      background: #f5f5f5;
    }
    .form-container {
      background: white;
      padding: 40px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    h1 {
      color: #333;
      margin-bottom: 10px;
    }
    .instructions {
      color: #666;
      margin-bottom: 30px;
      font-size: 14px;
    }
    .form-group {
      margin-bottom: 25px;
    }
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
      color: #333;
    }
    .required { color: #e74c3c; }
    input[type="text"],
    input[type="email"],
    input[type="number"],
    input[type="tel"],
    input[type="date"],
    textarea,
    select {
      width: 100%;
      padding: 10px 12px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 14px;
      transition: border-color 0.3s;
    }
    input:focus,
    textarea:focus,
    select:focus {
      outline: none;
      border-color: #3498db;
    }
    textarea {
      resize: vertical;
      min-height: 100px;
    }
    .help-text {
      font-size: 12px;
      color: #888;
      margin-top: 5px;
    }
    .checkbox-group,
    .radio-group {
      margin-top: 8px;
    }
    .checkbox-item,
    .radio-item {
      margin-bottom: 8px;
    }
    .checkbox-item input,
    .radio-item input {
      width: auto;
      margin-right: 8px;
    }
    .error {
      color: #e74c3c;
      font-size: 12px;
      margin-top: 5px;
      display: none;
    }
    .error.visible {
      display: block;
    }
    input.invalid,
    textarea.invalid,
    select.invalid {
      border-color: #e74c3c;
    }
    button[type="submit"] {
      background: #3498db;
      color: white;
      padding: 12px 30px;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      transition: background 0.3s;
    }
    button[type="submit"]:hover {
      background: #2980b9;
    }
    button[type="submit"]:disabled {
      background: #bdc3c7;
      cursor: not-allowed;
    }
    .success-message {
      display: none;
      background: #27ae60;
      color: white;
      padding: 15px;
      border-radius: 4px;
      margin-bottom: 20px;
    }
    @media (max-width: 600px) {
      body { padding: 10px; }
      .form-container { padding: 20px; }
    }
  </style>
</head>
<body>
  <div class="form-container">
    <div class="success-message" id="successMessage">
      Form submitted successfully! Thank you.
    </div>

    <h1>${formData.title}</h1>
    ${formData.instructions ? `<p class="instructions">${formData.instructions}</p>` : ''}

    <form id="mainForm" action="${formData.submitUrl || '#'}" method="${formData.method || 'POST'}">
      ${formData.fields.map(field => generateFieldHTML(field)).join('\n')}

      <div class="form-group">
        <button type="submit" id="submitBtn">Submit</button>
      </div>
    </form>
  </div>

  <script>
    ${generateValidationScript(formData)}
  </script>
</body>
</html>
  `;

  fs.writeFileSync(outputPath, html, 'utf8');
  return outputPath;
}

function generateFieldHTML(field) {
  const requiredAttr = field.required ? 'required' : '';
  const requiredLabel = field.required ? '<span class="required">*</span>' : '';

  switch (field.type) {
    case 'text':
    case 'email':
    case 'number':
    case 'tel':
    case 'date':
      return `
        <div class="form-group">
          <label for="${field.name}">${field.label} ${requiredLabel}</label>
          <input
            type="${field.type}"
            id="${field.name}"
            name="${field.name}"
            placeholder="${field.placeholder || ''}"
            ${requiredAttr}
            ${field.maxLength ? `maxlength="${field.maxLength}"` : ''}
            ${field.pattern ? `pattern="${field.pattern}"` : ''}
          >
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">${field.errorMessage || 'This field is required'}</div>
        </div>
      `;

    case 'textarea':
      return `
        <div class="form-group">
          <label for="${field.name}">${field.label} ${requiredLabel}</label>
          <textarea
            id="${field.name}"
            name="${field.name}"
            placeholder="${field.placeholder || ''}"
            ${requiredAttr}
            ${field.maxLength ? `maxlength="${field.maxLength}"` : ''}
          ></textarea>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">This field is required</div>
        </div>
      `;

    case 'select':
      return `
        <div class="form-group">
          <label for="${field.name}">${field.label} ${requiredLabel}</label>
          <select id="${field.name}" name="${field.name}" ${requiredAttr}>
            <option value="">-- Select --</option>
            ${field.options.map(opt => `<option value="${opt}">${opt}</option>`).join('\n')}
          </select>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">Please select an option</div>
        </div>
      `;

    case 'checkbox':
      return `
        <div class="form-group">
          <label>${field.label} ${requiredLabel}</label>
          <div class="checkbox-group">
            ${field.options.map((opt, idx) => `
              <div class="checkbox-item">
                <input type="checkbox" id="${field.name}_${idx}" name="${field.name}" value="${opt}">
                <label for="${field.name}_${idx}">${opt}</label>
              </div>
            `).join('\n')}
          </div>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
        </div>
      `;

    case 'radio':
      return `
        <div class="form-group">
          <label>${field.label} ${requiredLabel}</label>
          <div class="radio-group">
            ${field.options.map((opt, idx) => `
              <div class="radio-item">
                <input type="radio" id="${field.name}_${idx}" name="${field.name}" value="${opt}" ${requiredAttr}>
                <label for="${field.name}_${idx}">${opt}</label>
              </div>
            `).join('\n')}
          </div>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">Please select an option</div>
        </div>
      `;

    default:
      return '';
  }
}

function generateValidationScript(formData) {
  return `
    const form = document.getElementById('mainForm');
    const submitBtn = document.getElementById('submitBtn');
    const successMessage = document.getElementById('successMessage');

    // Validation rules
    const validationRules = {
      email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,
      phone: /^[\\d\\s\\-\\(\\)]+$/
    };

    // Real-time validation
    form.addEventListener('input', (e) => {
      validateField(e.target);
    });

    // Form submission
    form.addEventListener('submit', async (e) => {
      e.preventDefault();

      // Validate all fields
      let isValid = true;
      const fields = form.querySelectorAll('input, textarea, select');

      fields.forEach(field => {
        if (!validateField(field)) {
          isValid = false;
        }
      });

      if (!isValid) {
        return;
      }

      // Disable submit button
      submitBtn.disabled = true;
      submitBtn.textContent = 'Submitting...';

      try {
        // Collect form data
        const formData = new FormData(form);
        const data = Object.fromEntries(formData);

        // Submit to server
        ${formData.submitUrl ? `
        const response = await fetch('${formData.submitUrl}', {
          method: '${formData.method || 'POST'}',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });

        if (response.ok) {
          successMessage.style.display = 'block';
          form.reset();
        } else {
          alert('Submission failed. Please try again.');
        }
        ` : `
        // No submit URL - just show success
        console.log('Form data:', data);
        successMessage.style.display = 'block';
        form.reset();
        `}

      } catch (error) {
        alert('An error occurred. Please try again.');
        console.error(error);
      } finally {
        submitBtn.disabled = false;
        submitBtn.textContent = 'Submit';
      }
    });

    function validateField(field) {
      const errorEl = document.getElementById(field.name + '-error');
      if (!errorEl) return true;

      let isValid = true;
      let errorMessage = '';

      // Required check
      if (field.hasAttribute('required') && !field.value.trim()) {
        isValid = false;
        errorMessage = 'This field is required';
      }

      // Email validation
      if (field.type === 'email' && field.value && !validationRules.email.test(field.value)) {
        isValid = false;
        errorMessage = 'Please enter a valid email address';
      }

      // Pattern validation
      if (field.hasAttribute('pattern') && field.value) {
        const pattern = new RegExp(field.getAttribute('pattern'));
        if (!pattern.test(field.value)) {
          isValid = false;
          errorMessage = 'Please enter a valid value';
        }
      }

      // Update UI
      if (isValid) {
        field.classList.remove('invalid');
        errorEl.classList.remove('visible');
      } else {
        field.classList.add('invalid');
        errorEl.textContent = errorMessage;
        errorEl.classList.add('visible');
      }

      return isValid;
    }
  `;
}
ワークフロー 3:アンケート/質問票ジェネレーター

目的: 複数の質問タイプとロジックを備えた包括的なアンケートを作成します

手順:

  1. アンケートの構成とセクションを定義する
  2. さまざまな質問タイプを追加する(複数選択、評価、テキストなど)
  3. 条件分岐ロジックを実装する(スキップパターン)
  4. 検証ルールを追加する
  5. 回答の選択肢と尺度を設定する
  6. 結果の収集と分析方法を設計する
  7. 希望する形式でアンケートを生成する
  8. 回答の追跡を設定する

実装:

async function generateSurvey(surveyData, outputPath) {
  const survey = {
    title: surveyData.title,
    description: surveyData.description,
    sections: surveyData.sections.map(section => ({
      title: section.title,
      questions: section.questions.map((q, idx) => ({
        id: `q${section.id}_${idx + 1}`,
        type: q.type,
        text: q.text,
        required: q.required !== false,
        options: q.options,
        validation: q.validation,
        conditionalLogic: q.conditionalLogic
      }))
    })),
    settings: {
      allowAnonymous: surveyData.allowAnonymous !== false,
      showProgress: surveyData.showProgress !== false,
      randomizeQuestions: surveyData.randomizeQuestions || false,
      allowMultipleSubmissions: surveyData.allowMultipleSubmissions || false
    }
  };

  // Generate HTML survey
  const html = generateSurveyHTML(survey);
  fs.writeFileSync(outputPath, html, 'utf8');

  return {
    surveyPath: outputPath,
    questionCount: survey.sections.reduce((sum, s) => sum + s.questions.length, 0),
    settings: survey.settings
  };
}

function generateSurveyHTML(survey) {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${survey.title}</title>
  <style>
    /* Survey-specific styles */
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
      background: #f9f9f9;
    }
    .survey-container {
      background: white;
      padding: 40px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    .progress-bar {
      height: 8px;
      background: #e0e0e0;
      border-radius: 4px;
      margin-bottom: 30px;
    }
    .progress-fill {
      height: 100%;
      background: #4CAF50;
      border-radius: 4px;
      transition: width 0.3s;
    }
    .section {
      margin-bottom: 40px;
    }
    .section-title {
      font-size: 20px;
      font-weight: bold;
      margin-bottom: 20px;
      color: #333;
    }
    .question {
      margin-bottom: 30px;
      padding: 20px;
      background: #f5f5f5;
      border-radius: 6px;
    }
    .question-text {
      font-size: 16px;
      margin-bottom: 15px;
      color: #333;
    }
    .required-indicator {
      color: #e74c3c;
    }
    .rating-scale {
      display: flex;
      gap: 10px;
      justify-content: center;
      margin: 20px 0;
    }
    .rating-option {
      width: 50px;
      height: 50px;
      border: 2px solid #ddd;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      transition: all 0.3s;
    }
    .rating-option:hover {
      border-color: #4CAF50;
      background: #f0f0f0;
    }
    .rating-option.selected {
      background: #4CAF50;
      color: white;
      border-color: #4CAF50;
    }
    button.submit-survey {
      background: #4CAF50;
      color: white;
      padding: 15px 40px;
      border: none;
      border-radius: 6px;
      font-size: 16px;
      cursor: pointer;
      width: 100%;
      margin-top: 20px;
    }
    button.submit-survey:hover {
      background: #45a049;
    }
  </style>
</head>
<body>
  <div class="survey-container">
    <h1>${survey.title}</h1>
    <p>${survey.description}</p>

    ${survey.settings.showProgress ? `
    <div class="progress-bar">
      <div class="progress-fill" id="progressFill" style="width: 0%"></div>
    </div>
    ` : ''}

    <form id="surveyForm">
      ${survey.sections.map(section => `
        <div class="section">
          <h2 class="section-title">${section.title}</h2>
          ${section.questions.map(q => generateQuestionHTML(q)).join('\n')}
        </div>
      `).join('\n')}

      <button type="submit" class="submit-survey">Submit Survey</button>
    </form>
  </div>

  <script>
    // Survey logic and submission
    ${generateSurveyScript(survey)}
  </script>
</body>
</html>
  `;
}

function generateQuestionHTML(question) {
  const requiredMark = question.required ? '<span class="required-indicator">*</span>' : '';

  switch (question.type) {
    case 'multipleChoice':
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          ${question.options.map((opt, idx) => `
            <div>
              <input type="radio" id="${question.id}_${idx}" name="${question.id}" value="${opt}" ${question.required ? 'required' : ''}>
              <label for="${question.id}_${idx}">${opt}</label>
            </div>
          `).join('\n')}
        </div>
      `;

    case 'rating':
      const scale = question.scale || 5;
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          <div class="rating-scale">
            ${Array.from({ length: scale }, (_, i) => i + 1).map(num => `
              <div class="rating-option" data-value="${num}" onclick="selectRating('${question.id}', ${num})">
                ${num}
              </div>
            `).join('\n')}
          </div>
          <input type="hidden" name="${question.id}" id="${question.id}" ${question.required ? 'required' : ''}>
        </div>
      `;

    case 'text':
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          <textarea name="${question.id}" rows="4" style="width: 100%; padding: 10px;" ${question.required ? 'required' : ''}></textarea>
        </div>
      `;

    case 'checkbox':
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          ${question.options.map((opt, idx) => `
            <div>
              <input type="checkbox" id="${question.id}_${idx}" name="${question.id}" value="${opt}">
              <label for="${question.id}_${idx}">${opt}</label>
            </div>
          `).join('\n')}
        </div>
      `;

    default:
      return '';
  }
}

function generateSurveyScript(survey) {
  return `
    function selectRating(questionId, value) {
      // Remove previous selection
      const container = document.querySelector(\`[data-question-id="\${questionId}"]\`);
      container.querySelectorAll('.rating-option').forEach(opt => {
        opt.classList.remove('selected');
      });

      // Add new selection
      event.target.classList.add('selected');
      document.getElementById(questionId).value = value;
    }

    // Form submission
    document.getElementById('surveyForm').addEventListener('submit', async (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const responses = Object.fromEntries(formData);

      console.log('Survey responses:', responses);

      // Submit to server or save locally
      alert('Thank you for completing the survey!');
      e.target.reset();
    });

    // Progress tracking
    ${survey.settings.showProgress ? `
    const questions = document.querySelectorAll('.question');
    const progressFill = document.getElementById('progressFill');

    document.addEventListener('input', updateProgress);

    function updateProgress() {
      let answered = 0;
      questions.forEach(q => {
        const inputs = q.querySelectorAll('input, textarea');
        const hasAnswer = Array.from(inputs).some(input => {
          if (input.type === 'radio' || input.type === 'checkbox') {
            return input.checked;
          }
          return input.value.trim() !== '';
        });
        if (hasAnswer) answered++;
      });

      const progress = (answered / questions.length) * 100;
      progressFill.style.width = progress + '%';
    }
    ` : ''}
  `;
}
ワークフロー 4:フォームデータの処理

目的: フォームの送信を処理し、収集したデータを処理します

手順:

  1. 送信エンドポイントを設定する
  2. 送信されたデータを検証する
  3. 入力を処理してサニタイズする
  4. データベースまたはスプレッドシートにデータを保存する
  5. 確認メールを送信する
  6. 概要レポートを生成する
  7. 分析用にデータをエクスポートする
ワークフロー 5:進捗表示付き複数ページフォーム

目的: 進捗を追跡しながら、長いフォームを複数ページに分割して作成します

手順:

  1. フォームを論理的なセクションに分ける
  2. ページ間のナビゲーションを作成する
  3. 進捗バーを実装する
  4. ページ間で進捗を保存する
  5. 各ステップで検証を追加する
  6. 前のページに戻って編集できるようにする
  7. 最後にすべてのデータを送信する

クイックリファレンス

アクションコマンド/トリガー
入力可能なPDFフォーム"create fillable PDF form"
HTMLフォーム"generate HTML form with [fields]"
アンケート"build survey about [topic]"
お問い合わせフォーム"create contact form"
登録フォーム"make registration form"
フィードバックフォーム"generate feedback survey"
複数ステップのフォーム"create multi-page form"

ベストプラクティス

  • わかりやすいラベル: すべてのフィールドに、内容が明確で簡潔なラベルを使用する
  • 論理的なグループ分け: 関連するフィールドをまとめる
  • 必須フィールド: アスタリスクを付けて必須フィールドを明確に示す
  • ヘルプテキスト: 複雑なフィールドにはガイダンスを用意する
  • 検証: クライアント側とサーバー側の両方で検証を実装する
  • エラーメッセージ: 明確で役に立つエラーメッセージを表示する
  • アクセシビリティ: ARIAラベルとキーボード操作を取り入れる
  • モバイル対応: すべてのデバイスでフォームが機能することを確認する
  • 進捗インジケーター: 長いフォームでは進捗を表示する
  • 進捗の保存: ユーザーが進捗を保存し、後から再開できるようにする
  • 確認: 送信後に成功メッセージを表示する
  • セキュリティ: CSRF対策を使用し、入力をサニタイズする

よくあるパターン

お問い合わせフォーム:

{
  fields: [
    { name: 'name', type: 'text', label: 'Full Name', required: true },
    { name: 'email', type: 'email', label: 'Email', required: true },
    { name: 'phone', type: 'tel', label: 'Phone Number' },
    { name: 'subject', type: 'text', label: 'Subject', required: true },
    { name: 'message', type: 'textarea', label: 'Message', required: true }
  ]
}

イベント登録:

{
  fields: [
    { name: 'attendeeName', type: 'text', required: true },
    { name: 'email', type: 'email', required: true },
    { name: 'ticketType', type: 'select', options: ['General', 'VIP'], required: true },
    { name: 'dietaryRestrictions', type: 'checkbox', options: ['Vegetarian', 'Vegan', 'Gluten-Free'] },
    { name: 'specialRequests', type: 'textarea' }
  ]
}

依存関係

必要なパッケージをインストールします:

npm install pdf-lib        # For PDF forms
npm install express        # For form submission handling
npm install validator      # For input validation
npm install nodemailer     # For email confirmations

エラー処理

  • 検証エラー: フィールドごとの具体的なエラーを表示します
  • ネットワークエラー: 送信の失敗を適切に処理します
  • データ損失: データの損失を防ぐため、進行状況を自動保存します
  • ブラウザーの互換性: さまざまなブラウザーでテストします

高度な機能

条件付きロジック:

conditionalLogic: {
  showIf: { field: 'employment', value: 'employed' },
  hideIf: { field: 'student', value: 'yes' }
}

ファイルアップロード:

{
  name: 'resume',
  type: 'file',
  accept: '.pdf,.doc,.docx',
  maxSize: 5242880 // 5MB
}

自動保存:

setInterval(() => {
  const formData = new FormData(form);
  localStorage.setItem('formDraft', JSON.stringify(Object.fromEntries(formData)));
}, 30000);

コメント