Purpose: Manages circular progress spinner components with dynamic percentage updates, smooth animations, and customizable rotation effects.

Features

  • Dynamic percentage updates with visual circle progress
  • Smooth animation transitions between percentage values
  • Optional continuous rotation animation
  • Configurable animation speeds
  • ARIA accessibility attributes for screen readers
  • Support for custom text display alongside percentage

Static Values

static values = {
  percent: { type: Number, default: 0 },           // Current percentage (0-100)
  animate: { type: Boolean, default: false },      // Enable continuous rotation
  speed: { type: String, default: "normal" },      // Animation speed
  hasCustomText: { type: Boolean, default: false } // Whether custom text is set
}

Static Targets

static targets = [
  "progress",  // SVG circle element showing progress
  "text"       // Text element displaying percentage/custom text
]

Public Methods

  • setPercent(percent): Instantly update progress to new percentage
  • animateToPercent(targetPercent, duration): Smoothly animate to new percentage
  • setAnimate(animate): Toggle continuous rotation animation
  • startAnimation(): Start continuous rotation
  • stopAnimation(): Stop continuous rotation
  • setSpeed(speed): Change rotation animation speed

Usage Examples

Basic Progress Spinner

<%= fx_spinner_percent(
  percent: 75,
  color: :info,
  data: { controller: "fx-spinner-percent" }
) %>

With Custom Text and Animation

<%= fx_spinner_percent(
  percent: 50,
  text: "Loading...",
  animate: true,
  speed: :fast,
  color: :success,
  label_html: { class: "text-lg font-bold" }
) %>

JavaScript Control

<%= fx_spinner_percent(
  id: "upload-progress",
  percent: 0,
  text: "Starting...",
  color: :info
) %>

<script>
  // Get controller reference
  const spinner = document.getElementById('upload-progress');
  const controller = application.getControllerForElementAndIdentifier(spinner, 'fx-spinner-percent');

  // Update progress instantly
  controller.setPercent(25);

  // Animate to new percentage over 2 seconds
  controller.animateToPercent(75, 2000);

  // Start rotation animation
  controller.startAnimation();

  // Change animation speed
  controller.setSpeed('fast');

  // Stop animation
  controller.stopAnimation();
</script>

File Upload Progress

<div class="upload-container">
  <%= fx_spinner_percent(
    id: "file-upload-progress",
    percent: 0,
    text: "Ready",
    color: :default,
    size: 2
  ) %>

  <input type="file" id="file-input" class="mt-4">
</div>

<script>
  document.getElementById('file-input').addEventListener('change', function(e) {
    const file = e.target.files[0];
    if (!file) return;

    const spinner = document.getElementById('file-upload-progress');
    const controller = application.getControllerForElementAndIdentifier(spinner, 'fx-spinner-percent');

    // Simulate upload progress
    let progress = 0;
    const interval = setInterval(() => {
      progress += Math.random() * 10;
      if (progress >= 100) {
        progress = 100;
        clearInterval(interval);

        // Update to show completion
        controller.setPercent(100);
        setTimeout(() => {
          // Update text content manually
          const textElement = spinner.querySelector('[data-fx-spinner-percent-target="text"]');
          if (textElement) textElement.textContent = 'Complete!';
        }, 100);
      } else {
        controller.animateToPercent(Math.min(progress, 95), 300);
      }
    }, 200);
  });
</script>

Multi-Step Process Indicator

<div class="process-steps">
  <% %w[Upload Validate Process Complete].each_with_index do |step, index| %>
    <div class="step flex items-center mb-4">
      <%= fx_spinner_percent(
        id: "step-#{index}",
        percent: 0,
        text: step,
        color: :default,
        size: 1,
        label_html: { class: "text-sm" }
      ) %>
      <span class="ml-3"><%= step %></span>
    </div>
  <% end %>
</div>

<script>
  function updateStep(stepIndex, percentage, color = 'default') {
    const spinner = document.getElementById(`step-${stepIndex}`);
    const controller = application.getControllerForElementAndIdentifier(spinner, 'fx-spinner-percent');

    if (percentage === 100) {
      controller.animateToPercent(100, 500);
      // Update color classes manually for completion
      const progressCircle = spinner.querySelector('[data-fx-spinner-percent-target="progress"]');
      if (progressCircle) {
        progressCircle.classList.remove('stroke-blue-600', 'stroke-gray-200');
        progressCircle.classList.add('stroke-green-500');
      }
    } else {
      controller.animateToPercent(percentage, 300);
    }
  }

  // Example usage
  updateStep(0, 100); // Complete upload
  updateStep(1, 75);  // Validation in progress
  updateStep(2, 30);  // Processing started
</script>

Dashboard Metrics

<div class="metrics-grid grid grid-cols-3 gap-6">
  <div class="metric-card">
    <h3 class="text-lg font-semibold mb-4">Server Load</h3>
    <%= fx_spinner_percent(
      id: "server-load",
      percent: 45,
      color: :warning,
      size: 2,
      animate: true,
      speed: :slow
    ) %>
  </div>

  <div class="metric-card">
    <h3 class="text-lg font-semibold mb-4">Memory Usage</h3>
    <%= fx_spinner_percent(
      id: "memory-usage",
      percent: 78,
      color: :failure,
      size: 2
    ) %>
  </div>

  <div class="metric-card">
    <h3 class="text-lg font-semibold mb-4">Disk Space</h3>
    <%= fx_spinner_percent(
      id: "disk-space",
      percent: 34,
      color: :success,
      size: 2
    ) %>
  </div>
</div>

<script>
  // Update metrics every 5 seconds
  setInterval(() => {
    // Simulate real-time metric updates
    const serverLoad = document.getElementById('server-load');
    const memoryUsage = document.getElementById('memory-usage');
    const diskSpace = document.getElementById('disk-space');

    const serverController = application.getControllerForElementAndIdentifier(serverLoad, 'fx-spinner-percent');
    const memoryController = application.getControllerForElementAndIdentifier(memoryUsage, 'fx-spinner-percent');
    const diskController = application.getControllerForElementAndIdentifier(diskSpace, 'fx-spinner-percent');

    // Random metric updates
    serverController.animateToPercent(Math.floor(Math.random() * 100), 1000);
    memoryController.animateToPercent(Math.floor(Math.random() * 100), 1000);
    diskController.animateToPercent(Math.floor(Math.random() * 100), 1000);
  }, 5000);
</script>

Animation Speeds

  • slow: 3 seconds per rotation
  • normal: 1 second per rotation (default)
  • fast: 0.5 seconds per rotation
  • very_fast: 0.3 seconds per rotation

Accessibility Features

  • Automatic ARIA attributes (role="progressbar", aria-valuenow, etc.)
  • Screen reader compatible with customizable labels
  • Progress updates announce changes to assistive technology
  • Proper semantic markup for accessibility compliance