Skip to content

Version: Arctyk ITSM v0.6.0+ Last Updated: January 2026

JavaScript Patterns

This page documents JavaScript patterns and practices in Arctyk ITSM.


Overview

Arctyk ITSM uses vanilla JavaScript (no jQuery or heavy frameworks) following modern ES6+ patterns. The codebase emphasizes:

  • Module pattern for encapsulation
  • Event delegation for dynamic content
  • AJAX for form submissions and async operations
  • Progressive enhancement for core functionality

Module Pattern

All JavaScript uses the module pattern (IIFE) for encapsulation and namespace management.

Basic Module Structure

(function () {
  "use strict";

  // Private variables
  const API_ENDPOINT = "/api/tickets/";

  // Private functions
  function handleResponse(response) {
    // ...
  }

  // Public API (if needed)
  window.TicketModule = {
    init: function () {
      // Initialization code
    },
  };

  // Auto-initialize on DOMContentLoaded
  document.addEventListener("DOMContentLoaded", function () {
    // Setup code
  });
})();

Benefits

Encapsulation - Variables don't pollute global scope
Organization - Related code grouped together
Testability - Easier to test isolated modules
Maintenance - Clear boundaries between modules


AJAX Forms

Forms submit via AJAX for better UX without full page reloads.

Form Submission Pattern

const form = document.getElementById("comment-form");

form.addEventListener("submit", function (e) {
  e.preventDefault();

  const formData = new FormData(form);
  const csrfToken = document.querySelector("[name=csrfmiddlewaretoken]").value;

  fetch(form.action, {
    method: "POST",
    headers: {
      "X-CSRFToken": csrfToken,
      "X-Requested-With": "XMLHttpRequest",
    },
    body: formData,
  })
    .then((response) => response.json())
    .then((data) => {
      if (data.success) {
        // Update UI with new comment
        updateCommentList(data.comment);
        form.reset();
      } else {
        // Show validation errors
        displayErrors(data.errors);
      }
    })
    .catch((error) => {
      console.error("Error:", error);
      showErrorMessage("Failed to submit form");
    });
});

JSON Response Handling

function handleJsonResponse(response) {
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

fetch("/api/endpoint/")
  .then(handleJsonResponse)
  .then((data) => {
    // Process successful response
  })
  .catch((error) => {
    // Handle error
  });

Event Handling

Use event delegation for dynamic content that may be added/removed from the DOM.

Event Delegation Pattern

// ❌ Bad: Direct event listener (breaks for dynamic content)
document.querySelectorAll(".delete-btn").forEach((btn) => {
  btn.addEventListener("click", handleDelete);
});

// ✅ Good: Event delegation (works for dynamic content)
document.addEventListener("click", function (e) {
  if (e.target.matches(".delete-btn")) {
    handleDelete(e);
  }
});

Multiple Event Types

document.addEventListener("click", function (e) {
  // Edit button clicked
  if (e.target.matches(".edit-btn")) {
    handleEdit(e);
  }

  // Delete button clicked
  if (e.target.matches(".delete-btn")) {
    handleDelete(e);
  }

  // Close modal
  if (e.target.matches(".modal-close")) {
    closeModal(e);
  }
});

DOM Manipulation

Adding Elements

// Create element
const commentDiv = document.createElement("div");
commentDiv.className = "comment";
commentDiv.innerHTML = `
    <div class="comment-header">
        <span class="comment-author">${data.author}</span>
        <span class="comment-timestamp">${data.timestamp}</span>
    </div>
    <div class="comment-body">${data.content}</div>
`;

// Add to DOM
document.getElementById("comments-list").prepend(commentDiv);

Updating Elements

// Update text content
document.getElementById("ticket-status").textContent = newStatus;

// Update HTML content (be careful with XSS)
const safeHTML = escapeHTML(userContent);
document.getElementById("content").innerHTML = safeHTML;

// Update attributes
const btn = document.getElementById("submit-btn");
btn.disabled = true;
btn.setAttribute("aria-busy", "true");

Removing Elements

// Remove single element
const element = document.getElementById("temp-message");
element.remove();

// Remove all matching elements
document.querySelectorAll(".temp-alert").forEach((el) => el.remove());

Common Patterns

Confirmation Dialogs

function confirmDelete(e) {
  e.preventDefault();

  if (!confirm("Are you sure you want to delete this item?")) {
    return;
  }

  // Proceed with deletion
  deleteItem(e.target.dataset.itemId);
}

Loading States

function showLoading(button) {
  button.disabled = true;
  button.dataset.originalText = button.textContent;
  button.textContent = "Loading...";
  button.classList.add("is-loading");
}

function hideLoading(button) {
  button.disabled = false;
  button.textContent = button.dataset.originalText;
  button.classList.remove("is-loading");
}

Toast Notifications

function showToast(message, type = "info") {
  const toast = document.createElement("div");
  toast.className = `toast toast-${type}`;
  toast.textContent = message;

  document.body.appendChild(toast);

  // Auto-remove after 3 seconds
  setTimeout(() => {
    toast.classList.add("fade-out");
    setTimeout(() => toast.remove(), 300);
  }, 3000);
}

CSRF Protection

All AJAX POST/PUT/PATCH/DELETE requests must include the CSRF token.

function getCSRFToken() {
  return document.querySelector("[name=csrfmiddlewaretoken]").value;
}

fetch("/api/endpoint/", {
  method: "POST",
  headers: {
    "X-CSRFToken": getCSRFToken(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
});

Error Handling

Display Validation Errors

function displayErrors(errors) {
  // Clear previous errors
  document.querySelectorAll(".error-message").forEach((el) => el.remove());

  // Display new errors
  Object.entries(errors).forEach(([field, messages]) => {
    const input = document.querySelector(`[name="${field}"]`);
    if (input) {
      const errorDiv = document.createElement("div");
      errorDiv.className = "error-message text-danger";
      errorDiv.textContent = messages.join(", ");
      input.parentNode.appendChild(errorDiv);
    }
  });
}

Global Error Handler

window.addEventListener("error", function (e) {
  console.error("JavaScript error:", e.error);
  // Optionally send to error tracking service
});

Performance Best Practices

Debouncing

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Usage: Search input with 300ms debounce
const searchInput = document.getElementById("search");
const debouncedSearch = debounce(performSearch, 300);
searchInput.addEventListener("input", debouncedSearch);

Throttling

function throttle(func, limit) {
  let inThrottle;
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Usage: Scroll event with 100ms throttle
window.addEventListener("scroll", throttle(handleScroll, 100));

Accessibility

Keyboard Navigation

document.addEventListener("keydown", function (e) {
  // Close modal with Escape key
  if (e.key === "Escape") {
    closeModal();
  }

  // Submit form with Ctrl+Enter
  if (e.ctrlKey && e.key === "Enter") {
    document.getElementById("comment-form").submit();
  }
});

Focus Management

function openModal() {
  const modal = document.getElementById("modal");
  modal.classList.add("show");

  // Focus first interactive element
  const firstInput = modal.querySelector("input, button, textarea");
  if (firstInput) {
    firstInput.focus();
  }
}