Vanilla JavaScript Integration

Learn how to integrate the Tiro.health Web SDK into vanilla JavaScript applications using modern ES modules and build tools.

Overview

The Web SDK works seamlessly with vanilla JavaScript using:

  • ES6 modules for clean imports
  • Vite for fast development and building
  • Modern JavaScript (ES2022+)
  • Native DOM APIs for element manipulation
  • No framework overhead - lightweight and fast

This guide demonstrates a complete vanilla JavaScript integration using Vite as the build tool.


Prerequisites

Before starting, ensure you have:

  • Node.js 18 or later
  • npm or yarn package manager
  • Access to Tiro.health private npm registry (see Installation)
  • Basic knowledge of JavaScript modules and DOM manipulation

Project Setup

Create a New Vite Project

Vite provides fast development with hot module replacement:

npm create vite@latest my-forms-app -- --template vanilla
cd my-forms-app
npm install

This creates a minimal vanilla JavaScript project with:

  • index.html - Entry point
  • main.js - Main JavaScript file
  • style.css - Styles
  • vite.config.js - Vite configuration

Configure NPM Registry

Add registry authentication to .npmrc in your project root:

@tiro-health:registry=https://europe-npm.pkg.dev/tiroapp-4cb17/npm-ext/
//europe-npm.pkg.dev/tiroapp-4cb17/npm-ext/:always-auth=true

Install the Web SDK

npm install @tiro-health/web-sdk

Configure Environment Variables

Create a .env file in your project root:

# .env
VITE_SDC_ENDPOINT=https://your-sdc-backend.example.com/fhir
VITE_DATA_ENDPOINT=https://your-fhir-server.example.com/fhir
VITE_QUESTIONNAIRE_ID=your-questionnaire-id
VITE_PATIENT_ID=your-patient-id

Important: Add .env to your .gitignore to avoid committing sensitive data.


Basic Integration

HTML Structure

Update index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Patient Forms</title>
  </head>
  <body>
    <div id="app">
      <h1>Patient Questionnaire</h1>
      <div id="form-container"></div>
    </div>
    <script type="module" src="/main.js"></script>
  </body>
</html>

Simple JavaScript Integration

Update main.js:

import { FormFiller } from '@tiro-health/web-sdk'
import './style.css'

// Get environment variables
const sdcEndpoint = import.meta.env.VITE_SDC_ENDPOINT
const questionnaireId = import.meta.env.VITE_QUESTIONNAIRE_ID

// Create FormFiller instance
const filler = new FormFiller({
  questionnaire: questionnaireId,
  sdcEndpoint: {
    resourceType: 'Endpoint',
    address: sdcEndpoint,
  },
  onSubmit: (response) => {
    console.log('Form submitted:', response)
    alert('Form submitted successfully!')
  },
  onChange: (response) => {
    console.log('Form changed:', response)
  },
  onError: (error) => {
    console.error('Form error:', error)
    alert(`Error: ${error.message}`)
  },
})

// Mount to DOM element
const container = document.getElementById('form-container')
filler.mount(container)

// Cleanup on page unload
window.addEventListener('beforeunload', () => {
  filler.unmount()
})

Run Development Server

npm run dev

Visit http://localhost:5173 to see your form.


Complete Example

Full Application Structure

Project structure:

my-forms-app/
├── index.html
├── main.js
├── style.css
├── src/
│   ├── components.js
│   ├── config.js
│   └── utils.js
├── .env
├── .gitignore
├── package.json
└── vite.config.js

Configuration Module

Create src/config.js:

export const config = {
  sdcEndpoint: {
    resourceType: 'Endpoint',
    address: import.meta.env.VITE_SDC_ENDPOINT,
  },
  dataEndpoint: {
    resourceType: 'Endpoint',
    address: import.meta.env.VITE_DATA_ENDPOINT,
  },
  questionnaireId: import.meta.env.VITE_QUESTIONNAIRE_ID,
  patientId: import.meta.env.VITE_PATIENT_ID,
  isDevelopment: import.meta.env.DEV,
}

Components Module

Create src/components.js:

import {
  FormFiller,
  LaunchContextProvider,
  Narrative,
  ValidationFeedback,
  VisualizationToggle,
} from '@tiro-health/web-sdk'
import { config } from './config.js'

export class FormApp {
  constructor() {
    this.components = {
      contextProvider: null,
      filler: null,
      narrative: null,
      validation: null,
      toggle: null,
    }
    this.isInitialized = false
  }

