Rails Stimulus i Importmaps w 2026: Nowoczesny JavaScript bez narzędzi do budowania

Kompleksowy przewodnik po Stimulus i Importmaps w Rails 8 - jak tworzyć interaktywne aplikacje webowe bez webpack, esbuild ani żadnych bundlerów.

Rails Stimulus i Importmaps w 2026: Nowoczesny JavaScript bez narzędzi do budowania

Rails od zawsze stawiał na konwencję ponad konfiguracją, a podejście do JavaScript nie jest wyjątkiem. W 2026 roku Rails 8 oferuje w pełni dojrzały stos do budowy interaktywnych aplikacji bez potrzeby konfiguracji webpack, esbuild czy jakiegokolwiek bundlera. Stimulus wraz z Importmaps stanowią fundament nowoczesnego podejścia do JavaScript w ekosystemie Rails.

Stimulus i Importmaps są domyślnie włączone w każdej nowej aplikacji Rails 8. Oznacza to zero konfiguracji - wystarczy uruchomić rails new i można zacząć pisać interaktywny kod JavaScript.

Czym są Importmaps i dlaczego warto ich używać

Importmaps to natywna funkcjonalność przeglądarek pozwalająca na mapowanie nazw modułów JavaScript bezpośrednio do URL-i. Zamiast bundlować wszystkie pliki w jeden wielki plik, przeglądarka pobiera moduły na żądanie. Rails wykorzystuje tę technologię poprzez gem importmap-rails.

Kluczowe zalety tego podejścia to eliminacja kroku budowania, natychmiastowe odświeżanie podczas developmentu oraz prostota konfiguracji. Nie ma potrzeby instalacji Node.js ani zarządzania pakietami npm.

ruby
# config/importmap.rb
pin "application"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"

Powyższa konfiguracja definiuje wszystkie niezbędne moduły. Dyrektywa pin mapuje nazwę modułu do konkretnego pliku, a pin_all_from automatycznie rejestruje wszystkie kontrolery Stimulus z podanego katalogu.

Dodawanie zewnętrznych bibliotek

Dodawanie bibliotek JavaScript jest równie proste jak w tradycyjnych bundlerach. Polecenie bin/importmap pin pobiera bibliotekę z CDN i dodaje odpowiedni wpis do konfiguracji.

bash
# Dodawanie biblioteki lodash
bin/importmap pin lodash

# Dodawanie biblioteki z konkretną wersją
bin/importmap pin lodash@4.17.21

# Wyświetlanie aktualnych pinów
bin/importmap pins

Po wykonaniu polecenia biblioteka jest natychmiast dostępna do importu w kodzie JavaScript:

app/javascript/application.jsjavascript
import _ from "lodash"

console.log(_.chunk([1, 2, 3, 4, 5], 2))
// Output: [[1, 2], [3, 4], [5]]

Wprowadzenie do Stimulus

Stimulus to framework JavaScript zaprojektowany specjalnie do współpracy z HTML generowanym przez serwer. W przeciwieństwie do React czy Vue, Stimulus nie przejmuje kontroli nad renderowaniem - zamiast tego wzbogaca istniejący HTML o interaktywność.

Podstawowym budulcem Stimulus są kontrolery. Każdy kontroler to klasa JavaScript odpowiedzialna za określone zachowanie na stronie.

app/javascript/controllers/hello_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["name", "output"]

  greet() {
    this.outputTarget.textContent = `Cześć, ${this.nameTarget.value}!`
  }
}

Kontroler powyżej definiuje dwa targety (elementy DOM) i metodę greet, która aktualizuje zawartość elementu output. Aby połączyć kontroler z HTML, należy użyć atrybutów data:

html
<div data-controller="hello">
  <input data-hello-target="name" type="text" placeholder="Wpisz imię">
  <button data-action="click->hello#greet">Przywitaj</button>
  <p data-hello-target="output"></p>
</div>

Generowanie kontrolerów Stimulus

Rails dostarcza generator do tworzenia nowych kontrolerów Stimulus. Pozwala to zachować spójność struktury projektu i automatycznie rejestruje kontroler w systemie.

bash
# Generowanie nowego kontrolera
bin/rails generate stimulus dropdown

# Rezultat:
# create  app/javascript/controllers/dropdown_controller.js

Wygenerowany kontroler zawiera podstawową strukturę gotową do rozbudowy:

app/javascript/controllers/dropdown_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    // Wywoływane gdy kontroler zostaje połączony z DOM
  }
}

Lifecycle callbacks w Stimulus

Kontrolery Stimulus oferują zestaw metod lifecycle, które są wywoływane w określonych momentach cyklu życia kontrolera. Zrozumienie tych callbacków jest kluczowe dla prawidłowego zarządzania stanem i zasobami.

