flowengine-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/branch-name.sh +49 -0
- data/.claude/commands/create-pr.md +93 -0
- data/.claude/commands/stash-unstaged.md +21 -0
- data/.claude/commands/unstash-unstaged.md +15 -0
- data/.claude/settings.json +72 -0
- data/.rubocop_todo.yml +17 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +153 -0
- data/LICENSE.txt +21 -0
- data/README.md +294 -0
- data/Rakefile +43 -0
- data/app/assets/javascripts/flow_engine/embed.js +44 -0
- data/app/assets/javascripts/flow_engine/progress_controller.js +17 -0
- data/app/assets/javascripts/flow_engine/step_controller.js +22 -0
- data/app/assets/stylesheets/flow_engine/application.css +569 -0
- data/app/controllers/flow_engine/admin/definitions_controller.rb +95 -0
- data/app/controllers/flow_engine/application_controller.rb +41 -0
- data/app/controllers/flow_engine/sessions_controller.rb +91 -0
- data/app/helpers/flow_engine/sessions_helper.rb +20 -0
- data/app/models/flow_engine/application_record.rb +8 -0
- data/app/models/flow_engine/flow_definition.rb +75 -0
- data/app/models/flow_engine/flow_session.rb +98 -0
- data/app/views/flow_engine/admin/definitions/_form.html.erb +27 -0
- data/app/views/flow_engine/admin/definitions/edit.html.erb +5 -0
- data/app/views/flow_engine/admin/definitions/index.html.erb +41 -0
- data/app/views/flow_engine/admin/definitions/mermaid.html.erb +9 -0
- data/app/views/flow_engine/admin/definitions/new.html.erb +5 -0
- data/app/views/flow_engine/admin/definitions/show.html.erb +25 -0
- data/app/views/flow_engine/sessions/completed.html.erb +34 -0
- data/app/views/flow_engine/sessions/new.html.erb +17 -0
- data/app/views/flow_engine/sessions/show.html.erb +26 -0
- data/app/views/flow_engine/sessions/steps/_boolean.html.erb +10 -0
- data/app/views/flow_engine/sessions/steps/_display.html.erb +4 -0
- data/app/views/flow_engine/sessions/steps/_multi_select.html.erb +8 -0
- data/app/views/flow_engine/sessions/steps/_number.html.erb +3 -0
- data/app/views/flow_engine/sessions/steps/_number_matrix.html.erb +13 -0
- data/app/views/flow_engine/sessions/steps/_single_select.html.erb +8 -0
- data/app/views/flow_engine/sessions/steps/_text.html.erb +11 -0
- data/app/views/flow_engine/sessions/steps/_unknown.html.erb +4 -0
- data/app/views/layouts/flow_engine/application.html.erb +26 -0
- data/app/views/layouts/flow_engine/embed.html.erb +30 -0
- data/config/routes.rb +22 -0
- data/db/migrate/01_create_flow_engine_definitions.rb +18 -0
- data/db/migrate/02_create_flow_engine_sessions.rb +18 -0
- data/exe/flowengine-rails +4 -0
- data/justfile +49 -0
- data/lefthook.yml +16 -0
- data/lib/flowengine/rails/configuration.rb +23 -0
- data/lib/flowengine/rails/dsl_loader.rb +35 -0
- data/lib/flowengine/rails/engine.rb +26 -0
- data/lib/flowengine/rails/version.rb +7 -0
- data/lib/flowengine/rails.rb +27 -0
- data/lib/generators/flow_engine/flow/flow_generator.rb +29 -0
- data/lib/generators/flow_engine/flow/templates/flow_definition.rb.tt +27 -0
- data/lib/generators/flow_engine/flow/templates/seed_task.rake.tt +22 -0
- data/lib/generators/flow_engine/install/install_generator.rb +34 -0
- data/lib/generators/flow_engine/install/templates/initializer.rb +25 -0
- data/log/.gitkeep +0 -0
- data/sig/flowengine/rails.rbs +6 -0
- metadata +164 -0
data/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Flowengine::Rails
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kigster/flowengine-rail/actions/workflows/rspec.yml) [](https://github.com/kigster/flowengine-rail/actions/workflows/rubocop.yml) [](https://github.com/kigster/flowengine-rail/actions/workflows/main.yml)
|
|
4
|
+
|
|
5
|
+
## Introduction
|
|
6
|
+
|
|
7
|
+
**FlowEngine::Rails** is a Rails Engine that wraps the [`flowengine`](https://github.com/kigster/flowengine) core gem with ActiveRecord persistence, a Hotwire-based wizard UI, an admin CRUD interface, and an iframe-embeddable widget. It allows non-technical users to define multi-step form flows via a Ruby DSL, store them in the database, and serve them as interactive step-by-step wizards.
|
|
8
|
+
|
|
9
|
+
Flows support seven step types: `text`, `number`, `boolean`, `single_select`, `multi_select`, `number_matrix`, and `display`. Conditional transitions between steps are driven by the core engine's rule evaluator (`if_rule:`, `contains()`, etc.).
|
|
10
|
+
|
|
11
|
+
### Key features
|
|
12
|
+
|
|
13
|
+
- **Admin UI** -- full CRUD for flow definitions with DSL validation and Mermaid diagram visualization
|
|
14
|
+
- **Wizard UI** -- Hotwire/Turbo-powered step-by-step form sessions with progress tracking
|
|
15
|
+
- **Iframe embedding** -- embed flows on external sites with auto-resizing and completion callbacks
|
|
16
|
+
- **Versioning** -- definitions are automatically versioned; once sessions exist, the definition becomes immutable
|
|
17
|
+
- **Completion callbacks** -- fire custom logic when a user finishes a flow
|
|
18
|
+
|
|
19
|
+
### Requirements
|
|
20
|
+
|
|
21
|
+
- Ruby >= 4.0.1
|
|
22
|
+
- Rails >= 8.1.2
|
|
23
|
+
- The [`flowengine`](https://github.com/kigster/flowengine) gem (`~> 0.1`)
|
|
24
|
+
|
|
25
|
+
## Usage and Integration into a Rails Application
|
|
26
|
+
|
|
27
|
+
### Installation
|
|
28
|
+
|
|
29
|
+
Add the gem to your `Gemfile`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem "flowengine-rails"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run the install generator, which copies migrations, creates an initializer, and mounts the engine:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bin/rails generate flow_engine:install
|
|
39
|
+
bin/rails db:migrate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This mounts the engine at `/flow_engine`. To change the mount point, edit `config/routes.rb`:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
mount FlowEngine::Rails::Engine => "/wizards"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Configuration
|
|
49
|
+
|
|
50
|
+
Edit `config/initializers/flow_engine.rb`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
FlowEngine::Rails.configure do |config|
|
|
54
|
+
# Origins allowed to embed flows via iframe (use ["*"] for all)
|
|
55
|
+
config.embed_allowed_origins = ["https://example.com"]
|
|
56
|
+
|
|
57
|
+
# Cache parsed DSL definitions in memory (recommended for production)
|
|
58
|
+
config.cache_definitions = Rails.env.production?
|
|
59
|
+
|
|
60
|
+
# Callback fired when a session completes
|
|
61
|
+
config.on_session_complete = ->(session) {
|
|
62
|
+
LeadNotifier.qualified(session).deliver_later
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Admin authentication -- method called as a before_action
|
|
66
|
+
config.admin_authentication_method = :authenticate_admin!
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Creating a flow definition
|
|
71
|
+
|
|
72
|
+
Use the generator to scaffold a flow definition file and a seed rake task:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bin/rails generate flow_engine:flow lead_qualification
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This creates `db/flow_definitions/lead_qualification.rb` with a starter DSL template. Edit it to define your flow:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
LEAD_QUALIFICATION_DSL = <<~RUBY
|
|
82
|
+
FlowEngine.define do
|
|
83
|
+
start :company_size
|
|
84
|
+
|
|
85
|
+
step :company_size do
|
|
86
|
+
type :single_select
|
|
87
|
+
question "How many employees does your company have?"
|
|
88
|
+
options ["1-10", "11-50", "51-200", "200+"]
|
|
89
|
+
transition to: :budget, if_rule: "contains(answer, '200')"
|
|
90
|
+
transition to: :industry
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
step :industry do
|
|
94
|
+
type :single_select
|
|
95
|
+
question "What industry are you in?"
|
|
96
|
+
options ["Technology", "Finance", "Healthcare", "Other"]
|
|
97
|
+
transition to: :budget
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
step :budget do
|
|
101
|
+
type :number
|
|
102
|
+
question "What is your annual budget for this service?"
|
|
103
|
+
transition to: :thank_you
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
step :thank_you do
|
|
107
|
+
type :display
|
|
108
|
+
question "Thank you! We will be in touch."
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
RUBY
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Seed it into the database:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
bin/rails flow_engine:seed:lead_qualification
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or create definitions programmatically:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
FlowEngine::FlowDefinition.create!(
|
|
124
|
+
name: "lead_qualification",
|
|
125
|
+
dsl: LEAD_QUALIFICATION_DSL
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The version auto-increments per name. To make it available to users, activate it:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
FlowEngine::FlowDefinition.latest_version("lead_qualification").activate!
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Admin interface
|
|
136
|
+
|
|
137
|
+
The admin UI is available at `/flow_engine/admin/definitions`. To protect it, set `admin_authentication_method` in the initializer to a method defined on your `ApplicationController`:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# app/controllers/application_controller.rb
|
|
141
|
+
class ApplicationController < ActionController::Base
|
|
142
|
+
def authenticate_admin!
|
|
143
|
+
redirect_to root_path unless current_user&.admin?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
From the admin UI you can create, edit, activate/deactivate definitions, and view Mermaid flow diagrams.
|
|
149
|
+
|
|
150
|
+
### Embedding flows on external sites
|
|
151
|
+
|
|
152
|
+
Add `embed.js` to the host page and call `FlowEngineEmbed.embed()`:
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<script src="https://yourapp.com/flow_engine/assets/flow_engine/embed.js"></script>
|
|
156
|
+
<div id="flow-container"></div>
|
|
157
|
+
<script>
|
|
158
|
+
FlowEngineEmbed.embed({
|
|
159
|
+
target: "#flow-container",
|
|
160
|
+
definitionId: 1,
|
|
161
|
+
baseUrl: "https://yourapp.com/flow_engine",
|
|
162
|
+
onComplete: function(data) {
|
|
163
|
+
console.log("Flow completed:", data);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
</script>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The iframe auto-resizes to fit content. Make sure to add the host origin to `embed_allowed_origins` in the configuration.
|
|
170
|
+
|
|
171
|
+
### Accessing session results
|
|
172
|
+
|
|
173
|
+
After a flow session completes, its answers are available as JSON:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
session = FlowEngine::FlowSession.completed.last
|
|
177
|
+
session.result_json
|
|
178
|
+
# => { definition_name: "lead_qualification",
|
|
179
|
+
# definition_version: 1,
|
|
180
|
+
# answers: { "company_size" => "51-200", "budget" => 50000 },
|
|
181
|
+
# history: ["company_size", "industry", "budget", "thank_you"],
|
|
182
|
+
# status: "completed",
|
|
183
|
+
# completed_at: "2026-03-16T..." }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Troubleshooting
|
|
187
|
+
|
|
188
|
+
### DSL validation errors on save
|
|
189
|
+
|
|
190
|
+
The `FlowDefinition` model validates that the DSL string parses correctly before saving. If you see `"dsl is invalid"` errors, check that your DSL is valid Ruby and uses the correct `FlowEngine.define { ... }` syntax. Test your DSL in a console:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
FlowEngine.load_dsl(your_dsl_string)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### "Cannot edit definition with existing sessions"
|
|
197
|
+
|
|
198
|
+
Once a `FlowDefinition` has associated `FlowSession` records, it becomes read-only to preserve data integrity. Create a new version instead:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
old = FlowEngine::FlowDefinition.find(id)
|
|
202
|
+
FlowEngine::FlowDefinition.create!(name: old.name, dsl: updated_dsl)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Iframe embedding returns no content
|
|
206
|
+
|
|
207
|
+
1. Verify the host origin is listed in `config.embed_allowed_origins`
|
|
208
|
+
1. Ensure the `?embed=true` query param is being passed (the embed script handles this automatically)
|
|
209
|
+
1. Check browser console for CORS errors
|
|
210
|
+
|
|
211
|
+
### Sessions stuck in `in_progress`
|
|
212
|
+
|
|
213
|
+
A session can be abandoned manually:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
FlowEngine::FlowSession.find(id).abandon!
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
To find stale sessions:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
FlowEngine::FlowSession.in_progress.where("updated_at < ?", 24.hours.ago)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Migration conflicts
|
|
226
|
+
|
|
227
|
+
If your app already has tables named `flow_engine_definitions` or `flow_engine_sessions`, the install migrations will conflict. Rename or drop the existing tables before running `bin/rails db:migrate`.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
### Setup
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
git clone https://github.com/kigster/flowengine-rails.git
|
|
235
|
+
cd flowengine-rails
|
|
236
|
+
bundle install
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The `flowengine` core gem is expected as a sibling directory for local development:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
flowengine-gems/
|
|
243
|
+
flowengine/ # core gem
|
|
244
|
+
flowengine-rails/ # this gem
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Running tests
|
|
248
|
+
|
|
249
|
+
Tests use an in-memory SQLite database -- no external database setup required:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
bundle exec rspec # run specs
|
|
253
|
+
bundle exec rubocop # run linter
|
|
254
|
+
bundle exec rake # both (default task)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
A dummy Rails app lives in `spec/dummy/` and is used by controller and integration specs.
|
|
258
|
+
|
|
259
|
+
### Project structure
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
app/
|
|
263
|
+
models/flow_engine/ # FlowDefinition, FlowSession
|
|
264
|
+
controllers/flow_engine/ # SessionsController, Admin::DefinitionsController
|
|
265
|
+
views/flow_engine/ # Wizard UI, admin UI, step partials
|
|
266
|
+
assets/ # embed.js, Stimulus controllers, CSS
|
|
267
|
+
|
|
268
|
+
lib/
|
|
269
|
+
flowengine/rails/ # Engine, Configuration, DslLoader
|
|
270
|
+
generators/flow_engine/ # install and flow generators
|
|
271
|
+
|
|
272
|
+
db/migrate/ # Definition and session tables
|
|
273
|
+
spec/ # RSpec suite with dummy app
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### CI
|
|
277
|
+
|
|
278
|
+
Three GitHub Actions workflows run on push:
|
|
279
|
+
|
|
280
|
+
- **main.yml** -- `bundle exec rake` on Ruby 3.4.4
|
|
281
|
+
- **rspec.yml** -- `bundle exec rspec` on Ruby 4.0 (checks out `flowengine` core alongside)
|
|
282
|
+
- **rubocop.yml** -- `bundle exec rubocop` on Ruby 4.0
|
|
283
|
+
|
|
284
|
+
### Contributing
|
|
285
|
+
|
|
286
|
+
1. Fork the repo
|
|
287
|
+
1. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
288
|
+
1. Commit your changes (keep commits atomic, one logical change per commit)
|
|
289
|
+
1. Ensure `bundle exec rake` passes
|
|
290
|
+
1. Open a pull request
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
Released under the [MIT License](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "rubocop/rake_task"
|
|
6
|
+
require "timeout"
|
|
7
|
+
require "yard"
|
|
8
|
+
|
|
9
|
+
def shell(*args)
|
|
10
|
+
puts "running: #{args.join(" ")}"
|
|
11
|
+
system(args.join(" "))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Clean all temporary files"
|
|
15
|
+
task :clean do
|
|
16
|
+
shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "Build the gem and install it locally"
|
|
20
|
+
task gem: [:build] do
|
|
21
|
+
shell("gem install pkg/*")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "Update unix permissions on the gem sources before packaging"
|
|
25
|
+
task permissions: [:clean] do
|
|
26
|
+
shell("chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*")
|
|
27
|
+
shell("find . -type d -exec chmod o+x,g+x {} \\;")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "Build the gem for distribution"
|
|
31
|
+
task build: :permissions
|
|
32
|
+
|
|
33
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
34
|
+
t.files = %w[lib/**/*.rb exe/*.rb - README.md LICENSE.txt CHANGELOG.md]
|
|
35
|
+
t.options.unshift("--title", '"FlowEngine — DSL + AST for buildiong complex flows in Ruby."')
|
|
36
|
+
t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
40
|
+
|
|
41
|
+
RuboCop::RakeTask.new
|
|
42
|
+
|
|
43
|
+
task default: %i[spec rubocop]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// FlowEngine Embed Script
|
|
2
|
+
// Usage: <script src="/flow_engine/assets/flow_engine/embed.js"></script>
|
|
3
|
+
// Then: FlowEngine.embed({ target: '#container', definitionId: 1, baseUrl: '/flow_engine' })
|
|
4
|
+
(function(global) {
|
|
5
|
+
var FlowEngineEmbed = {
|
|
6
|
+
embed: function(options) {
|
|
7
|
+
var target = typeof options.target === 'string'
|
|
8
|
+
? document.querySelector(options.target)
|
|
9
|
+
: options.target;
|
|
10
|
+
|
|
11
|
+
if (!target) {
|
|
12
|
+
console.error('FlowEngine: target element not found');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var baseUrl = options.baseUrl || '/flow_engine';
|
|
17
|
+
var src = baseUrl + '/sessions/new?definition_id=' + options.definitionId + '&embed=true';
|
|
18
|
+
|
|
19
|
+
var iframe = document.createElement('iframe');
|
|
20
|
+
iframe.src = src;
|
|
21
|
+
iframe.style.width = '100%';
|
|
22
|
+
iframe.style.border = 'none';
|
|
23
|
+
iframe.style.minHeight = '400px';
|
|
24
|
+
iframe.setAttribute('frameborder', '0');
|
|
25
|
+
|
|
26
|
+
target.appendChild(iframe);
|
|
27
|
+
|
|
28
|
+
window.addEventListener('message', function(event) {
|
|
29
|
+
if (event.data && event.data.type === 'flowengine:resize') {
|
|
30
|
+
iframe.style.height = event.data.height + 'px';
|
|
31
|
+
}
|
|
32
|
+
if (event.data && event.data.type === 'flowengine:completed') {
|
|
33
|
+
if (typeof options.onComplete === 'function') {
|
|
34
|
+
options.onComplete(event.data);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return iframe;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
global.FlowEngineEmbed = FlowEngineEmbed;
|
|
44
|
+
})(window);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Stimulus controller for progress bar animation
|
|
2
|
+
// data-controller="progress" data-progress-value="75"
|
|
3
|
+
(function() {
|
|
4
|
+
if (typeof window.Stimulus !== 'undefined') {
|
|
5
|
+
window.Stimulus.register("progress", class extends window.Stimulus.Controller {
|
|
6
|
+
static values = { value: Number }
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
const bar = this.element.querySelector('.fe-progress__bar');
|
|
10
|
+
if (bar) {
|
|
11
|
+
bar.style.transition = 'width 0.4s ease-in-out';
|
|
12
|
+
bar.style.width = this.valueValue + '%';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Stimulus controller for step forms — prevents double-submit
|
|
2
|
+
// data-controller="step"
|
|
3
|
+
(function() {
|
|
4
|
+
if (typeof window.Stimulus !== 'undefined') {
|
|
5
|
+
window.Stimulus.register("step", class extends window.Stimulus.Controller {
|
|
6
|
+
static targets = ["form", "submit"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
if (this.hasFormTarget) {
|
|
10
|
+
this.formTarget.addEventListener('submit', this.disableSubmit.bind(this));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
disableSubmit() {
|
|
15
|
+
if (this.hasSubmitTarget) {
|
|
16
|
+
this.submitTarget.disabled = true;
|
|
17
|
+
this.submitTarget.value = 'Submitting...';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
})();
|