From 877465fce04f1a8236c3dbcbc92fab9707dfb24d Mon Sep 17 00:00:00 2001 From: Robert Fletcher Date: Thu, 1 Jan 2026 00:38:46 -0500 Subject: [PATCH] Template: configure Stimulus and Turbo --- Gemfile | 5 ++ Gemfile.lock | 18 +++++ app/javascript/application.ts | 6 ++ app/javascript/controllers/application.ts | 5 ++ .../controllers/dialog_controller.ts | 7 ++ app/javascript/controllers/index.ts | 10 +++ package.json | 7 ++ pnpm-lock.yaml | 67 +++++++++++++++++++ .../controllers/dialog_controller_spec.ts | 10 +++ spec/javascript/support/stimulus.ts | 27 ++++++++ tsconfig.json | 1 + 11 files changed, 163 insertions(+) create mode 100644 app/javascript/controllers/application.ts create mode 100644 app/javascript/controllers/dialog_controller.ts create mode 100644 app/javascript/controllers/index.ts create mode 100644 spec/javascript/controllers/dialog_controller_spec.ts create mode 100644 spec/javascript/support/stimulus.ts diff --git a/Gemfile b/Gemfile index 5f42e9cf7..055488684 100644 --- a/Gemfile +++ b/Gemfile @@ -22,8 +22,11 @@ gem "propshaft" gem "puma", "~> 7.0" gem "rack-ssl" gem "sass" +gem "stimulus-rails" gem "stripe" +gem "strong_migrations" gem "thread" +gem "turbo-rails" gem "will_paginate" group :development do @@ -39,10 +42,12 @@ group :development do end group :development, :test do + gem "bundler-audit", require: false gem "capybara" gem "coveralls_reborn", require: false gem "debug" gem "factory_bot" + gem "factory_bot_rails", require: false gem "pry-byebug" gem "rspec" gem "rspec-rails" diff --git a/Gemfile.lock b/Gemfile.lock index bbbc98712..9f99efc25 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,9 @@ GEM brakeman (8.0.2) racc builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) byebug (13.0.0) reline (>= 0.6.0) capybara (3.40.0) @@ -146,6 +149,9 @@ GEM tzinfo factory_bot (6.5.6) activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) feedbag (1.0.2) addressable (~> 2.8) nokogiri (~> 1.8, >= 1.8.2) @@ -381,8 +387,12 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) + stimulus-rails (1.3.4) + railties (>= 6.0.0) stringio (3.2.0) stripe (18.3.1) + strong_migrations (2.5.2) + activerecord (>= 7.1) sync (0.5.0) term-ansicolor (1.11.3) tins (~> 1) @@ -396,6 +406,9 @@ GEM readline sync tsort (0.2.0) + turbo-rails (2.0.20) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.2.0) @@ -440,11 +453,13 @@ DEPENDENCIES bcrypt bootsnap brakeman + bundler-audit capybara coveralls_reborn debug dotenv-rails factory_bot + factory_bot_rails feedbag feedjira good_job (~> 4.13.0) @@ -470,8 +485,11 @@ DEPENDENCIES sass selenium-webdriver simplecov + stimulus-rails stripe + strong_migrations thread + turbo-rails web-console webdrivers webmock diff --git a/app/javascript/application.ts b/app/javascript/application.ts index 4881d7cbc..09fde6d40 100644 --- a/app/javascript/application.ts +++ b/app/javascript/application.ts @@ -1,4 +1,6 @@ // @ts-nocheck +import "@hotwired/turbo-rails"; +import "@rails/activestorage"; import "jquery"; import "bootstrap"; import "mousetrap"; @@ -6,6 +8,10 @@ import "jquery-visible"; import _ from "underscore"; import Backbone from "backbone"; +import "./controllers/index"; + +Turbo.session.drive = false; + /* global jQuery, Mousetrap */ var $ = jQuery; diff --git a/app/javascript/controllers/application.ts b/app/javascript/controllers/application.ts new file mode 100644 index 000000000..4001c5817 --- /dev/null +++ b/app/javascript/controllers/application.ts @@ -0,0 +1,5 @@ +import {Application} from "@hotwired/stimulus"; + +const application = Application.start(); + +export {application}; diff --git a/app/javascript/controllers/dialog_controller.ts b/app/javascript/controllers/dialog_controller.ts new file mode 100644 index 000000000..ef0ed4369 --- /dev/null +++ b/app/javascript/controllers/dialog_controller.ts @@ -0,0 +1,7 @@ +import {Controller} from "@hotwired/stimulus"; + +export default class extends Controller { + connect(): void { + this.element.textContent = "Hello World!"; + } +} diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts new file mode 100644 index 000000000..f54870434 --- /dev/null +++ b/app/javascript/controllers/index.ts @@ -0,0 +1,10 @@ +/* + * This file is auto-generated by ./bin/rails stimulus:manifest:update + * Run that command whenever you add a new controller or create them with + * ./bin/rails generate stimulus controllerName + */ + +import {application} from "./application"; + +import DialogController from "./dialog_controller"; +application.register("dialog", DialogController); diff --git a/package.json b/package.json index ec32d64cb..8abcfd722 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,11 @@ "dependencies": { "@fontsource/lato": "^5.2.7", "@fontsource/reenie-beanie": "^5.2.8", + "@hotwired/stimulus": "^3.2.2", + "@hotwired/turbo": "^8.0.20", + "@hotwired/turbo-rails": "^8.0.20", + "@rails/actioncable": "^8.1.100", + "@rails/activestorage": "^8.1.100", "backbone": "1.0.0", "bootstrap": "3.1.1", "font-awesome": "4.7.0", @@ -25,9 +30,11 @@ "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.7.0", "@types/backbone": "latest", + "@types/hotwired__turbo": "^8.0.5", "@types/jquery": "latest", "@types/mousetrap": "latest", "@types/node": "^25.2.3", + "@types/rails__actioncable": "^8.0.3", "@types/underscore": "latest", "@typescript-eslint/eslint-plugin": "^8.50.0", "@typescript-eslint/parser": "^8.50.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5507fda9..a9d171d22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,21 @@ importers: '@fontsource/reenie-beanie': specifier: ^5.2.8 version: 5.2.8 + '@hotwired/stimulus': + specifier: ^3.2.2 + version: 3.2.2 + '@hotwired/turbo': + specifier: ^8.0.20 + version: 8.0.23 + '@hotwired/turbo-rails': + specifier: ^8.0.20 + version: 8.0.23 + '@rails/actioncable': + specifier: ^8.1.100 + version: 8.1.200 + '@rails/activestorage': + specifier: ^8.1.100 + version: 8.1.200 backbone: specifier: 1.0.0 version: 1.0.0 @@ -48,6 +63,9 @@ importers: '@types/backbone': specifier: latest version: 1.4.23 + '@types/hotwired__turbo': + specifier: ^8.0.5 + version: 8.0.6 '@types/jquery': specifier: latest version: 3.5.33 @@ -57,6 +75,9 @@ importers: '@types/node': specifier: ^25.2.3 version: 25.2.3 + '@types/rails__actioncable': + specifier: ^8.0.3 + version: 8.0.3 '@types/underscore': specifier: latest version: 1.13.0 @@ -443,6 +464,16 @@ packages: '@fontsource/reenie-beanie@5.2.8': resolution: {integrity: sha512-fDPSpU64a8rVtu5NKRogzKx/fukotfpcet8gFDzHYctRX3xIZ8MCbCGGS4XxJuwhSuhRJX59Ur0eU0QpBQpTZg==} + '@hotwired/stimulus@3.2.2': + resolution: {integrity: sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==} + + '@hotwired/turbo-rails@8.0.23': + resolution: {integrity: sha512-iBILwda3qmQC7FYM70+4s6kEQ7Fx9dJ6+yGxjPyrz9a5JDx1+y7OAA5TA7GGVOZJoicMLrKGdFDNorl40X35lw==} + + '@hotwired/turbo@8.0.23': + resolution: {integrity: sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ==} + engines: {node: '>= 18'} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -493,6 +524,12 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@rails/actioncable@8.1.200': + resolution: {integrity: sha512-on0DSb7AFUkq1ocxivDNQhhGW/RQpY91zvRVyyaEWP4gOOZWy33P/UyxjQk74IENWNrTqs8+zOGHwTjiiFruRw==} + + '@rails/activestorage@8.1.200': + resolution: {integrity: sha512-bPZqv447REBd1NQfba//FjgUqbUd93zKh7+BWhh3vRZ7Nm+RUgm6c5GbWctmik/rMHjsruTHhusYGyoKyf60pg==} + '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -649,6 +686,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hotwired__turbo@8.0.6': + resolution: {integrity: sha512-jqWRyXz+wAa7E3iB3WKcpXjVUgfLt2zGO68x3ANPtM4JSVDOT/q9GhPQzXf3EtPOkdiHfUp7JAC0EU5bSwluFA==} + '@types/jquery@3.5.33': resolution: {integrity: sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==} @@ -664,6 +704,9 @@ packages: '@types/node@25.2.3': resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/rails__actioncable@8.0.3': + resolution: {integrity: sha512-y46MOTYorVQVwlHUyaZYbrh3nIkXsRYNuPna32lb3RngLVBlndNbIPvAUywFfhivftNhYg+vW5sZKWYCVIX2lA==} + '@types/sizzle@2.3.10': resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} @@ -2138,6 +2181,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -2763,6 +2809,15 @@ snapshots: '@fontsource/reenie-beanie@5.2.8': {} + '@hotwired/stimulus@3.2.2': {} + + '@hotwired/turbo-rails@8.0.23': + dependencies: + '@hotwired/turbo': 8.0.23 + '@rails/actioncable': 8.1.200 + + '@hotwired/turbo@8.0.23': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -2810,6 +2865,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@rails/actioncable@8.1.200': {} + + '@rails/activestorage@8.1.200': + dependencies: + spark-md5: 3.0.2 + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -2920,6 +2981,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/hotwired__turbo@8.0.6': {} + '@types/jquery@3.5.33': dependencies: '@types/sizzle': 2.3.10 @@ -2934,6 +2997,8 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/rails__actioncable@8.0.3': {} + '@types/sizzle@2.3.10': {} '@types/underscore@1.13.0': {} @@ -4601,6 +4666,8 @@ snapshots: source-map-js@1.2.1: {} + spark-md5@3.0.2: {} + stable-hash-x@0.2.0: {} stackback@0.0.2: {} diff --git a/spec/javascript/controllers/dialog_controller_spec.ts b/spec/javascript/controllers/dialog_controller_spec.ts new file mode 100644 index 000000000..35954f6ad --- /dev/null +++ b/spec/javascript/controllers/dialog_controller_spec.ts @@ -0,0 +1,10 @@ +import {expect, it} from "vitest"; +import {bootStimulus} from "spec/javascript/support/stimulus"; +import DialogController from "controllers/dialog_controller"; + +it("updates the text content of its element", async () => { + document.body.innerHTML = "
"; + await bootStimulus("dialog", DialogController); + + expect(document.body.textContent).toBe("Hello World!"); +}); diff --git a/spec/javascript/support/stimulus.ts b/spec/javascript/support/stimulus.ts new file mode 100644 index 000000000..c833fecb6 --- /dev/null +++ b/spec/javascript/support/stimulus.ts @@ -0,0 +1,27 @@ +import {afterEach} from "vitest"; +import type {Context, Controller} from "@hotwired/stimulus"; +import {Application} from "@hotwired/stimulus"; + +let application: Application | null = null; + +type ControllerClass = new (context: Context) => T; + +async function bootStimulus( + name: string, + controller: ControllerClass, +): Promise { + application ??= Application.start(); + + application.register(name, controller); + application.handleError = (error: Error): void => { throw error; }; + + await Promise.resolve(); +} + +afterEach(() => { + if (application) { application.stop(); } + + application = null; +}); + +export {bootStimulus}; diff --git a/tsconfig.json b/tsconfig.json index 840239704..a86233385 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": true, "skipLibCheck": true }, "exclude": ["app/assets/builds", "coverage", "node_modules", "public", "vendor"]