app/javascript/controllers/timer_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { interval: { type: Number, default: 1000 } }
  static targets = ["display"]

  connect() {
    this.count = 0
    this.startTimer()
  }

  disconnect() {
    this.stopTimer()
  }

  startTimer() {
    this.timer = setInterval(() => {
      this.count++
      this.displayTarget.textContent = this.count
    }, this.intervalValue)
  }

  stopTimer() {
    if (this.timer) {
      clearInterval(this.timer)
    }
  }
}

Metoda connect() jest wywoływana gdy element z kontrolerem zostaje dodany do DOM, natomiast disconnect() gdy element zostaje usunięty. To idealne miejsce na inicjalizację i czyszczenie zasobów.

Values - reaktywne właściwości

Stimulus Values to mechanizm przechowywania danych konfiguracyjnych bezpośrednio w atrybutach HTML. Values automatycznie parsują typy danych i oferują reaktywność poprzez callbacki zmian.

app/javascript/controllers/slideshow_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    index: { type: Number, default: 0 },
    autoplay: { type: Boolean, default: false },
    images: Array
  }
  static targets = ["slide"]

  indexValueChanged() {
    this.showCurrentSlide()
  }

  autoplayValueChanged() {
    if (this.autoplayValue) {
      this.startAutoplay()
    } else {
      this.stopAutoplay()
    }
  }

  next() {
    if (this.indexValue < this.imagesValue.length - 1) {
      this.indexValue++
    } else {
      this.indexValue = 0
    }
  }

  previous() {
    if (this.indexValue > 0) {
      this.indexValue--
    } else {
      this.indexValue = this.imagesValue.length - 1
    }
  }

  showCurrentSlide() {
    this.slideTarget.src = this.imagesValue[this.indexValue]
  }

  startAutoplay() {
    this.autoplayTimer = setInterval(() => this.next(), 3000)
  }

  stopAutoplay() {
    if (this.autoplayTimer) {
      clearInterval(this.autoplayTimer)
    }
  }
}

Konfiguracja w HTML wygląda następująco:

html
<div data-controller="slideshow"
     data-slideshow-index-value="0"
     data-slideshow-autoplay-value="true"
     data-slideshow-images-value='["img1.jpg", "img2.jpg", "img3.jpg"]'>
  <img data-slideshow-target="slide" src="">
  <button data-action="slideshow#previous">Poprzedni</button>
  <button data-action="slideshow#next">Następny</button>
</div>

Outlets - komunikacja między kontrolerami

Outlets pozwalają kontrolerom na referencjonowanie innych kontrolerów na stronie. To elegancki sposób na budowanie złożonych interakcji między komponentami.

app/javascript/controllers/search_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static outlets = ["results"]
  static targets = ["input"]

  search() {
    const query = this.inputTarget.value
    if (this.hasResultsOutlet) {
      this.resultsOutlet.filter(query)
    }
  }
}
app/javascript/controllers/results_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["item"]

  filter(query) {
    this.itemTargets.forEach(item => {
      const visible = item.textContent.toLowerCase().includes(query.toLowerCase())
      item.classList.toggle("hidden", !visible)
    })
  }
}

Konfiguracja połączenia odbywa się poprzez atrybut outlet:

html
<div data-controller="search" data-search-results-outlet="#search-results">
  <input data-search-target="input" data-action="input->search#search">
</div>

<div id="search-results" data-controller="results">
  <div data-results-target="item">Element 1</div>
  <div data-results-target="item">Element 2</div>
  <div data-results-target="item">Element 3</div>
</div>

Praktyczny przykład: modal z formularzem

Poniższy przykład demonstruje bardziej złożony kontroler obsługujący modal z formularzem, włączając animacje i walidację.

app/javascript/controllers/modal_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["dialog", "form"]
  static values = { open: Boolean }

  connect() {
    this.boundHandleKeydown = this.handleKeydown.bind(this)
  }

  open() {
    this.openValue = true
    this.dialogTarget.showModal()
    document.addEventListener("keydown", this.boundHandleKeydown)
    this.dialogTarget.classList.add("opacity-100")
    this.dialogTarget.classList.remove("opacity-0")
  }

  close() {
    this.openValue = false
    this.dialogTarget.classList.add("opacity-0")
    this.dialogTarget.classList.remove("opacity-100")
    setTimeout(() => {
      this.dialogTarget.close()
    }, 200)
    document.removeEventListener("keydown", this.boundHandleKeydown)
  }

  handleKeydown(event) {
    if (event.key === "Escape") {
      this.close()
    }
  }

  submitForm(event) {
    if (!this.formTarget.checkValidity()) {
      event.preventDefault()
      this.formTarget.reportValidity()
    }
  }

  clickOutside(event) {
    if (event.target === this.dialogTarget) {
      this.close()
    }
  }
}
html
<div data-controller="modal">
  <button data-action="modal#open">Otwórz formularz</button>

  <dialog data-modal-target="dialog"
          data-action="click->modal#clickOutside"
          class="opacity-0 transition-opacity duration-200 backdrop:bg-black/50">
    <form data-modal-target="form" data-action="submit->modal#submitForm">
      <h2>Formularz kontaktowy</h2>
      <input type="email" required placeholder="Email">
      <textarea required placeholder="Wiadomość"></textarea>
      <div>
        <button type="button" data-action="modal#close">Anuluj</button>
        <button type="submit">Wyślij</button>
      </div>
    </form>
  </dialog>
