bpmn-js-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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +6 -0
  3. data/app/assets/javascripts/bpmn-js/bpmn-modeler.production.min.js +199 -0
  4. data/app/assets/javascripts/bpmn-js/bpmn-viewer.production.min.js +22 -0
  5. data/app/assets/javascripts/dmn-js/dmn-modeler.production.min.js +194 -0
  6. data/app/assets/javascripts/dmn-js/dmn-viewer.production.min.js +16 -0
  7. data/app/assets/javascripts/form-js/form-editor.umd.js +78936 -0
  8. data/app/assets/javascripts/form-js/form-viewer.umd.js +54147 -0
  9. data/app/assets/stylesheets/bpmn-js/bpmn-font/css/bpmn-codes.css +108 -0
  10. data/app/assets/stylesheets/bpmn-js/bpmn-font/css/bpmn-embedded.css +166 -0
  11. data/app/assets/stylesheets/bpmn-js/bpmn-font/css/bpmn.css +164 -0
  12. data/app/assets/stylesheets/bpmn-js/bpmn-font/font/bpmn.eot +0 -0
  13. data/app/assets/stylesheets/bpmn-js/bpmn-font/font/bpmn.svg +224 -0
  14. data/app/assets/stylesheets/bpmn-js/bpmn-font/font/bpmn.ttf +0 -0
  15. data/app/assets/stylesheets/bpmn-js/bpmn-font/font/bpmn.woff +0 -0
  16. data/app/assets/stylesheets/bpmn-js/bpmn-font/font/bpmn.woff2 +0 -0
  17. data/app/assets/stylesheets/bpmn-js/bpmn-js.css +145 -0
  18. data/app/assets/stylesheets/bpmn-js/diagram-js.css +1011 -0
  19. data/app/assets/stylesheets/dmn-js/diagram-js.css +1011 -0
  20. data/app/assets/stylesheets/dmn-js/dmn-font/css/dmn-codes.css +41 -0
  21. data/app/assets/stylesheets/dmn-js/dmn-font/css/dmn-embedded.css +98 -0
  22. data/app/assets/stylesheets/dmn-js/dmn-font/css/dmn.css +97 -0
  23. data/app/assets/stylesheets/dmn-js/dmn-font/font/dmn.eot +0 -0
  24. data/app/assets/stylesheets/dmn-js/dmn-font/font/dmn.svg +90 -0
  25. data/app/assets/stylesheets/dmn-js/dmn-font/font/dmn.ttf +0 -0
  26. data/app/assets/stylesheets/dmn-js/dmn-font/font/dmn.woff +0 -0
  27. data/app/assets/stylesheets/dmn-js/dmn-font/font/dmn.woff2 +0 -0
  28. data/app/assets/stylesheets/dmn-js/dmn-js-boxed-expression-controls.css +130 -0
  29. data/app/assets/stylesheets/dmn-js/dmn-js-boxed-expression.css +186 -0
  30. data/app/assets/stylesheets/dmn-js/dmn-js-decision-table-controls.css +301 -0
  31. data/app/assets/stylesheets/dmn-js/dmn-js-decision-table.css +580 -0
  32. data/app/assets/stylesheets/dmn-js/dmn-js-drd.css +95 -0
  33. data/app/assets/stylesheets/dmn-js/dmn-js-literal-expression.css +165 -0
  34. data/app/assets/stylesheets/dmn-js/dmn-js-shared.css +221 -0
  35. data/app/assets/stylesheets/form-js/form-js-editor.css +2342 -0
  36. data/app/assets/stylesheets/form-js/form-js.css +3694 -0
  37. data/app/helpers/bpmn_js_rails/view_helper.rb +291 -0
  38. data/app/javascript/bpmn_js_rails/controllers/bpmn_js_modeler_controller.js +85 -0
  39. data/app/javascript/bpmn_js_rails/controllers/bpmn_js_viewer_controller.js +46 -0
  40. data/app/javascript/bpmn_js_rails/controllers/dmn_js_modeler_controller.js +102 -0
  41. data/app/javascript/bpmn_js_rails/controllers/dmn_js_viewer_controller.js +45 -0
  42. data/app/javascript/bpmn_js_rails/controllers/form_js_editor_controller.js +80 -0
  43. data/app/javascript/bpmn_js_rails/controllers/form_js_viewer_controller.js +52 -0
  44. data/app/javascript/bpmn_js_rails/index.js +34 -0
  45. data/app/models/bpmn_js_rails/decision.rb +60 -0
  46. data/app/models/bpmn_js_rails/diagram.rb +51 -0
  47. data/app/models/bpmn_js_rails/form.rb +32 -0
  48. data/config/importmap.rb +7 -0
  49. data/config/routes.rb +2 -0
  50. data/db/migrate/20260319000001_create_bpmn_js_rails_diagrams.rb +15 -0
  51. data/db/migrate/20260319000002_create_bpmn_js_rails_forms.rb +15 -0
  52. data/db/migrate/20260319000003_create_bpmn_js_rails_decisions.rb +15 -0
  53. data/lib/bpmn-js-rails.rb +1 -0
  54. data/lib/bpmn_js_rails/engine.rb +30 -0
  55. data/lib/bpmn_js_rails/version.rb +3 -0
  56. data/lib/bpmn_js_rails/version.rb.erb +3 -0
  57. data/lib/bpmn_js_rails.rb +5 -0
  58. data/lib/generators/bpmn_js_rails/install/install_generator.rb +58 -0
  59. data/lib/generators/bpmn_js_rails/install/templates/create_bpmn_js_rails_decisions.rb.erb +15 -0
  60. data/lib/generators/bpmn_js_rails/install/templates/create_bpmn_js_rails_diagrams.rb.erb +15 -0
  61. data/lib/generators/bpmn_js_rails/install/templates/create_bpmn_js_rails_forms.rb.erb +15 -0
  62. data/lib/tasks/form_js_rails_tasks.rake +4 -0
  63. metadata +116 -0
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BpmnJsRails
4
+ module ViewHelper
5
+ # ── form-js assets ───────────────────────────────────────────────
6
+
7
+ # Render <link> and <script> tags for form-js assets.
8
+ #
9
+ # <%= form_js_assets %> # viewer only
10
+ # <%= form_js_assets(:viewer, :editor) %> # both viewer and editor
11
+ #
12
+ def form_js_assets(*components)
13
+ components = [ :viewer ] if components.empty?
14
+ tags = []
15
+
16
+ tags << tag.link(
17
+ rel: "stylesheet",
18
+ href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,600;1,400&display=swap"
19
+ )
20
+
21
+ tags << stylesheet_link_tag("form-js/form-js", "data-turbo-track": "reload")
22
+
23
+ if components.include?(:editor)
24
+ tags << stylesheet_link_tag("form-js/form-js-editor", "data-turbo-track": "reload")
25
+ end
26
+
27
+ if components.include?(:viewer)
28
+ tags << javascript_include_tag("form-js/form-viewer.umd", "data-turbo-track": "reload")
29
+ end
30
+
31
+ if components.include?(:editor)
32
+ tags << javascript_include_tag("form-js/form-editor.umd", "data-turbo-track": "reload")
33
+ end
34
+
35
+ safe_join(tags, "\n")
36
+ end
37
+
38
+ # Render a form-js viewer that displays/fills a form.
39
+ #
40
+ # <%= form_js_viewer(@form) %>
41
+ # <%= form_js_viewer(@form, data: { name: "John" }) %>
42
+ # <%= form_js_viewer({ type: "default", components: [...] }) %>
43
+ #
44
+ def form_js_viewer(form_or_schema, data: {}, **html_options)
45
+ schema = extract_schema(form_or_schema)
46
+
47
+ html_options[:class] = Array(html_options[:class]).push("form-js-viewer-container").join(" ")
48
+ html_options[:style] = [ html_options[:style], "min-height: 200px" ].compact.join("; ")
49
+
50
+ html_options[:data] = (html_options[:data] || {}).merge(
51
+ controller: "form-js-viewer",
52
+ form_js_viewer_schema_value: schema.to_json,
53
+ form_js_viewer_data_value: data.to_json
54
+ )
55
+
56
+ content_tag(:div, "", **html_options)
57
+ end
58
+
59
+ # Render a form-js editor for designing forms.
60
+ #
61
+ # <%= form_js_editor(@form) %>
62
+ # <%= form_js_editor(@form, field_name: "form[schema]") %>
63
+ #
64
+ def form_js_editor(form_or_schema, field_name: nil, **html_options)
65
+ schema = extract_schema(form_or_schema)
66
+
67
+ html_options[:class] = Array(html_options[:class]).push("form-js-editor-container").join(" ")
68
+
69
+ html_options[:data] = (html_options[:data] || {}).merge(
70
+ controller: "form-js-editor",
71
+ form_js_editor_schema_value: schema.to_json
72
+ )
73
+
74
+ content_tag(:div, **html_options) do
75
+ children = []
76
+ children << content_tag(:div, "", style: "min-height: 500px", data: { form_js_editor_target: "container" })
77
+
78
+ if field_name
79
+ children << tag.input(
80
+ type: "hidden",
81
+ name: field_name,
82
+ value: schema.to_json,
83
+ data: { form_js_editor_target: "schemaField" }
84
+ )
85
+ end
86
+
87
+ safe_join(children)
88
+ end
89
+ end
90
+
91
+ # ── bpmn-js assets ───────────────────────────────────────────────
92
+
93
+ # Render <link> and <script> tags for bpmn-js assets.
94
+ #
95
+ # <%= bpmn_js_assets %> # viewer only
96
+ # <%= bpmn_js_assets(:viewer, :modeler) %> # both viewer and modeler
97
+ #
98
+ def bpmn_js_assets(*components)
99
+ components = [ :viewer ] if components.empty?
100
+ tags = []
101
+
102
+ tags << stylesheet_link_tag("bpmn-js/diagram-js", "data-turbo-track": "reload")
103
+ tags << stylesheet_link_tag("bpmn-js/bpmn-js", "data-turbo-track": "reload")
104
+ tags << stylesheet_link_tag("bpmn-js/bpmn-font/css/bpmn-embedded", "data-turbo-track": "reload")
105
+
106
+ if components.include?(:viewer) && !components.include?(:modeler)
107
+ tags << javascript_include_tag("bpmn-js/bpmn-viewer.production.min", "data-turbo-track": "reload")
108
+ end
109
+
110
+ if components.include?(:modeler)
111
+ tags << javascript_include_tag("bpmn-js/bpmn-modeler.production.min", "data-turbo-track": "reload")
112
+ end
113
+
114
+ safe_join(tags, "\n")
115
+ end
116
+
117
+ # Render a bpmn-js viewer that displays a BPMN diagram (read-only).
118
+ #
119
+ # <%= bpmn_js_viewer(@diagram) %>
120
+ # <%= bpmn_js_viewer("<bpmn:definitions ...>...</bpmn:definitions>") %>
121
+ #
122
+ def bpmn_js_viewer(diagram_or_xml, **html_options)
123
+ xml = extract_xml(diagram_or_xml)
124
+
125
+ html_options[:class] = Array(html_options[:class]).push("bpmn-js-viewer-container").join(" ")
126
+ html_options[:style] = [ html_options[:style], "height: 500px" ].compact.join("; ")
127
+
128
+ html_options[:data] = (html_options[:data] || {}).merge(
129
+ controller: "bpmn-js-viewer",
130
+ bpmn_js_viewer_xml_value: xml
131
+ )
132
+
133
+ content_tag(:div, "", **html_options)
134
+ end
135
+
136
+ # Render a bpmn-js modeler for designing BPMN diagrams.
137
+ #
138
+ # <%= bpmn_js_modeler(@diagram) %>
139
+ # <%= bpmn_js_modeler(@diagram, field_name: "diagram[xml]") %>
140
+ #
141
+ def bpmn_js_modeler(diagram_or_xml, field_name: nil, **html_options)
142
+ xml = extract_xml(diagram_or_xml)
143
+
144
+ html_options[:class] = Array(html_options[:class]).push("bpmn-js-modeler-container").join(" ")
145
+
146
+ html_options[:data] = (html_options[:data] || {}).merge(
147
+ controller: "bpmn-js-modeler",
148
+ bpmn_js_modeler_xml_value: xml
149
+ )
150
+
151
+ content_tag(:div, **html_options) do
152
+ children = []
153
+ children << content_tag(:div, "", style: "height: 500px", data: { bpmn_js_modeler_target: "container" })
154
+
155
+ if field_name
156
+ children << tag.input(
157
+ type: "hidden",
158
+ name: field_name,
159
+ value: xml,
160
+ data: { bpmn_js_modeler_target: "xmlField" }
161
+ )
162
+ end
163
+
164
+ safe_join(children)
165
+ end
166
+ end
167
+
168
+ # ── dmn-js assets ────────────────────────────────────────────────
169
+
170
+ # Render <link> and <script> tags for dmn-js assets.
171
+ #
172
+ # <%= dmn_js_assets %> # viewer only
173
+ # <%= dmn_js_assets(:viewer, :modeler) %> # both viewer and modeler
174
+ #
175
+ def dmn_js_assets(*components)
176
+ components = [ :viewer ] if components.empty?
177
+ tags = []
178
+
179
+ tags << stylesheet_link_tag("dmn-js/diagram-js", "data-turbo-track": "reload")
180
+ tags << stylesheet_link_tag("dmn-js/dmn-js-shared", "data-turbo-track": "reload")
181
+ tags << stylesheet_link_tag("dmn-js/dmn-js-drd", "data-turbo-track": "reload")
182
+ tags << stylesheet_link_tag("dmn-js/dmn-js-decision-table", "data-turbo-track": "reload")
183
+ tags << stylesheet_link_tag("dmn-js/dmn-js-literal-expression", "data-turbo-track": "reload")
184
+ tags << stylesheet_link_tag("dmn-js/dmn-js-boxed-expression", "data-turbo-track": "reload")
185
+ tags << stylesheet_link_tag("dmn-js/dmn-font/css/dmn-embedded", "data-turbo-track": "reload")
186
+
187
+ if components.include?(:modeler)
188
+ tags << stylesheet_link_tag("dmn-js/dmn-js-decision-table-controls", "data-turbo-track": "reload")
189
+ tags << stylesheet_link_tag("dmn-js/dmn-js-boxed-expression-controls", "data-turbo-track": "reload")
190
+ end
191
+
192
+ if components.include?(:viewer) && !components.include?(:modeler)
193
+ tags << javascript_include_tag("dmn-js/dmn-viewer.production.min", "data-turbo-track": "reload")
194
+ end
195
+
196
+ if components.include?(:modeler)
197
+ tags << javascript_include_tag("dmn-js/dmn-modeler.production.min", "data-turbo-track": "reload")
198
+ end
199
+
200
+ safe_join(tags, "\n")
201
+ end
202
+
203
+ # Render a dmn-js viewer that displays a DMN decision (read-only).
204
+ #
205
+ # <%= dmn_js_viewer(@decision) %>
206
+ # <%= dmn_js_viewer("<definitions ...>...</definitions>") %>
207
+ #
208
+ def dmn_js_viewer(decision_or_xml, **html_options)
209
+ xml = extract_dmn_xml(decision_or_xml)
210
+
211
+ html_options[:class] = Array(html_options[:class]).push("dmn-js-viewer-container").join(" ")
212
+ html_options[:style] = [ html_options[:style], "height: 500px" ].compact.join("; ")
213
+
214
+ html_options[:data] = (html_options[:data] || {}).merge(
215
+ controller: "dmn-js-viewer",
216
+ dmn_js_viewer_xml_value: xml
217
+ )
218
+
219
+ content_tag(:div, "", **html_options)
220
+ end
221
+
222
+ # Render a dmn-js modeler for designing DMN decisions.
223
+ #
224
+ # <%= dmn_js_modeler(@decision) %>
225
+ # <%= dmn_js_modeler(@decision, field_name: "decision[xml]") %>
226
+ #
227
+ def dmn_js_modeler(decision_or_xml, field_name: nil, **html_options)
228
+ xml = extract_dmn_xml(decision_or_xml)
229
+
230
+ html_options[:class] = Array(html_options[:class]).push("dmn-js-modeler-container").join(" ")
231
+
232
+ html_options[:data] = (html_options[:data] || {}).merge(
233
+ controller: "dmn-js-modeler",
234
+ dmn_js_modeler_xml_value: xml
235
+ )
236
+
237
+ content_tag(:div, **html_options) do
238
+ children = []
239
+ children << content_tag(:div, "", style: "height: 500px", data: { dmn_js_modeler_target: "container" })
240
+
241
+ if field_name
242
+ children << tag.input(
243
+ type: "hidden",
244
+ name: field_name,
245
+ value: xml,
246
+ data: { dmn_js_modeler_target: "xmlField" }
247
+ )
248
+ end
249
+
250
+ safe_join(children)
251
+ end
252
+ end
253
+
254
+ private
255
+
256
+ def extract_schema(form_or_schema)
257
+ case form_or_schema
258
+ when BpmnJsRails::Form
259
+ form_or_schema.schema || { "type" => "default", "components" => [] }
260
+ when Hash
261
+ form_or_schema
262
+ when String
263
+ JSON.parse(form_or_schema)
264
+ else
265
+ form_or_schema.try(:schema) || { "type" => "default", "components" => [] }
266
+ end
267
+ end
268
+
269
+ def extract_xml(diagram_or_xml)
270
+ case diagram_or_xml
271
+ when BpmnJsRails::Diagram
272
+ diagram_or_xml.xml || BpmnJsRails::Diagram::DEFAULT_XML
273
+ when String
274
+ diagram_or_xml
275
+ else
276
+ diagram_or_xml.try(:xml) || BpmnJsRails::Diagram::DEFAULT_XML
277
+ end
278
+ end
279
+
280
+ def extract_dmn_xml(decision_or_xml)
281
+ case decision_or_xml
282
+ when BpmnJsRails::Decision
283
+ decision_or_xml.xml || BpmnJsRails::Decision::DEFAULT_XML
284
+ when String
285
+ decision_or_xml
286
+ else
287
+ decision_or_xml.try(:xml) || BpmnJsRails::Decision::DEFAULT_XML
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,85 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="bpmn-js-modeler"
4
+ //
5
+ // Values:
6
+ // xml (String) - the BPMN 2.0 XML to load into the modeler
7
+ //
8
+ // Targets:
9
+ // container - the child div that bpmn-js renders into (falls back to this.element)
10
+ // xmlField - a hidden <input> that receives the serialised XML
11
+ // on every change and before form submission
12
+ //
13
+ // Lifecycle:
14
+ // connect: creates the BpmnJS modeler instance, imports XML, wires change + submit sync
15
+ // disconnect: destroys the modeler
16
+ //
17
+ // Events dispatched on the controller element:
18
+ // bpmn-js-modeler:ready - { detail: { modeler } }
19
+ // bpmn-js-modeler:changed - after XML sync
20
+ //
21
+ export default class extends Controller {
22
+ static values = {
23
+ xml: { type: String, default: "" }
24
+ }
25
+
26
+ static targets = ["container", "xmlField"]
27
+
28
+ async connect() {
29
+ if (typeof BpmnJS === "undefined") {
30
+ console.error("[bpmn-js-rails] BpmnJS global not found. Make sure bpmn-modeler.production.min.js is loaded.")
31
+ return
32
+ }
33
+
34
+ const renderTarget = this.hasContainerTarget ? this.containerTarget : this.element
35
+
36
+ this.modeler = new BpmnJS({ container: renderTarget })
37
+
38
+ if (this.xmlValue) {
39
+ try {
40
+ await this.modeler.importXML(this.xmlValue)
41
+ this.modeler.get("canvas").zoom("fit-viewport")
42
+ } catch (err) {
43
+ console.error("[bpmn-js-rails] Failed to import XML:", err)
44
+ }
45
+ }
46
+
47
+ this.modeler.on("commandStack.changed", () => {
48
+ this._syncXmlField()
49
+ this.dispatch("changed")
50
+ })
51
+
52
+ // Also sync right before the enclosing <form> submits
53
+ this._form = this.element.closest("form")
54
+ if (this._form) {
55
+ this._boundSubmitHandler = () => this._syncXmlField()
56
+ this._form.addEventListener("submit", this._boundSubmitHandler)
57
+ }
58
+
59
+ this.dispatch("ready", { detail: { modeler: this.modeler } })
60
+ }
61
+
62
+ disconnect() {
63
+ if (this._form && this._boundSubmitHandler) {
64
+ this._form.removeEventListener("submit", this._boundSubmitHandler)
65
+ this._boundSubmitHandler = null
66
+ this._form = null
67
+ }
68
+
69
+ if (this.modeler) {
70
+ this.modeler.destroy()
71
+ this.modeler = null
72
+ }
73
+ }
74
+
75
+ // Serialise the current modeler XML into the hidden field target.
76
+ async _syncXmlField() {
77
+ if (!this.modeler || !this.hasXmlFieldTarget) return
78
+ try {
79
+ const { xml } = await this.modeler.saveXML({ format: true })
80
+ this.xmlFieldTarget.value = xml
81
+ } catch (_) {
82
+ // modeler may be mid-import; ignore
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,46 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="bpmn-js-viewer"
4
+ //
5
+ // Values:
6
+ // xml (String) - the BPMN 2.0 XML to display
7
+ //
8
+ // Lifecycle:
9
+ // connect: creates the BpmnJS viewer instance, imports XML
10
+ // disconnect: destroys it (cleans up DOM + listeners)
11
+ //
12
+ // Events dispatched on the controller element:
13
+ // bpmn-js-viewer:loaded - { detail: { warnings } }
14
+ // bpmn-js-viewer:error - { detail: { error } }
15
+ //
16
+ export default class extends Controller {
17
+ static values = {
18
+ xml: { type: String, default: "" }
19
+ }
20
+
21
+ async connect() {
22
+ if (typeof BpmnJS === "undefined") {
23
+ console.error("[bpmn-js-rails] BpmnJS global not found. Make sure bpmn-viewer.production.min.js or bpmn-modeler.production.min.js is loaded.")
24
+ return
25
+ }
26
+
27
+ this.viewer = new BpmnJS({ container: this.element })
28
+
29
+ if (this.xmlValue) {
30
+ try {
31
+ const result = await this.viewer.importXML(this.xmlValue)
32
+ this.viewer.get("canvas").zoom("fit-viewport")
33
+ this.dispatch("loaded", { detail: { warnings: result.warnings } })
34
+ } catch (err) {
35
+ this.dispatch("error", { detail: { error: err } })
36
+ }
37
+ }
38
+ }
39
+
40
+ disconnect() {
41
+ if (this.viewer) {
42
+ this.viewer.destroy()
43
+ this.viewer = null
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,102 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="dmn-js-modeler"
4
+ //
5
+ // Values:
6
+ // xml (String) - the DMN 1.3 XML to load into the modeler
7
+ //
8
+ // Targets:
9
+ // container - the child div that dmn-js renders into (falls back to this.element)
10
+ // xmlField - a hidden <input> that receives the serialised XML
11
+ // on every change and before form submission
12
+ //
13
+ // Lifecycle:
14
+ // connect: creates the DmnJS modeler instance, imports XML, wires change + submit sync
15
+ // disconnect: destroys the modeler
16
+ //
17
+ // Events dispatched on the controller element:
18
+ // dmn-js-modeler:ready - { detail: { modeler } }
19
+ // dmn-js-modeler:changed - after XML sync
20
+ //
21
+ export default class extends Controller {
22
+ static values = {
23
+ xml: { type: String, default: "" }
24
+ }
25
+
26
+ static targets = ["container", "xmlField"]
27
+
28
+ async connect() {
29
+ if (typeof DmnJS === "undefined") {
30
+ console.error("[bpmn-js-rails] DmnJS global not found. Make sure dmn-modeler.production.min.js is loaded.")
31
+ return
32
+ }
33
+
34
+ const renderTarget = this.hasContainerTarget ? this.containerTarget : this.element
35
+
36
+ this.modeler = new DmnJS({
37
+ container: renderTarget,
38
+ keyboard: { bindTo: window }
39
+ })
40
+
41
+ if (this.xmlValue) {
42
+ try {
43
+ await this.modeler.importXML(this.xmlValue)
44
+ } catch (err) {
45
+ console.error("[bpmn-js-rails] Failed to import DMN XML:", err)
46
+ }
47
+ }
48
+
49
+ // Listen for changes on the active viewer's command stack
50
+ this.modeler.on("views.changed", ({ activeView }) => {
51
+ const activeViewer = this.modeler.getActiveViewer()
52
+ if (activeViewer && this._currentViewer !== activeViewer) {
53
+ this._currentViewer = activeViewer
54
+
55
+ try {
56
+ const eventBus = activeViewer.get("eventBus")
57
+ eventBus.on("commandStack.changed", () => {
58
+ this._syncXmlField()
59
+ this.dispatch("changed")
60
+ })
61
+ } catch (_) {
62
+ // some view types may not have a commandStack
63
+ }
64
+ }
65
+ })
66
+
67
+ // Also sync right before the enclosing <form> submits
68
+ this._form = this.element.closest("form")
69
+ if (this._form) {
70
+ this._boundSubmitHandler = () => this._syncXmlField()
71
+ this._form.addEventListener("submit", this._boundSubmitHandler)
72
+ }
73
+
74
+ this.dispatch("ready", { detail: { modeler: this.modeler } })
75
+ }
76
+
77
+ disconnect() {
78
+ if (this._form && this._boundSubmitHandler) {
79
+ this._form.removeEventListener("submit", this._boundSubmitHandler)
80
+ this._boundSubmitHandler = null
81
+ this._form = null
82
+ }
83
+
84
+ this._currentViewer = null
85
+
86
+ if (this.modeler) {
87
+ this.modeler.destroy()
88
+ this.modeler = null
89
+ }
90
+ }
91
+
92
+ // Serialise the current modeler XML into the hidden field target.
93
+ async _syncXmlField() {
94
+ if (!this.modeler || !this.hasXmlFieldTarget) return
95
+ try {
96
+ const { xml } = await this.modeler.saveXML({ format: true })
97
+ this.xmlFieldTarget.value = xml
98
+ } catch (_) {
99
+ // modeler may be mid-import; ignore
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,45 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="dmn-js-viewer"
4
+ //
5
+ // Values:
6
+ // xml (String) - the DMN 1.3 XML to display
7
+ //
8
+ // Lifecycle:
9
+ // connect: creates the DmnJS viewer instance, imports XML
10
+ // disconnect: destroys it (cleans up DOM + listeners)
11
+ //
12
+ // Events dispatched on the controller element:
13
+ // dmn-js-viewer:loaded - { detail: { warnings } }
14
+ // dmn-js-viewer:error - { detail: { error } }
15
+ //
16
+ export default class extends Controller {
17
+ static values = {
18
+ xml: { type: String, default: "" }
19
+ }
20
+
21
+ async connect() {
22
+ if (typeof DmnJS === "undefined") {
23
+ console.error("[bpmn-js-rails] DmnJS global not found. Make sure dmn-viewer.production.min.js or dmn-modeler.production.min.js is loaded.")
24
+ return
25
+ }
26
+
27
+ this.viewer = new DmnJS({ container: this.element })
28
+
29
+ if (this.xmlValue) {
30
+ try {
31
+ const result = await this.viewer.importXML(this.xmlValue)
32
+ this.dispatch("loaded", { detail: { warnings: result.warnings } })
33
+ } catch (err) {
34
+ this.dispatch("error", { detail: { error: err } })
35
+ }
36
+ }
37
+ }
38
+
39
+ disconnect() {
40
+ if (this.viewer) {
41
+ this.viewer.destroy()
42
+ this.viewer = null
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,80 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="form-js-editor"
4
+ //
5
+ // Values:
6
+ // schema (Object) - the form-js JSON schema to load into the editor
7
+ //
8
+ // Targets:
9
+ // container - the child div that form-js renders into (falls back to this.element)
10
+ // schemaField - a hidden <input> that receives the serialised schema JSON
11
+ // on every change and before form submission
12
+ //
13
+ // Lifecycle:
14
+ // connect: creates the FormEditor instance, wires change + submit sync
15
+ // disconnect: destroys the editor
16
+ //
17
+ // Events dispatched on the controller element:
18
+ // form-js:editor:ready - { detail: { editor } }
19
+ // form-js:editor:changed - after schema sync
20
+ //
21
+ export default class extends Controller {
22
+ static values = {
23
+ schema: { type: Object, default: { type: "default", components: [] } }
24
+ }
25
+
26
+ static targets = ["container", "schemaField"]
27
+
28
+ connect() {
29
+ if (typeof FormEditor === "undefined") {
30
+ console.error("[bpmn-js-rails] FormEditor global not found. Make sure form-editor.umd.js is loaded.")
31
+ return
32
+ }
33
+
34
+ const renderTarget = this.hasContainerTarget ? this.containerTarget : this.element
35
+
36
+ FormEditor.createFormEditor({
37
+ container: renderTarget,
38
+ schema: this.schemaValue
39
+ }).then((editor) => {
40
+ this.editor = editor
41
+
42
+ editor.on("changed", () => {
43
+ this._syncSchemaField()
44
+ this.dispatch("changed")
45
+ })
46
+
47
+ // Also sync right before the enclosing <form> submits
48
+ this._form = this.element.closest("form")
49
+ if (this._form) {
50
+ this._boundSubmitHandler = () => this._syncSchemaField()
51
+ this._form.addEventListener("submit", this._boundSubmitHandler)
52
+ }
53
+
54
+ this.dispatch("ready", { detail: { editor } })
55
+ })
56
+ }
57
+
58
+ disconnect() {
59
+ if (this._form && this._boundSubmitHandler) {
60
+ this._form.removeEventListener("submit", this._boundSubmitHandler)
61
+ this._boundSubmitHandler = null
62
+ this._form = null
63
+ }
64
+
65
+ if (this.editor) {
66
+ this.editor.destroy()
67
+ this.editor = null
68
+ }
69
+ }
70
+
71
+ // Serialise the current editor schema into the hidden field target.
72
+ _syncSchemaField() {
73
+ if (!this.editor || !this.hasSchemaFieldTarget) return
74
+ try {
75
+ this.schemaFieldTarget.value = JSON.stringify(this.editor.saveSchema())
76
+ } catch (_) {
77
+ // editor may be mid-import; ignore
78
+ }
79
+ }
80
+ }