  async initialize() {
    try {
      this.showLoading()

      // Create FormFiller
      this.components.filler = new FormFiller({
        questionnaire: config.questionnaireId,
        sdcEndpoint: config.sdcEndpoint,
        visualize: config.isDevelopment,
        onSubmit: this.handleSubmit.bind(this),
        onChange: this.handleChange.bind(this),
        onError: this.handleError.bind(this),
      })

      // Create LaunchContextProvider
      this.components.contextProvider = new LaunchContextProvider({
        dataEndpoint: config.dataEndpoint,
        filler: this.components.filler,
        patientId: config.patientId,
      })

      // Create Narrative
      this.components.narrative = new Narrative({
        filler: this.components.filler,
        visualize: config.isDevelopment,
      })

      // Create ValidationFeedback
      this.components.validation = new ValidationFeedback({
        filler: this.components.filler,
        visualize: config.isDevelopment,
      })

      // Create VisualizationToggle
      this.components.toggle = new VisualizationToggle()

      // Mount all components
      this.mountComponents()

      this.isInitialized = true
      this.hideLoading()
    } catch (error) {
      console.error('Failed to initialize app:', error)
      this.showError(error)
    }
  }

  mountComponents() {
    const elements = {
      context: document.getElementById('context-container'),
      form: document.getElementById('form-container'),
      narrative: document.getElementById('narrative-container'),
      validation: document.getElementById('validation-container'),
      toggle: document.getElementById('toggle-container'),
    }

    // Check all elements exist
    for (const [key, element] of Object.entries(elements)) {
      if (!element) {
        throw new Error(`Container element not found: ${key}-container`)
      }
    }

    // Mount components
    this.components.contextProvider.mount(elements.context)
    this.components.filler.mount(elements.form)
    this.components.narrative.mount(elements.narrative)
    this.components.validation.mount(elements.validation)
    this.components.toggle.mount(elements.toggle)
  }

  destroy() {
    // Unmount all components
    Object.values(this.components).forEach((component) => {
      component?.unmount()
    })

    // Clear references
    this.components = {
      contextProvider: null,
      filler: null,
      narrative: null,
      validation: null,
      toggle: null,
    }

    this.isInitialized = false
  }

  handleSubmit(response) {
    console.log('Form submitted:', response)
    alert('Form submitted successfully!')
  }

  handleChange(response) {
    console.log('Form updated:', response)
  }

  handleError(error) {
    console.error('Form error:', error)
    this.showError(error)
  }

  showLoading() {
    const loading = document.getElementById('loading')
    if (loading) loading.style.display = 'block'

    const content = document.getElementById('content')
    if (content) content.style.display = 'none'
  }

  hideLoading() {
    const loading = document.getElementById('loading')
    if (loading) loading.style.display = 'none'

    const content = document.getElementById('content')
    if (content) content.style.display = 'block'
  }

  showError(error) {
    this.hideLoading()

    const errorDiv = document.getElementById('error')
    const errorMessage = document.getElementById('error-message')

    if (errorDiv && errorMessage) {
      errorMessage.textContent = error.message
      errorDiv.style.display = 'block'
    }
  }

  retry() {
    const errorDiv = document.getElementById('error')
    if (errorDiv) errorDiv.style.display = 'none'

    this.initialize()
  }
}

Main Application File

Update main.js:

import { FormApp } from './src/components.js'
import './style.css'

// Create app instance
const app = new FormApp()

// Initialize when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => app.initialize())
} else {
  app.initialize()
}

// Cleanup on page unload
window.addEventListener('beforeunload', () => {
  app.destroy()
})

// Expose app to window for debugging (optional)
if (import.meta.env.DEV) {
  window.formApp = app
}

Complete HTML

Update index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Patient Forms - Tiro.health</title>
  </head>
  <body>
    <div id="app">
      <!-- Loading State -->
      <div id="loading" class="loading">
        <h2>Loading...</h2>
      </div>

      <!-- Error State -->
      <div id="error" class="error" style="display: none;">
        <h2>Error</h2>
        <p id="error-message"></p>
        <button onclick="window.formApp?.retry()">Retry</button>
      </div>

      <!-- Main Content -->
      <div id="content" style="display: none;">
        <header>
          <h1>Patient Forms</h1>
          <div id="toggle-container"></div>
        </header>

        <div class="layout">
          <aside class="sidebar">
            <h2>Context</h2>
            <div id="context-container"></div>

            <h2>Validation</h2>
            <div id="validation-container"></div>
          </aside>

          <main class="main-content">
            <h2>Questionnaire</h2>
            <div id="form-container"></div>

            <h2>Clinical Narrative</h2>
            <div id="narrative-container"></div>
          </main>
        </div>
      </div>
    </div>

    <script type="module" src="/main.js"></script>
  </body>