</div>

Integracja z Turbo

Stimulus doskonale współpracuje z Turbo, drugim filarem Hotwire. Turbo obsługuje nawigację i aktualizacje strony, podczas gdy Stimulus dodaje interaktywność po stronie klienta.

app/javascript/controllers/form_controller.jsjavascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["submit", "spinner"]

  submitting() {
    this.submitTarget.disabled = true
    this.spinnerTarget.classList.remove("hidden")
  }

  submitted() {
    this.submitTarget.disabled = false
    this.spinnerTarget.classList.add("hidden")
  }
}
erb
<%= form_with model: @contact,
              data: {
                controller: "form",
                action: "turbo:submit-start->form#submitting turbo:submit-end->form#submitted"
              } do |f| %>
  <%= f.text_field :name %>
  <%= f.email_field :email %>
  <%= f.submit "Wyślij", data: { form_target: "submit" } %>
  <span data-form-target="spinner" class="hidden">Wysyłanie...</span>
<% end %>

Gotowy na rozmowy o Ruby on Rails?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Debugowanie i narzędzia deweloperskie

Podczas developmentu pomocne są narzędzia do debugowania kontrolerów Stimulus. Można włączyć tryb debug w aplikacji:

app/javascript/application.jsjavascript
import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Włączenie debugowania
application.debug = true

// Dostęp do aplikacji z konsoli przeglądarki
window.Stimulus = application

Z włączonym debugowaniem, konsola przeglądarki wyświetla informacje o łączeniu i rozłączaniu kontrolerów, co znacznie ułatwia diagnozowanie problemów.

Testowanie kontrolerów Stimulus

Testowanie kontrolerów Stimulus można przeprowadzać za pomocą systemu testów JavaScript lub testów systemowych Rails.

test/javascript/controllers/hello_controller.test.jsjavascript
import { Application } from "@hotwired/stimulus"
import HelloController from "controllers/hello_controller"

describe("HelloController", () => {
  let application

  beforeEach(() => {
    document.body.innerHTML = `
      <div data-controller="hello">
        <input data-hello-target="name" value="Jan">
        <button data-action="click->hello#greet">Przywitaj</button>
        <p data-hello-target="output"></p>
      </div>
    `
    application = Application.start()
    application.register("hello", HelloController)
  })

  afterEach(() => {
    application.stop()
  })

  it("wyświetla powitanie po kliknięciu", () => {
    const button = document.querySelector("button")
    const output = document.querySelector("[data-hello-target='output']")

    button.click()

    expect(output.textContent).toBe("Cześć, Jan!")
  })
})

Organizacja kodu w większych projektach

W większych aplikacjach warto organizować kontrolery w podkatalogi według funkcjonalności. Importmaps automatycznie obsługuje zagnieżdżone struktury.

ruby
# config/importmap.rb
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/controllers/admin", under: "controllers/admin"
pin_all_from "app/javascript/controllers/shared", under: "controllers/shared"

Struktura katalogów:

text
app/javascript/controllers/
├── application.js
├── index.js
├── hello_controller.js
├── admin/
│   ├── dashboard_controller.js
│   └── users_controller.js
└── shared/
    ├── modal_controller.js
    └── dropdown_controller.js

W HTML kontrolery z podkatalogów są referencjonowane z prefiksem:

html
<div data-controller="admin--dashboard">...</div>
<div data-controller="shared--modal">...</div>

Podsumowanie

Stimulus i Importmaps w Rails 8 oferują eleganckie rozwiązanie do budowy interaktywnych aplikacji webowych bez złożoności tradycyjnych bundlerów JavaScript. Podejście to doskonale wpisuje się w filozofię Rails - konwencja ponad konfiguracją.

Kluczowe korzyści to zerowa konfiguracja, natychmiastowe odświeżanie podczas developmentu, pełna kompatybilność z HTML generowanym przez serwer oraz doskonała integracja z pozostałymi elementami stosu Hotwire.

Dla zespołów pracujących z Rails, Stimulus stanowi naturalne rozszerzenie istniejącej architektury, pozwalając na stopniowe dodawanie interaktywności bez przepisywania aplikacji na framework SPA. W połączeniu z Turbo tworzy kompletny zestaw narzędzi do budowy nowoczesnych aplikacji webowych.

Wyzwanie dnia

Znajdziesz błąd w Ruby on Rails?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 29 sierpnia 2026

Udostępnij

Powiązane artykuły