Building a Bulk Data Import Modal in Vue 3: A Prescription App Case Study

Vue 3 JSON import modal component - data validation and bulk import pattern

When building the Pharmacokinetics Grapher—an educational web app for visualizing medication concentration curves—I faced a common problem: users needed to input multiple drug prescriptions, but typing each one manually into a form was tedious and error-prone.

The solution? A Vue 3 JSON import modal—a reusable component that lets users paste structured data and validates everything before importing. Here’s how I built it, what I learned, and the iterative process that got there.

The Problem: Manual Data Entry vs. Bulk Import

The Pharmacokinetics Grapher requires detailed pharmacokinetic (PK) parameters for each drug:

  • Drug name
  • Dose per administration
  • Dosing frequency (bid, tid, q6h, etc.)
  • Dosing times (HH:MM format)
  • Half-life (hours)
  • Time to peak concentration (Tmax, hours)
  • Absorption time (uptake, hours)
  • Optional metabolite half-life

Filling out 8+ fields for each drug is fine for one prescription. But for testing, comparison, or educational demonstrations, users wanted to import multiple drugs at once. Manually entering acetaminophen, ibuprofen, amoxicillin, and sertraline? That’s repetitive and frustrating.

The Journey: From Requirements to Implementation

Phase 1: Clarifying the Ask

I started by asking: Does the user want real medication data or synthetic test data?

The answer: real data. So I researched actual PK parameters from pharmacy references and created a reference database:

| Medication | Dose | Frequency | Half-Life | Tmax | Uptake |
|----------|------|-----------|-----------|------|--------|
| Acetaminophen | 500mg | q6h | 2-3h | 0.5-1h | 0.5h |
| Ibuprofen | 200-400mg | q6-8h | 2h | 1-2h | 0.5-1h |
| Amoxicillin | 500mg | tid | 1h | 1-2h | 0.5h |
| Sertraline | 50mg | once daily | 26h | 5.5h | 1-2h |

With real data in hand, I could now design the import system.

Phase 2: Designing the JSON Format

I needed a JSON schema that was:

  • Simple — users can paste it easily
  • Flexible — handles optional fields (metabolite half-life)
  • Structured — validates against the same rules as the form

The result:

{
  "prescriptions": [
    {
      "name": "Acetaminophen (Tylenol)",
      "dose": 500,
      "frequency": "q6h",
      "times": ["06:00", "12:00", "18:00", "00:00"],
      "halfLife": 2.5,
      "peak": 0.75,
      "uptake": 0.5
    }
  ]
}

Simple. Array of prescriptions. Field names match the app’s internal type definition. Optional fields can be omitted.

Phase 3: Building the Modal Component

Now the real work: the ImportPrescriptions.vue component.

Key requirements:

  • Accept pasted JSON
  • Validate JSON syntax in real-time
  • Validate each prescription using existing validation logic
  • Show success/failure feedback
  • List detailed errors (which rows failed, why)
  • Import to localStorage on success

I started with the JSON validation. Initially, I wrote:

const isValidJson = computed(() => {
  try {
    jsonInput.value.trim() && JSON.parse(jsonInput.value)
    return true
  } catch {
    return false
  }
})

Linting caught this immediately: “Expected expression to be used” — the AND operator result was never used. The fix:

const isValidJson = computed(() => {
  try {
    if (!jsonInput.value.trim()) return false
    JSON.parse(jsonInput.value)
    return true
  } catch {
    return false
  }
})

Lesson: Linters catch subtle bugs. Listen to them.

Phase 4: Validation and Error Handling

The tricky part: validating multiple prescriptions and collecting all errors, not just stopping at the first failure.

prescriptions.forEach((rx, index) => {
  const validation = validatePrescription(rx)
  if (validation.valid) {
    try {
      savePrescription(rx)
      importResult.value.success++
    } catch (e) {
      importResult.value.failed++
      importResult.value.errors.push(
        `Row ${index + 1} (${rx.name}): Failed to save - ${error}`
      )
    }
  } else {
    importResult.value.failed++
    importResult.value.errors.push(
      `Row ${index + 1} (${rx.name}): ${validation.errors.join(', ')}`
    )
  }
})