</html>

Styles

Update style.css:

:root {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.5;
  font-weight: 400;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: #f5f5f5;
  color: #333;
}

#app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
}

.layout {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 2rem;
}

.sidebar {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.main-content {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.loading,
.error {
  text-align: center;
  padding: 3rem;
}

.error {
  color: #d32f2f;
}

.error button {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  background-color: #1976d2;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
}

.error button:hover {
  background-color: #1565c0;
}

h1 {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

h2 {
  font-size: 1.5rem;
  margin-bottom: 1rem;
}

/* Responsive layout */
@media (max-width: 768px) {
  .layout {
    grid-template-columns: 1fr;
  }

  #app {
    padding: 1rem;
  }
}

Module Patterns

ES6 Modules

Use modern ES6 module syntax:

// Importing
import { FormFiller, Narrative } from '@tiro-health/web-sdk'

// Named exports
export const config = { /* ... */ }
export function initializeForm() { /* ... */ }

// Default export
export default class FormApp { /* ... */ }

Dynamic Imports

Load components on demand:

async function loadFormComponents() {
  const { FormFiller } = await import('@tiro-health/web-sdk')

  const filler = new FormFiller(config)
  return filler
}

Module Bundling

Vite automatically bundles your modules for production:

npm run build

Output is optimized and tree-shaken in the dist/ directory.


Best Practices

1. Environment Variables

Always use environment variables for configuration:

// ✅ Good
const endpoint = import.meta.env.VITE_SDC_ENDPOINT

// ❌ Bad - hardcoded
const endpoint = 'https://api.example.com/fhir'

2. Error Handling

Always handle errors gracefully:

try {
  const filler = new FormFiller(config)
  filler.mount(container)
} catch (error) {
  console.error('Failed to initialize form:', error)
  showErrorMessage(error.message)
}

3. DOM Ready Check

Ensure DOM is ready before mounting:

function initApp() {
  const container = document.getElementById('form-container')

  if (!container) {
    console.error('Container element not found')
    return
  }

  const filler = new FormFiller(config)
  filler.mount(container)
}

// Wait for DOM
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initApp)
} else {
  initApp()
}

4. Cleanup

Always clean up when navigating away:

let filler

function initialize() {
  filler = new FormFiller(config)
  filler.mount(container)
}

window.addEventListener('beforeunload', () => {
  filler?.unmount()
})

5. Debugging

Use console logging in development:

if (import.meta.env.DEV) {
  console.log('Initializing form with config:', config)
  window.formApp = app // Expose to window for debugging
}

Common Pitfalls

1. Mounting Before DOM is Ready

Problem:

// ❌ Script runs before DOM is ready
const container = document.getElementById('form-container') // null!
filler.mount(container) // Error!

Solution:

// ✅ Wait for DOM
document.addEventListener('DOMContentLoaded', () => {
  const container = document.getElementById('form-container')
  filler.mount(container)
})

2. Not Handling Missing Elements

Problem:

// ❌ No null check
const container = document.getElementById('form-container')
filler.mount(container) // Error if container is null

Solution:

// ✅ Check element exists
const container = document.getElementById('form-container')
if (!container) {
  console.error('Container not found')
  return
}
filler.mount(container)

3. Forgetting to Unmount

Problem:

// ❌ No cleanup - memory leak
const filler = new FormFiller(config)
filler.mount(container)
// Navigation happens - filler is never unmounted

Solution:

// ✅ Add cleanup listener
window.addEventListener('beforeunload', () => {
  filler.unmount()
})

4. Incorrect Module Paths

Problem:

// ❌ Wrong import path
import { FormFiller } from './node_modules/@tiro-health/web-sdk'

Solution:

// ✅ Use package name
import { FormFiller } from '@tiro-health/web-sdk'

5. Missing Environment Variables

Problem:

// ❌ Undefined environment variable
const endpoint = import.meta.env.VITE_SDC_ENDPOINT // undefined

Solution:

// ✅ Check and provide fallback
const endpoint = import.meta.env.VITE_SDC_ENDPOINT
if (!endpoint) {
  throw new Error('VITE_SDC_ENDPOINT is not configured')
}

Next Steps

For questions or support, please contact the Tiro.health team.

Was this page helpful?