This approach:

  • Validates all rows, not just the first one
  • Provides context (row number, drug name)
  • Reuses existing validationvalidatePrescription() already knows all the rules
  • Captures save errors — in case localStorage fails

Result: Users see exactly which drugs imported successfully and which failed, with reasons why.

Phase 5: Integration with PrescriptionForm

The final step: wire the import modal into the existing form. I added:

  • State ref for modal visibility
  • A subdued “or import prescriptions” link below the submit button
  • Modal component that emits @imported on success
<!-- Template -->
<div class="import-link-container">
  <button
    type="button"
    @click="showImportModal = true"
    class="import-link"
  >
    or import prescriptions
  </button>
</div>

<!-- Import modal -->
<ImportPrescriptions
  v-if="showImportModal"
  @imported="handleImportSuccess"
  @close="showImportModal = false"
/>

The link is intentionally subdued (gray text, no border, underline) so it doesn’t distract from the main form, but it’s discoverable for users who want bulk import.

Technical Deep Dive: Vue 3 JSON Import Modal

Here’s what makes this pattern reusable for other projects:

1. JSON Validation is Cheap

Validating JSON syntax happens in a computed, so it’s instant as users type. The import button only enables when the JSON is valid:

<button
  :disabled="!isValidJson || jsonInput.trim().length === 0"
>
  Import
</button>

2. Reuse Existing Validation Logic

Don’t duplicate validation. The import modal calls the same validatePrescription() function that the form uses. This means:

  • Consistency: Same rules everywhere
  • Maintainability: Update validation once, both work
  • Confidence: If the form validates it, the import will too

3. localStorage Integration

Each prescription is saved using the existing savePrescription() function, which handles ID generation and localStorage persistence. No special code needed—the modal just leverages existing infrastructure.

4. User Feedback is Critical

The modal shows:

  • Success count: “Successfully imported 3 prescriptions”
  • Failure count: “Failed to import 1 prescription”
  • Detailed errors: Row number, drug name, specific validation failures

This transparency helps users fix data issues and retry.

Challenges and Solutions

Challenge 1: Linting Errors

The Problem: My initial JSON validation used an AND operator that wasn’t assigned, triggering oxlint’s “expected expression to be used” error.

The Fix: Explicit if statement. More readable anyway.

Challenge 2: Error Collection

The Problem: Stopping at the first error leaves users guessing about other issues in their data.

The Fix: Validate all rows, collect all errors. Users see everything at once and can fix multiple issues in one pass.

Lessons Learned

  • Modal components are powerful for side workflows. Importing bulk data is a secondary action—keep it off the main form but easily accessible.
  • Reuse validation logic. Don’t write different validators for form and import. Use one source of truth.
  • Linters are your friend. Fixed a subtle bug immediately instead of causing issues later.
  • Real data matters for testing. Using actual drug PK parameters (from pharmacy references) made testing feel authentic.
  • Error context is valuable. Showing row numbers and drug names makes debugging user data problems easier.

Results and Next Steps

What works now:

  • ✓ Users can paste JSON with multiple prescriptions
  • ✓ Each prescription is validated against the same rules as the form
  • ✓ Detailed error feedback for debugging bad data
  • ✓ Successful imports are stored in localStorage
  • ✓ Modal integrates seamlessly with existing form workflow

Future enhancements:

  • CSV import (convert CSV to JSON, then use existing logic)
  • Export prescriptions as JSON (mirror of import)
  • Pre-populated templates for common drugs
  • Database of medications (so users don’t need to research PK parameters)

Key Takeaway

Building a robust import system isn’t just about parsing JSON. It’s about understanding your users’ workflows, leveraging existing validation logic, providing clear feedback, and integrating thoughtfully with the rest of your app. A Vue 3 modal is a perfect vehicle for this—non-disruptive, reusable, and focused.

If you’re building data-heavy applications (CMSs, dashboards, educational tools), consider a similar import pattern. Your users will appreciate the bulk operations capability.

Resources

Full Source Code:

«

Leave a Reply