compony 0.11.8 → 0.11.9

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +36 -1
  3. data/CHANGELOG.md +31 -0
  4. data/CLAUDE.md +85 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +13 -3
  7. data/VERSION +1 -1
  8. data/compony.gemspec +3 -3
  9. data/doc/ComponentGenerator.html +1 -1
  10. data/doc/Components.html +1 -1
  11. data/doc/ComponentsGenerator.html +1 -1
  12. data/doc/Compony/Component.html +54 -54
  13. data/doc/Compony/ComponentMixins/Default/Labelling.html +1 -1
  14. data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
  15. data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +109 -70
  16. data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +64 -28
  17. data/doc/Compony/ComponentMixins/Default/Standalone.html +1 -1
  18. data/doc/Compony/ComponentMixins/Default.html +1 -1
  19. data/doc/Compony/ComponentMixins/Resourceful.html +213 -74
  20. data/doc/Compony/ComponentMixins.html +1 -1
  21. data/doc/Compony/Components/Buttons/CssButton.html +1 -1
  22. data/doc/Compony/Components/Buttons/Link.html +1 -1
  23. data/doc/Compony/Components/Buttons.html +1 -1
  24. data/doc/Compony/Components/Destroy.html +83 -29
  25. data/doc/Compony/Components/Edit.html +110 -38
  26. data/doc/Compony/Components/Form.html +551 -208
  27. data/doc/Compony/Components/Index.html +1 -1
  28. data/doc/Compony/Components/List.html +3 -3
  29. data/doc/Compony/Components/New.html +110 -38
  30. data/doc/Compony/Components/Show.html +1 -1
  31. data/doc/Compony/Components/WithForm.html +194 -47
  32. data/doc/Compony/Components.html +1 -1
  33. data/doc/Compony/ControllerMixin.html +1 -1
  34. data/doc/Compony/Engine.html +1 -1
  35. data/doc/Compony/Intent.html +2 -2
  36. data/doc/Compony/ManageIntentsDsl.html +1 -1
  37. data/doc/Compony/MethodAccessibleHash.html +1 -1
  38. data/doc/Compony/ModelFields/Anchormodel.html +1 -1
  39. data/doc/Compony/ModelFields/Association.html +1 -1
  40. data/doc/Compony/ModelFields/Attachment.html +1 -1
  41. data/doc/Compony/ModelFields/Base.html +1 -1
  42. data/doc/Compony/ModelFields/Boolean.html +1 -1
  43. data/doc/Compony/ModelFields/Color.html +1 -1
  44. data/doc/Compony/ModelFields/Currency.html +1 -1
  45. data/doc/Compony/ModelFields/Date.html +1 -1
  46. data/doc/Compony/ModelFields/Datetime.html +1 -1
  47. data/doc/Compony/ModelFields/Decimal.html +1 -1
  48. data/doc/Compony/ModelFields/Email.html +1 -1
  49. data/doc/Compony/ModelFields/Float.html +1 -1
  50. data/doc/Compony/ModelFields/Integer.html +1 -1
  51. data/doc/Compony/ModelFields/Percentage.html +1 -1
  52. data/doc/Compony/ModelFields/Phone.html +1 -1
  53. data/doc/Compony/ModelFields/RichText.html +1 -1
  54. data/doc/Compony/ModelFields/String.html +1 -1
  55. data/doc/Compony/ModelFields/Text.html +1 -1
  56. data/doc/Compony/ModelFields/Time.html +1 -1
  57. data/doc/Compony/ModelFields/Url.html +1 -1
  58. data/doc/Compony/ModelFields.html +1 -1
  59. data/doc/Compony/ModelMixin.html +1 -1
  60. data/doc/Compony/NaturalOrdering.html +1 -1
  61. data/doc/Compony/RequestContext.html +1 -1
  62. data/doc/Compony/Version.html +1 -1
  63. data/doc/Compony/ViewHelpers.html +1 -1
  64. data/doc/Compony/VirtualModel.html +1 -1
  65. data/doc/Compony.html +1 -1
  66. data/doc/ComponyController.html +1 -1
  67. data/doc/_index.html +97 -1
  68. data/doc/file.CHANGELOG.html +758 -0
  69. data/doc/file.README.html +25 -4
  70. data/doc/file.basic_component.html +314 -0
  71. data/doc/file.cookbook.html +189 -0
  72. data/doc/file.destroy.html +105 -0
  73. data/doc/file.dsl_reference.html +672 -0
  74. data/doc/file.edit.html +109 -0
  75. data/doc/file.example.html +291 -0
  76. data/doc/file.example_advanced.html +257 -0
  77. data/doc/file.feasibility.html +115 -0
  78. data/doc/file.form.html +195 -0
  79. data/doc/file.generators.html +89 -0
  80. data/doc/file.glossary.html +217 -0
  81. data/doc/file.gotchas.html +222 -0
  82. data/doc/file.index.html +135 -0
  83. data/doc/file.inheritance.html +136 -0
  84. data/doc/file.installation.html +115 -0
  85. data/doc/file.integrations.html +218 -0
  86. data/doc/file.intents.html +265 -0
  87. data/doc/file.internal_datastructures.html +129 -0
  88. data/doc/file.list.html +253 -0
  89. data/doc/file.maintaining.html +127 -0
  90. data/doc/file.model_fields.html +137 -0
  91. data/doc/file.nesting.html +237 -0
  92. data/doc/file.new.html +109 -0
  93. data/doc/file.ownership.html +98 -0
  94. data/doc/file.patterns.html +669 -0
  95. data/doc/file.pre_built_components.html +99 -0
  96. data/doc/file.resourceful.html +181 -0
  97. data/doc/file.show.html +158 -0
  98. data/doc/file.standalone.html +233 -0
  99. data/doc/file.virtual_models.html +117 -0
  100. data/doc/file.with_form.html +157 -0
  101. data/doc/file_list.html +160 -0
  102. data/doc/guide/cookbook.md +41 -0
  103. data/doc/guide/dsl_reference.md +155 -0
  104. data/doc/guide/example_advanced.md +209 -0
  105. data/doc/guide/generators.md +1 -1
  106. data/doc/guide/glossary.md +42 -0
  107. data/doc/guide/gotchas.md +125 -0
  108. data/doc/guide/maintaining.md +64 -0
  109. data/doc/guide/patterns.md +681 -0
  110. data/doc/guide/pre_built_components/edit.md +1 -1
  111. data/doc/guide/pre_built_components/index.md +64 -1
  112. data/doc/guide/pre_built_components/list.md +111 -7
  113. data/doc/guide/pre_built_components/show.md +57 -2
  114. data/doc/guide/pre_built_components/with_form.md +56 -9
  115. data/doc/guide/pre_built_components.md +7 -2
  116. data/doc/guide/standalone.md +16 -1
  117. data/doc/index.html +25 -4
  118. data/doc/integrations.md +61 -0
  119. data/doc/llms.txt +62 -0
  120. data/doc/top-level-namespace.html +1 -1
  121. data/lib/compony/component.rb +8 -3
  122. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +32 -15
  123. data/lib/compony/component_mixins/default/standalone/verb_dsl.rb +11 -3
  124. data/lib/compony/component_mixins/resourceful.rb +30 -16
  125. data/lib/compony/components/destroy.rb +21 -1
  126. data/lib/compony/components/edit.rb +25 -1
  127. data/lib/compony/components/form.rb +63 -21
  128. data/lib/compony/components/list.rb +1 -1
  129. data/lib/compony/components/new.rb +25 -1
  130. data/lib/compony/components/with_form.rb +20 -5
  131. data/lib/compony/intent.rb +1 -1
  132. metadata +43 -1
@@ -0,0 +1,669 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ File: patterns
8
+
9
+ &mdash; Documentation by YARD 0.9.34
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" />
16
+
17
+ <script type="text/javascript">
18
+ pathId = "patterns";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="file_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+ <span class="title">File: patterns</span>
41
+
42
+ </div>
43
+
44
+ <div id="search">
45
+
46
+ <a class="full_list_link" id="class_list_link"
47
+ href="class_list.html">
48
+
49
+ <svg width="24" height="24">
50
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
51
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
52
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
53
+ </svg>
54
+ </a>
55
+
56
+ </div>
57
+ <div class="clear"></div>
58
+ </div>
59
+
60
+ <div id="content"><div id='filecontents'>
61
+ <p><a href="/README_md.html#guide--documentation">Back to the guide</a></p>
62
+
63
+ <h1 id="label-Real-world+patterns">Real-world patterns</h1>
64
+
65
+ <p>Conventions distilled from a range of production Compony apps. These are <em>idioms</em>, not framework requirements — but they recur consistently and are worth adopting. Every example uses a neutral domain (<code>Account</code>, <code>Order</code>, <code>LineItem</code>, <code>Document</code>). Where a pattern relies on a companion gem (CanCanCan, ActiveType, simple_form, a date/select input) that is called out.</p>
66
+
67
+ <p>For exact method signatures see <a href="/doc/guide/dsl_reference_md.html">dsl_reference.md</a>; for footguns see <a href="/doc/guide/gotchas_md.html">gotchas.md</a>.</p>
68
+
69
+ <h2 id="label-1.+The+app+base-component+layer">1. The app base-component layer</h2>
70
+
71
+ <p>Almost every non-trivial app inserts one abstract layer between Compony’s pre-built components and the concrete ones. Concrete components inherit from the app layer, never from Compony directly. This centralizes layout, button styling, and chrome so the whole app’s look changes in one place.</p>
72
+
73
+ <pre class="code ruby"><code class="ruby"><span class='comment'># app/components/base_components/show.rb (a common location; app/compony/ is also used)
74
+ </span><span class='kw'>module</span> <span class='const'>BaseComponents</span>
75
+ <span class='kw'>class</span> <span class='const'>Show</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Show.html" title="Compony::Components::Show (class)">Show</a></span></span>
76
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
77
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_layout'>layout</span> <span class='symbol'>:backend</span> <span class='rbrace'>}</span> <span class='comment'># app-wide Rails layout for all non-publicly accessible components
78
+ </span> <span class='id identifier rubyid_button'>button</span><span class='lparen'>(</span><span class='symbol'>:icon</span><span class='rparen'>)</span> <span class='lbrace'>{</span> <span class='symbol'>:eye</span> <span class='rbrace'>}</span>
79
+ <span class='id identifier rubyid_content'>content</span> <span class='symbol'>:main</span><span class='comma'>,</span> <span class='label'>hidden:</span> <span class='kw'>true</span> <span class='comment'># concrete comps fill :main…
80
+ </span> <span class='id identifier rubyid_content'>content</span> <span class='symbol'>:wrapper</span> <span class='kw'>do</span> <span class='comment'># …chrome lives here, inherited
81
+ </span> <span class='id identifier rubyid_div'>div</span> <span class='label'>class:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>card card-body</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
82
+ <span class='id identifier rubyid_content'>content</span> <span class='symbol'>:main</span>
83
+ <span class='kw'>end</span>
84
+ <span class='kw'>end</span>
85
+ <span class='kw'>end</span>
86
+ <span class='kw'>end</span>
87
+ <span class='kw'>end</span>
88
+
89
+ <span class='comment'># app/components/orders/show.rb
90
+ </span><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Show</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>Show</span>
91
+ <span class='kw'>end</span> <span class='comment'># fully functional, empty body
92
+ </span></code></pre>
93
+
94
+ <p>Recurring forms of this layer: <code>BaseComponents::{Index,Show,New,Edit,Destroy,List}</code>. The <code>content :main, hidden: true</code> + <code>content :wrapper</code> pair is the standard way to let children override the inner content while inheriting the outer chrome (see basic_componentbasic_component.md[/doc/guide/basic_component.md#nesting-content-blocks-calling-a-content-block-from-another]).</p>
95
+
96
+ <p>Teams sometimes add their own helper DSL on top of this layer (CSV/PDF helpers, archive toggles, etc.). Keep such helpers in the app base layer, not in concrete components.</p>
97
+
98
+ <h2 id="label-2.+Thin+leaf+components">2. Thin leaf components</h2>
99
+
100
+ <p>Concrete CRUD components are usually empty — all behavior is inherited. Add a <code>setup</code> block only to deviate.</p>
101
+
102
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Destroy</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>Destroy</span><span class='semicolon'>;</span> <span class='kw'>end</span>
103
+ <span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>New</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>New</span><span class='semicolon'>;</span> <span class='kw'>end</span>
104
+ <span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Edit</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>Edit</span><span class='semicolon'>;</span> <span class='kw'>end</span>
105
+ </code></pre>
106
+
107
+ <p>This is the single most common pattern. Prefer it over hand-written endpoints (<a href="/doc/guide/gotchas_md.html#15-hand-rolled-endpoint-where-a-pre-built-crud-component-exists">gotchas.md #15</a>).</p>
108
+
109
+ <h2 id="label-3.+Index+-3D+load_data+scope+-2B+nested+-3Alist">3. Index = <code>load_data</code> scope + nested <code>:list</code></h2>
110
+
111
+ <p>Index components rarely render rows themselves; they load a scope and embed the family’s List via <code>render_sub_comp</code>.</p>
112
+
113
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Index</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>Index</span>
114
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
115
+ <span class='id identifier rubyid_load_data'>load_data</span> <span class='lbrace'>{</span> <span class='ivar'>@data</span> <span class='op'>=</span> <span class='const'>Order</span><span class='period'>.</span><span class='id identifier rubyid_accessible_by'>accessible_by</span><span class='lparen'>(</span><span class='id identifier rubyid_current_ability'>current_ability</span><span class='rparen'>)</span><span class='period'>.</span><span class='id identifier rubyid_order'>order</span><span class='lparen'>(</span><span class='label'>created_at:</span> <span class='symbol'>:desc</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
116
+ <span class='id identifier rubyid_content'>content</span> <span class='kw'>do</span>
117
+ <span class='id identifier rubyid_h1'>h1</span> <span class='const'>Order</span><span class='period'>.</span><span class='id identifier rubyid_model_name'>model_name</span><span class='period'>.</span><span class='id identifier rubyid_human'>human</span><span class='lparen'>(</span><span class='label'>count:</span> <span class='int'>2</span><span class='rparen'>)</span>
118
+ <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_render_sub_comp'>render_sub_comp</span><span class='lparen'>(</span><span class='symbol'>:list</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span>
119
+ <span class='kw'>end</span>
120
+ <span class='kw'>end</span>
121
+ <span class='kw'>end</span>
122
+ </code></pre>
123
+ <ul><li>
124
+ <p><code>accessible_by(current_ability)</code> is the CanCanCan scoping idiom — pair it with the <code>authorize</code> block so list and access rules agree.</p>
125
+ </li><li>
126
+ <p><code>concat</code> is mandatory around <code>render_sub_comp</code>/<code>render_intent</code> (<a href="/doc/guide/gotchas_md.html#2-render_intent--render_sub_comp-output-not-appearing">gotchas.md #2</a>).</p>
127
+ </li></ul>
128
+
129
+ <h2 id="label-4.+List+customization">4. List customization</h2>
130
+
131
+ <p>This pattern is typically combined with a customized <code>BaseComponents::List</code> that adds styling and features to the pre-built list component.</p>
132
+
133
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>List</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>List</span>
134
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
135
+ <span class='id identifier rubyid_columns'>columns</span> <span class='symbol'>:number</span><span class='comma'>,</span> <span class='symbol'>:customer</span><span class='comma'>,</span> <span class='label'>as_title:</span> <span class='kw'>true</span> <span class='comment'># as_title -&gt; card title on mobile
136
+ </span> <span class='id identifier rubyid_columns'>columns</span> <span class='symbol'>:total</span><span class='comma'>,</span> <span class='symbol'>:created_at</span>
137
+ <span class='id identifier rubyid_column'>column</span> <span class='symbol'>:status</span> <span class='kw'>do</span> <span class='op'>|</span><span class='id identifier rubyid_order'>order</span><span class='op'>|</span> <span class='comment'># computed/custom cell
138
+ </span> <span class='id identifier rubyid_span'>span</span> <span class='id identifier rubyid_order'>order</span><span class='period'>.</span><span class='id identifier rubyid_status'>status</span><span class='period'>.</span><span class='id identifier rubyid_label'>label</span><span class='comma'>,</span> <span class='label'>class:</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>badge bg-</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_order'>order</span><span class='period'>.</span><span class='id identifier rubyid_status'>status</span><span class='period'>.</span><span class='id identifier rubyid_key'>key</span><span class='embexpr_end'>}</span><span class='tstring_end'>&quot;</span></span>
139
+ <span class='kw'>end</span>
140
+ <span class='id identifier rubyid_filters'>filters</span> <span class='symbol'>:number</span><span class='comma'>,</span> <span class='symbol'>:status</span>
141
+ <span class='id identifier rubyid_sorts'>sorts</span> <span class='symbol'>:number</span><span class='comma'>,</span> <span class='symbol'>:created_at</span>
142
+ <span class='id identifier rubyid_default_sorting'>default_sorting</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>created_at desc</span><span class='tstring_end'>&#39;</span></span>
143
+ <span class='kw'>end</span>
144
+ <span class='kw'>end</span>
145
+ </code></pre>
146
+
147
+ <p>Embedding a child list inside a Show, dropping the redundant FK column and preserving the active tab across filter submits:</p>
148
+
149
+ <pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_render_sub_comp'>render_sub_comp</span><span class='lparen'>(</span><span class='symbol'>:list</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_line_items'>line_items</span><span class='comma'>,</span> <span class='label'>skip_columns:</span> <span class='lbracket'>[</span><span class='symbol'>:order</span><span class='rbracket'>]</span><span class='comma'>,</span>
150
+ <span class='label'>params_in_filter:</span> <span class='lbracket'>[</span><span class='id identifier rubyid_param_name'>param_name</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>tab</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span><span class='rbracket'>]</span><span class='rparen'>)</span>
151
+ </code></pre>
152
+
153
+ <p><code>skip_*</code> options (<code>skip_pagination:</code>, <code>skip_filtering:</code>, <code>skip_columns:</code>, …) are constructor kwargs passed through <code>render_sub_comp</code>, useful for read-only embeds.</p>
154
+
155
+ <h2 id="label-5.+Custom+form+-2B+Schemacop-2C+kept+in+sync">5. Custom form + Schemacop, kept in sync</h2>
156
+
157
+ <p><code>form_fields</code> (rendering) and <code>schema_*</code> (param whitelist) must mirror each other.</p>
158
+
159
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Form</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Form.html" title="Compony::Components::Form (class)">Form</a></span></span>
160
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
161
+ <span class='id identifier rubyid_form_fields'>form_fields</span> <span class='kw'>do</span>
162
+ <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_field'>field</span><span class='lparen'>(</span><span class='symbol'>:number</span><span class='rparen'>)</span>
163
+ <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_field'>field</span><span class='lparen'>(</span><span class='symbol'>:customer</span><span class='comma'>,</span> <span class='label'>as:</span> <span class='symbol'>:tom_select</span><span class='rparen'>)</span> <span class='comment'># association name, not _id
164
+ </span> <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_field'>field</span><span class='lparen'>(</span><span class='symbol'>:placed_at</span><span class='comma'>,</span> <span class='label'>as:</span> <span class='symbol'>:flatpickr_datetime</span><span class='rparen'>)</span>
165
+ <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_pw_field'>pw_field</span><span class='lparen'>(</span><span class='symbol'>:access_code</span><span class='rparen'>)</span>
166
+ <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_field'>field</span><span class='lparen'>(</span><span class='symbol'>:internal_ref</span><span class='comma'>,</span> <span class='label'>hidden:</span> <span class='kw'>true</span><span class='rparen'>)</span> <span class='comment'># submitted, not shown
167
+ </span> <span class='id identifier rubyid_div'>div</span> <span class='label'>class:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>row</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span> <span class='comment'># arbitrary Dyny layout
168
+ </span> <span class='id identifier rubyid_div'>div</span> <span class='id identifier rubyid_field'>field</span><span class='lparen'>(</span><span class='symbol'>:first_name</span><span class='rparen'>)</span><span class='comma'>,</span> <span class='label'>class:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>col</span><span class='tstring_end'>&#39;</span></span>
169
+ <span class='id identifier rubyid_div'>div</span> <span class='id identifier rubyid_field'>field</span><span class='lparen'>(</span><span class='symbol'>:last_name</span><span class='rparen'>)</span><span class='comma'>,</span> <span class='label'>class:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>col</span><span class='tstring_end'>&#39;</span></span>
170
+ <span class='kw'>end</span>
171
+ <span class='kw'>end</span>
172
+
173
+ <span class='id identifier rubyid_schema_fields'>schema_fields</span> <span class='symbol'>:number</span><span class='comma'>,</span> <span class='symbol'>:customer</span><span class='comma'>,</span> <span class='symbol'>:placed_at</span><span class='comma'>,</span> <span class='symbol'>:internal_ref</span>
174
+ <span class='id identifier rubyid_schema_pw_field'>schema_pw_field</span> <span class='symbol'>:access_code</span>
175
+ <span class='kw'>end</span>
176
+ <span class='kw'>end</span>
177
+ </code></pre>
178
+ <ul><li>
179
+ <p><code>as: :tom_select</code> / <code>as: :flatpickr_date(time)</code> are app-registered simple_form inputs (TomSelect, Flatpickr) — a good choice for selects and date pickers.</p>
180
+ </li><li>
181
+ <p>Use the <strong>association name</strong> in <code>field</code>/<code>schema_field</code>; <code>_id</code> is added automatically (<a href="/doc/guide/gotchas_md.html#4-schema_field-with-the-foreign-key-name">gotchas.md #4</a>).</p>
182
+ </li><li>
183
+ <p>Nested attributes: <code>f.simple_fields_for(:line_items)</code> in <code>form_fields</code> plus a raw <code>schema_line { ary? :line_items_attributes do ... end }</code>.</p>
184
+ </li><li>
185
+ <p>Multilang fields: <code>field(:title, multilang: true).each { |i| concat i }</code> paired with <code>schema_field :title, multilang: true</code>.</p>
186
+ </li></ul>
187
+
188
+ <p>Wire a non-default form into New/Edit with <code>form_comp_class</code>:</p>
189
+
190
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>QuickAdd</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/New.html" title="Compony::Components::New (class)">New</a></span></span>
191
+ <span class='id identifier rubyid_setup'>setup</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_form_comp_class'>form_comp_class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>QuickAddForm</span> <span class='rbrace'>}</span>
192
+ <span class='kw'>end</span>
193
+ </code></pre>
194
+
195
+ <h2 id="label-6.+Autocomplete+form+-28app-level+subclass-29">6. Autocomplete form (app-level subclass)</h2>
196
+
197
+ <p>Compony does not ship autocomplete, but a very common app pattern is an <code>AutocompleteForm</code> base (subclass of <code>Compony::Components::Form</code>) exposing an extra <code>standalone</code> JSON endpoint for an ajax select. Shape:</p>
198
+
199
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>AutocompleteForm</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Form.html" title="Compony::Components::Form (class)">Form</a></span></span>
200
+ <span class='comment'># class-level `autocomplete(field) { |query, ability| ...collection... }` that
201
+ </span> <span class='comment'># registers an extra `standalone :autocomplete_&lt;field&gt;` returning
202
+ </span> <span class='comment'># [{ text:, value:, icon: }] JSON, consumed by a TomSelect Stimulus controller.
203
+ </span><span class='kw'>end</span>
204
+
205
+ <span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Form</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>AutocompleteForm</span>
206
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
207
+ <span class='id identifier rubyid_form_fields'>form_fields</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_field'>field</span><span class='lparen'>(</span><span class='symbol'>:customer</span><span class='comma'>,</span> <span class='label'>as:</span> <span class='symbol'>:tom_select</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
208
+ <span class='id identifier rubyid_schema_field'>schema_field</span> <span class='symbol'>:customer</span>
209
+ <span class='id identifier rubyid_autocomplete'>autocomplete</span><span class='lparen'>(</span><span class='symbol'>:customer</span><span class='rparen'>)</span> <span class='lbrace'>{</span> <span class='op'>|</span><span class='id identifier rubyid_q'>q</span><span class='comma'>,</span> <span class='id identifier rubyid_ability'>ability</span><span class='op'>|</span> <span class='const'>Customer</span><span class='period'>.</span><span class='id identifier rubyid_accessible_by'>accessible_by</span><span class='lparen'>(</span><span class='id identifier rubyid_ability'>ability</span><span class='rparen'>)</span><span class='period'>.</span><span class='id identifier rubyid_search'>search</span><span class='lparen'>(</span><span class='id identifier rubyid_q'>q</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
210
+ <span class='kw'>end</span>
211
+ <span class='kw'>end</span>
212
+ </code></pre>
213
+
214
+ <p>If you need autocomplete, build this base once and reuse it.</p>
215
+
216
+ <h2 id="label-7.+Tabbed+Show+via+a+mixin">7. Tabbed Show via a mixin</h2>
217
+
218
+ <p>Detail pages are split into tabs with a small app mixin that adds a <code>tab</code> DSL and renders a tab bar into <code>:main</code>. Each tab body typically renders <code>content :data</code> or a nested list.</p>
219
+
220
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Show</span> <span class='op'>&lt;</span> <span class='const'>BaseComponents</span><span class='op'>::</span><span class='const'>Show</span>
221
+ <span class='id identifier rubyid_include'>include</span> <span class='const'>ComponentMixins</span><span class='op'>::</span><span class='const'>Tabs</span>
222
+
223
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
224
+ <span class='id identifier rubyid_tab'>tab</span><span class='lparen'>(</span><span class='symbol'>:overview</span><span class='comma'>,</span> <span class='id identifier rubyid__'>_</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Overview</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span><span class='rparen'>)</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_content'>content</span> <span class='symbol'>:data</span> <span class='rbrace'>}</span>
225
+ <span class='id identifier rubyid_tab'>tab</span><span class='lparen'>(</span><span class='symbol'>:items</span><span class='comma'>,</span> <span class='id identifier rubyid__'>_</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Items</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span><span class='rparen'>)</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_render_sub_comp'>render_sub_comp</span><span class='lparen'>(</span><span class='symbol'>:list</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_line_items'>line_items</span><span class='comma'>,</span>
226
+ <span class='label'>skip_columns:</span> <span class='lbracket'>[</span><span class='symbol'>:order</span><span class='rbracket'>]</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
227
+ <span class='kw'>end</span>
228
+ <span class='kw'>end</span>
229
+ </code></pre>
230
+
231
+ <p>The mixin keys the active tab off a prefixed param (<code>param_name(&#39;tab&#39;)</code>) so multiple tabbed components can coexist. Compony has no built-in tabs — copy the mixin per app.</p>
232
+
233
+ <h2 id="label-8.+Lifecycle+hooks+for+derived+data">8. Lifecycle hooks for derived data</h2>
234
+ <ul><li>
235
+ <p><strong><code>after_assign_attributes</code></strong> — fill defaults / context after params are assigned, before validation: <code>@data.account_id ||= current_user.account_id</code>.</p>
236
+ </li><li>
237
+ <p><strong><code>before_render</code></strong> — verb-independent guards and precomputation. Redirect and the content chain is skipped: <code>ruby before_render do redirect_to Compony.path(:show, @data) if @data.locked? end </code></p>
238
+ </li><li>
239
+ <p><strong><code>load_data</code></strong> — narrow the scope (<code>accessible_by</code>, <code>includes</code>, ordering).</p>
240
+ </li><li>
241
+ <p><strong><code>store_data</code></strong> — override persistence (virtual models, file handling, bulk import).</p>
242
+ </li><li>
243
+ <p><strong><code>on_{created,updated,destroyed}_redirect_path</code></strong> — control where success lands, e.g. <code>Compony.path(:show, @data.parent)</code> for owned records.</p>
244
+ </li></ul>
245
+
246
+ <h2 id="label-9.+Exposed+intents+as+the+action+toolbar">9. Exposed intents as the action toolbar</h2>
247
+
248
+ <p>Concrete components tailor the header toolbar by <code>add</code>/<code>remove</code> on inherited intents.</p>
249
+
250
+ <pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_exposed_intents'>exposed_intents</span> <span class='kw'>do</span>
251
+ <span class='id identifier rubyid_remove'>remove</span> <span class='symbol'>:destroy</span>
252
+ <span class='id identifier rubyid_add'>add</span> <span class='symbol'>:show</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='comma'>,</span> <span class='label'>label:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>PDF</span><span class='tstring_end'>&#39;</span></span><span class='comma'>,</span> <span class='label'>name:</span> <span class='symbol'>:pdf</span><span class='comma'>,</span> <span class='label'>path:</span> <span class='lbrace'>{</span> <span class='label'>format:</span> <span class='symbol'>:pdf</span> <span class='rbrace'>}</span><span class='comma'>,</span>
253
+ <span class='label'>feasibility_action:</span> <span class='symbol'>:pdf</span>
254
+ <span class='id identifier rubyid_add'>add</span> <span class='symbol'>:archive</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='comma'>,</span> <span class='label'>method:</span> <span class='symbol'>:patch</span><span class='comma'>,</span> <span class='label'>before:</span> <span class='symbol'>:destroy</span>
255
+ <span class='kw'>end</span>
256
+ </code></pre>
257
+ <ul><li>
258
+ <p><code>path: { format: :pdf }</code> points a button at a format endpoint (see pattern 10).</p>
259
+ </li><li>
260
+ <p><code>feasibility_action:</code> ties the button’s enabled state to a model <code>prevent</code> (<a href="/doc/guide/feasibility_md.html">feasibility.md</a>).</p>
261
+ </li><li>
262
+ <p>State-dependent toolbars (archived vs active) are done by branching inside the <code>exposed_intents</code> block on <code>@data</code>.</p>
263
+ </li><li>
264
+ <p>Generating one intent per enum value is common: <code>Period.all.each { |p| add :new, :prices, name: :&quot;new_#{p.key}&quot;, path: { price: { period: p.key } } }</code>.</p>
265
+ </li></ul>
266
+
267
+ <h2 id="label-10.+CSV+-2F+PDF+via+respond+-3Aformat">10. CSV / PDF via <code>respond :format</code></h2>
268
+
269
+ <p>A format export is the same component with an extra <code>respond</code> branch and an exposed intent pointing at it. Because overriding <code>respond</code> skips the default <code>authorize</code>, re-check there (<a href="/doc/guide/gotchas_md.html#3-overriding-respond-skips-authorization">gotchas.md #3</a>).</p>
270
+
271
+ <pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>orders</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
272
+ <span class='id identifier rubyid_verb'>verb</span> <span class='symbol'>:get</span> <span class='kw'>do</span>
273
+ <span class='id identifier rubyid_authorize'>authorize</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_can?'>can?</span><span class='lparen'>(</span><span class='symbol'>:read</span><span class='comma'>,</span> <span class='const'>Order</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
274
+ <span class='id identifier rubyid_respond'>respond</span> <span class='symbol'>:csv</span> <span class='kw'>do</span>
275
+ <span class='id identifier rubyid_can?'>can?</span><span class='lparen'>(</span><span class='symbol'>:read</span><span class='comma'>,</span> <span class='const'>Order</span><span class='rparen'>)</span> <span class='kw'>or</span> <span class='id identifier rubyid_raise'>raise</span> <span class='const'>CanCan</span><span class='op'>::</span><span class='const'>AccessDenied</span>
276
+ <span class='id identifier rubyid_send_data'>send_data</span><span class='lparen'>(</span><span class='const'>OrderCsv</span><span class='period'>.</span><span class='id identifier rubyid_new'>new</span><span class='lparen'>(</span><span class='ivar'>@data</span><span class='rparen'>)</span><span class='period'>.</span><span class='id identifier rubyid_to_csv'>to_csv</span><span class='comma'>,</span> <span class='label'>filename:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>orders.csv</span><span class='tstring_end'>&#39;</span></span><span class='comma'>,</span> <span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>text/csv</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span>
277
+ <span class='kw'>end</span>
278
+ <span class='id identifier rubyid_respond'>respond</span> <span class='symbol'>:pdf</span> <span class='kw'>do</span>
279
+ <span class='id identifier rubyid_can?'>can?</span><span class='lparen'>(</span><span class='symbol'>:read</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span> <span class='kw'>or</span> <span class='id identifier rubyid_raise'>raise</span> <span class='const'>CanCan</span><span class='op'>::</span><span class='const'>AccessDenied</span>
280
+ <span class='id identifier rubyid_send_data'>send_data</span><span class='lparen'>(</span><span class='const'>OrderPdf</span><span class='period'>.</span><span class='id identifier rubyid_new'>new</span><span class='lparen'>(</span><span class='ivar'>@data</span><span class='rparen'>)</span><span class='period'>.</span><span class='id identifier rubyid_render'>render</span><span class='comma'>,</span> <span class='label'>filename:</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_pdf_name'>pdf_name</span><span class='comma'>,</span>
281
+ <span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>application/pdf</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span>
282
+ <span class='kw'>end</span>
283
+ <span class='kw'>end</span>
284
+ <span class='kw'>end</span>
285
+ <span class='comment'># exposed_intents { add :index, :orders, label: &#39;CSV&#39;, path: { format: :csv } }
286
+ </span></code></pre>
287
+
288
+ <h2 id="label-11.+Non-CRUD-3A+job+dispatch-2C+toggles-2C+clone">11. Non-CRUD: job dispatch, toggles, clone</h2>
289
+
290
+ <p><strong>Job dispatch</strong> — POST-only custom component, enqueue, flash, redirect:</p>
291
+
292
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>ScheduleSync</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Component.html" title="Compony::Component (class)">Component</a></span></span>
293
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
294
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>orders/schedule_sync</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
295
+ <span class='id identifier rubyid_verb'>verb</span> <span class='symbol'>:post</span> <span class='kw'>do</span>
296
+ <span class='id identifier rubyid_authorize'>authorize</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_can?'>can?</span><span class='lparen'>(</span><span class='symbol'>:create</span><span class='comma'>,</span> <span class='const'>Order</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
297
+ <span class='id identifier rubyid_respond'>respond</span> <span class='kw'>do</span>
298
+ <span class='const'>SyncOrdersJob</span><span class='period'>.</span><span class='id identifier rubyid_perform_later'>perform_later</span>
299
+ <span class='id identifier rubyid_flash'>flash</span><span class='period'>.</span><span class='id identifier rubyid_notice'>notice</span> <span class='op'>=</span> <span class='id identifier rubyid__'>_</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Queued — give it a few minutes.</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span>
300
+ <span class='id identifier rubyid_redirect_to'>redirect_to</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='period'>.</span><span class='id identifier rubyid_path'><span class='object_link'><a href="Compony.html#path-class_method" title="Compony.path (method)">path</a></span></span><span class='lparen'>(</span><span class='symbol'>:index</span><span class='comma'>,</span> <span class='symbol'>:orders</span><span class='rparen'>)</span>
301
+ <span class='kw'>end</span>
302
+ <span class='kw'>end</span>
303
+ <span class='kw'>end</span>
304
+ <span class='id identifier rubyid_label'>label</span><span class='lparen'>(</span><span class='symbol'>:all</span><span class='rparen'>)</span> <span class='lbrace'>{</span> <span class='id identifier rubyid__'>_</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Sync now</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span> <span class='rbrace'>}</span>
305
+ <span class='id identifier rubyid_button'>button</span><span class='lparen'>(</span><span class='symbol'>:icon</span><span class='rparen'>)</span> <span class='lbrace'>{</span> <span class='symbol'>:rotate</span> <span class='rbrace'>}</span>
306
+ <span class='kw'>end</span>
307
+ <span class='kw'>end</span>
308
+ </code></pre>
309
+
310
+ <p>Expose it from Index: <code>exposed_intents { add :schedule_sync, :orders, method: :post }</code>.</p>
311
+
312
+ <p><strong>State toggle</strong> — inherit <code>Edit</code>, flip in <code>after_assign_attributes</code>, dynamic label:</p>
313
+
314
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Accounts</span><span class='op'>::</span><span class='const'>ToggleActive</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Edit.html" title="Compony::Components::Edit (class)">Edit</a></span></span>
315
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
316
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>accounts/:id/toggle_active</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
317
+ <span class='id identifier rubyid_verb'>verb</span> <span class='symbol'>:patch</span> <span class='kw'>do</span> <span class='id identifier rubyid_authorize'>authorize</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_can?'>can?</span><span class='lparen'>(</span><span class='symbol'>:toggle_active</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span> <span class='rbrace'>}</span> <span class='kw'>end</span>
318
+ <span class='kw'>end</span>
319
+ <span class='id identifier rubyid_label'>label</span><span class='lparen'>(</span><span class='symbol'>:long</span><span class='rparen'>)</span> <span class='lbrace'>{</span> <span class='op'>|</span><span class='id identifier rubyid_a'>a</span><span class='op'>|</span> <span class='id identifier rubyid_a'>a</span><span class='period'>.</span><span class='id identifier rubyid_active?'>active?</span> <span class='op'>?</span> <span class='id identifier rubyid__'>_</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Deactivate</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span> <span class='op'>:</span> <span class='id identifier rubyid__'>_</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Activate</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span> <span class='rbrace'>}</span>
320
+ <span class='id identifier rubyid_after_assign_attributes'>after_assign_attributes</span> <span class='lbrace'>{</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_active'>active</span> <span class='op'>=</span> <span class='op'>!</span><span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_active'>active</span> <span class='rbrace'>}</span>
321
+ <span class='kw'>end</span>
322
+ <span class='kw'>end</span>
323
+ </code></pre>
324
+
325
+ <p><strong>Clone</strong> — inherit <code>New</code>, load + dup the source in <code>load_data</code>, redirect to the copy:</p>
326
+
327
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Clone</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/New.html" title="Compony::Components::New (class)">New</a></span></span>
328
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
329
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>orders/:id/clone</span><span class='tstring_end'>&#39;</span></span>
330
+ <span class='id identifier rubyid_load_data'>load_data</span> <span class='kw'>do</span>
331
+ <span class='id identifier rubyid_source'>source</span> <span class='op'>=</span> <span class='const'>Order</span><span class='period'>.</span><span class='id identifier rubyid_find'>find</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span><span class='lbracket'>[</span><span class='symbol'>:id</span><span class='rbracket'>]</span><span class='rparen'>)</span>
332
+ <span class='id identifier rubyid_authorize!'>authorize!</span><span class='lparen'>(</span><span class='symbol'>:read</span><span class='comma'>,</span> <span class='id identifier rubyid_source'>source</span><span class='rparen'>)</span> <span class='comment'># CanCanCan bang form
333
+ </span> <span class='ivar'>@data</span> <span class='op'>=</span> <span class='id identifier rubyid_source'>source</span><span class='period'>.</span><span class='id identifier rubyid_dup'>dup</span>
334
+ <span class='kw'>end</span>
335
+ <span class='id identifier rubyid_on_created_redirect_path'>on_created_redirect_path</span> <span class='lbrace'>{</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='period'>.</span><span class='id identifier rubyid_path'><span class='object_link'><a href="Compony.html#path-class_method" title="Compony.path (method)">path</a></span></span><span class='lparen'>(</span><span class='symbol'>:show</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
336
+ <span class='kw'>end</span>
337
+ <span class='kw'>end</span>
338
+ </code></pre>
339
+
340
+ <h2 id="label-12.+Virtual+model+for+non-persistent+-2F+upload+forms">12. Virtual model for non-persistent / upload forms</h2>
341
+
342
+ <p>Inherit <code>New</code>, back it with a <code>Compony::VirtualModel</code>, take over the response. <code>@data.save</code> is a no-op so business logic goes in <code>on_created_respond</code> (or <code>store_data</code>).</p>
343
+
344
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Documents</span><span class='op'>::</span><span class='const'>Import</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/New.html" title="Compony::Components::New (class)">New</a></span></span>
345
+ <span class='kw'>class</span> <span class='const'>VirtualModel</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/VirtualModel.html" title="Compony::VirtualModel (class)">VirtualModel</a></span></span>
346
+ <span class='id identifier rubyid_attribute'>attribute</span> <span class='symbol'>:id</span><span class='comma'>,</span> <span class='symbol'>:bigint</span>
347
+ <span class='id identifier rubyid_belongs_to'>belongs_to</span> <span class='symbol'>:account</span>
348
+ <span class='id identifier rubyid_has_one_attached'>has_one_attached</span> <span class='symbol'>:file</span>
349
+ <span class='id identifier rubyid_field'>field</span> <span class='symbol'>:account</span><span class='comma'>,</span> <span class='symbol'>:association</span>
350
+ <span class='id identifier rubyid_field'>field</span> <span class='symbol'>:file</span><span class='comma'>,</span> <span class='symbol'>:attachment</span>
351
+ <span class='id identifier rubyid_validates'>validates</span> <span class='symbol'>:file</span><span class='comma'>,</span> <span class='label'>presence:</span> <span class='kw'>true</span>
352
+ <span class='kw'>end</span>
353
+
354
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
355
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>documents/import</span><span class='tstring_end'>&#39;</span></span>
356
+ <span class='id identifier rubyid_data_class'>data_class</span> <span class='const'>VirtualModel</span>
357
+ <span class='id identifier rubyid_form_comp_class'>form_comp_class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Documents</span><span class='op'>::</span><span class='const'>ImportForm</span>
358
+
359
+ <span class='comment'># ActiveStorage on a virtual model: validate only, read the tempfile yourself.
360
+ </span> <span class='id identifier rubyid_store_data'>store_data</span> <span class='kw'>do</span>
361
+ <span class='ivar'>@create_succeeded</span> <span class='op'>=</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_validate'>validate</span>
362
+ <span class='kw'>next</span> <span class='kw'>unless</span> <span class='ivar'>@create_succeeded</span>
363
+ <span class='id identifier rubyid_tempfile'>tempfile</span> <span class='op'>=</span> <span class='id identifier rubyid_params'>params</span><span class='period'>.</span><span class='id identifier rubyid_dig'>dig</span><span class='lparen'>(</span><span class='symbol'>:documents_virtual_model</span><span class='comma'>,</span> <span class='symbol'>:file</span><span class='rparen'>)</span><span class='op'>&amp;.</span><span class='id identifier rubyid_tempfile'>tempfile</span>
364
+ <span class='const'>DocumentImporter</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span><span class='lparen'>(</span><span class='label'>account:</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_account'>account</span><span class='comma'>,</span> <span class='label'>io:</span> <span class='id identifier rubyid_tempfile'>tempfile</span><span class='rparen'>)</span>
365
+ <span class='kw'>end</span>
366
+
367
+ <span class='id identifier rubyid_on_created_respond'>on_created_respond</span> <span class='kw'>do</span>
368
+ <span class='id identifier rubyid_flash'>flash</span><span class='period'>.</span><span class='id identifier rubyid_notice'>notice</span> <span class='op'>=</span> <span class='id identifier rubyid__'>_</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Imported.</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span>
369
+ <span class='id identifier rubyid_redirect_to'>redirect_to</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='period'>.</span><span class='id identifier rubyid_path'><span class='object_link'><a href="Compony.html#path-class_method" title="Compony.path (method)">path</a></span></span><span class='lparen'>(</span><span class='symbol'>:index</span><span class='comma'>,</span> <span class='symbol'>:documents</span><span class='rparen'>)</span>
370
+ <span class='kw'>end</span>
371
+ <span class='kw'>end</span>
372
+ <span class='kw'>end</span>
373
+ </code></pre>
374
+
375
+ <p>See <a href="/doc/guide/virtual_models_md.html">virtual_models.md</a> and <a href="/doc/guide/gotchas_md.html#12-activestorage-attachment-on-a-virtual-model">gotchas.md #12</a>.</p>
376
+
377
+ <h2 id="label-13.+Public+endpoints+-26+webhooks">13. Public endpoints &amp; webhooks</h2>
378
+
379
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Public</span><span class='op'>::</span><span class='const'>Webhook</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Component.html" title="Compony::Component (class)">Component</a></span></span>
380
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
381
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>/webhooks/orders</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
382
+ <span class='id identifier rubyid_skip_authentication!'>skip_authentication!</span>
383
+ <span class='id identifier rubyid_skip_forgery_protection!'>skip_forgery_protection!</span>
384
+ <span class='id identifier rubyid_verb'>verb</span> <span class='symbol'>:post</span> <span class='kw'>do</span>
385
+ <span class='id identifier rubyid_authorize'>authorize</span> <span class='lbrace'>{</span> <span class='kw'>true</span> <span class='rbrace'>}</span> <span class='comment'># still mandatory
386
+ </span> <span class='id identifier rubyid_respond'>respond</span> <span class='kw'>do</span>
387
+ <span class='id identifier rubyid_expected'>expected</span> <span class='op'>=</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>Bearer </span><span class='embexpr_beg'>#{</span><span class='const'>ENV</span><span class='period'>.</span><span class='id identifier rubyid_fetch'>fetch</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>WEBHOOK_TOKEN</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span><span class='embexpr_end'>}</span><span class='tstring_end'>&quot;</span></span>
388
+ <span class='id identifier rubyid_got'>got</span> <span class='op'>=</span> <span class='id identifier rubyid_request'>request</span><span class='period'>.</span><span class='id identifier rubyid_headers'>headers</span><span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Authorization</span><span class='tstring_end'>&#39;</span></span><span class='rbracket'>]</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span>
389
+ <span class='kw'>unless</span> <span class='const'>ActiveSupport</span><span class='op'>::</span><span class='const'>SecurityUtils</span><span class='period'>.</span><span class='id identifier rubyid_secure_compare'>secure_compare</span><span class='lparen'>(</span><span class='id identifier rubyid_got'>got</span><span class='comma'>,</span> <span class='id identifier rubyid_expected'>expected</span><span class='rparen'>)</span>
390
+ <span class='id identifier rubyid_sleep'>sleep</span> <span class='int'>1</span> <span class='comment'># crude timing equalization
391
+ </span> <span class='kw'>next</span> <span class='id identifier rubyid_controller'>controller</span><span class='period'>.</span><span class='id identifier rubyid_head'>head</span><span class='lparen'>(</span><span class='symbol'>:unauthorized</span><span class='rparen'>)</span>
392
+ <span class='kw'>end</span>
393
+ <span class='const'>OrderWebhook</span><span class='period'>.</span><span class='id identifier rubyid_process!'>process!</span><span class='lparen'>(</span><span class='id identifier rubyid_request'>request</span><span class='period'>.</span><span class='id identifier rubyid_params'>params</span><span class='rparen'>)</span>
394
+ <span class='id identifier rubyid_controller'>controller</span><span class='period'>.</span><span class='id identifier rubyid_head'>head</span> <span class='symbol'>:accepted</span>
395
+ <span class='kw'>end</span>
396
+ <span class='kw'>end</span>
397
+ <span class='kw'>end</span>
398
+ <span class='kw'>end</span>
399
+ <span class='kw'>end</span>
400
+ </code></pre>
401
+
402
+ <p>A login-aware redirect splitter is the same shape with <code>verb :get</code> + <code>before_render</code> choosing a <code>Compony.path</code> by <code>current_user</code>.</p>
403
+
404
+ <h2 id="label-14.+Custom+button+style">14. Custom button style</h2>
405
+
406
+ <p>Register one app button style and refer to it everywhere via <code>style:</code>.</p>
407
+
408
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Commons</span><span class='op'>::</span><span class='const'>BootstrapButton</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Buttons.html" title="Compony::Components::Buttons (module)">Buttons</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Buttons/Link.html" title="Compony::Components::Buttons::Link (class)">Link</a></span></span>
409
+ <span class='id identifier rubyid_protected'>protected</span>
410
+ <span class='kw'>def</span> <span class='id identifier rubyid_prepare_opts!'>prepare_opts!</span>
411
+ <span class='kw'>super</span>
412
+ <span class='id identifier rubyid_classes'>classes</span> <span class='op'>=</span> <span class='lparen'>(</span><span class='ivar'>@comp_opts</span><span class='lbracket'>[</span><span class='symbol'>:class</span><span class='rbracket'>]</span> <span class='op'>||</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span><span class='period'>.</span><span class='id identifier rubyid_split'>split</span>
413
+ <span class='id identifier rubyid_classes'>classes</span> <span class='op'>&lt;&lt;</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>btn</span><span class='tstring_end'>&#39;</span></span> <span class='op'>&lt;&lt;</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>btn-</span><span class='embexpr_beg'>#{</span><span class='ivar'>@comp_opts</span><span class='lbracket'>[</span><span class='symbol'>:color</span><span class='rbracket'>]</span> <span class='op'>||</span> <span class='symbol'>:primary</span><span class='embexpr_end'>}</span><span class='tstring_end'>&quot;</span></span>
414
+ <span class='ivar'>@comp_opts</span><span class='lbracket'>[</span><span class='symbol'>:class</span><span class='rbracket'>]</span> <span class='op'>=</span> <span class='id identifier rubyid_classes'>classes</span><span class='period'>.</span><span class='id identifier rubyid_join'>join</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'> </span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span>
415
+ <span class='kw'>end</span>
416
+ <span class='kw'>end</span>
417
+ <span class='comment'># config/initializers/compony.rb
418
+ </span><span class='comment'># Compony.register_button_style :bootstrap, &#39;::Components::Commons::BootstrapButton&#39;
419
+ </span><span class='comment'># Compony.default_button_style = :bootstrap
420
+ </span></code></pre>
421
+
422
+ <p>Make a separate style per visual kind (dropdown item, pill, compact) and select with <code>render_intent(:show, @data, style: :compact)</code>.</p>
423
+
424
+ <h2 id="label-15.+Inline-edit+card+with+a+Turbo+Frame">15. Inline-edit card with a Turbo Frame</h2>
425
+
426
+ <p>A Show panel where the Edit form swaps in place (no full-page nav) and swaps back on save. Wrap both the Show content and the Edit form in a <strong>same-named</strong> <code>turbo_frame_tag</code>; Turbo Drive then scopes navigation to that frame. Distinct from the <code>render_sub_comp(:list, …, turbo_frame:)</code> use in <a href="/doc/guide/nesting_md.html">nesting.md</a> (there the frame isolates a nested list’s own search/filter params; here it is the inline-edit boundary for one record’s Show/Edit pair).</p>
427
+
428
+ <pre class="code ruby"><code class="ruby"><span class='comment'># One frame name shared by the Show panel and the Edit form.
429
+ </span><span class='kw'>def</span> <span class='id identifier rubyid_card_frame'>card_frame</span><span class='lparen'>(</span><span class='id identifier rubyid_record'>record</span><span class='rparen'>)</span> <span class='op'>=</span> <span class='symbol'>:&quot;#{</span><span class='id identifier rubyid_record'>record</span><span class='period'>.</span><span class='id identifier rubyid_model_name'>model_name</span><span class='period'>.</span><span class='id identifier rubyid_singular'>singular</span><span class='embexpr_end'>}</span><span class='tstring_content'>_</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_record'>record</span><span class='period'>.</span><span class='id identifier rubyid_id'>id</span><span class='embexpr_end'>}</span><span class='tstring_content'>_card</span><span class='tstring_end'>&quot;</span></span>
430
+
431
+ <span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Accounts</span><span class='op'>::</span><span class='const'>Show</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Show.html" title="Compony::Components::Show (class)">Show</a></span></span>
432
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
433
+ <span class='id identifier rubyid_content'>content</span> <span class='symbol'>:data</span> <span class='kw'>do</span>
434
+ <span class='id identifier rubyid_turbo_frame_tag'>turbo_frame_tag</span> <span class='id identifier rubyid_card_frame'>card_frame</span><span class='lparen'>(</span><span class='ivar'>@data</span><span class='rparen'>)</span> <span class='kw'>do</span> <span class='comment'># Dyny: Rails view helper
435
+ </span> <span class='comment'># …render fields…
436
+ </span> <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_render_intent'>render_intent</span><span class='lparen'>(</span><span class='symbol'>:edit</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='comma'>,</span> <span class='label'>label:</span> <span class='lbrace'>{</span> <span class='label'>format:</span> <span class='symbol'>:short</span> <span class='rbrace'>}</span><span class='rparen'>)</span>
437
+ <span class='kw'>end</span>
438
+ <span class='kw'>end</span>
439
+ <span class='kw'>end</span>
440
+ <span class='kw'>end</span>
441
+
442
+ <span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Accounts</span><span class='op'>::</span><span class='const'>Edit</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Edit.html" title="Compony::Components::Edit (class)">Edit</a></span></span>
443
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
444
+ <span class='id identifier rubyid_content'>content</span> <span class='kw'>do</span>
445
+ <span class='id identifier rubyid_turbo_frame_tag'>turbo_frame_tag</span> <span class='id identifier rubyid_card_frame'>card_frame</span><span class='lparen'>(</span><span class='ivar'>@data</span><span class='rparen'>)</span> <span class='kw'>do</span> <span class='comment'># same frame name
446
+ </span> <span class='id identifier rubyid_concat'>concat</span> <span class='id identifier rubyid_form_comp'>form_comp</span><span class='period'>.</span><span class='id identifier rubyid_render'>render</span><span class='lparen'>(</span><span class='id identifier rubyid_controller'>controller</span><span class='comma'>,</span> <span class='label'>data:</span> <span class='ivar'>@data</span><span class='rparen'>)</span>
447
+ <span class='kw'>end</span>
448
+ <span class='kw'>end</span>
449
+ <span class='comment'># Default on_updated_redirect_path → Show; Turbo replaces just the frame.
450
+ </span> <span class='kw'>end</span>
451
+ <span class='kw'>end</span>
452
+ </code></pre>
453
+ <ul><li>
454
+ <p>Frame name must match exactly; deriving it from the record id keeps it unique when several cards render on one page.</p>
455
+ </li><li>
456
+ <p>A failed save re-renders Edit with HTTP 422 — keep the <code>turbo_frame_tag</code> wrapper in the Edit content so errors render in-frame too.</p>
457
+ </li></ul>
458
+
459
+ <h2 id="label-16.+Multi-step+wizard+across+components">16. Multi-step wizard across components</h2>
460
+
461
+ <p>A create/edit flow split over several steps, each its own component, advancing on save. Chain steps with <code>on_updated_redirect_path</code> (or <code>on_created_redirect_path</code>) and render a step indicator via a shared mixin (same mechanism as the tabs mixin in §7).</p>
462
+
463
+ <pre class="code ruby"><code class="ruby"><span class='kw'>module</span> <span class='const'>OrderWizard</span>
464
+ <span class='id identifier rubyid_extend'>extend</span> <span class='const'>ActiveSupport</span><span class='op'>::</span><span class='const'>Concern</span>
465
+ <span class='const'>STEPS</span> <span class='op'>=</span> <span class='qsymbols_beg'>%i[</span><span class='tstring_content'>details_edit</span><span class='words_sep'> </span><span class='tstring_content'>shipping_edit</span><span class='words_sep'> </span><span class='tstring_content'>confirm_edit</span><span class='tstring_end'>]</span></span><span class='period'>.</span><span class='id identifier rubyid_freeze'>freeze</span>
466
+
467
+ <span class='id identifier rubyid_included'>included</span> <span class='kw'>do</span>
468
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
469
+ <span class='id identifier rubyid_content'>content</span> <span class='symbol'>:wizard_nav</span><span class='comma'>,</span> <span class='label'>before:</span> <span class='symbol'>:main</span> <span class='kw'>do</span>
470
+ <span class='id identifier rubyid_ol'>ol</span> <span class='label'>class:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>wizard</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
471
+ <span class='const'>OrderWizard</span><span class='op'>::</span><span class='const'>STEPS</span><span class='period'>.</span><span class='id identifier rubyid_each'>each</span> <span class='kw'>do</span> <span class='op'>|</span><span class='id identifier rubyid_step'>step</span><span class='op'>|</span>
472
+ <span class='id identifier rubyid_li'>li</span> <span class='id identifier rubyid_step'>step</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span><span class='period'>.</span><span class='id identifier rubyid_delete_suffix'>delete_suffix</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>_edit</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span><span class='comma'>,</span>
473
+ <span class='label'>class:</span> <span class='lparen'>(</span><span class='id identifier rubyid_component'>component</span><span class='period'>.</span><span class='id identifier rubyid_comp_name'>comp_name</span><span class='period'>.</span><span class='id identifier rubyid_to_sym'>to_sym</span> <span class='op'>==</span> <span class='id identifier rubyid_step'>step</span> <span class='op'>?</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>active</span><span class='tstring_end'>&#39;</span></span> <span class='op'>:</span> <span class='kw'>nil</span><span class='rparen'>)</span>
474
+ <span class='kw'>end</span>
475
+ <span class='kw'>end</span>
476
+ <span class='kw'>end</span>
477
+ <span class='kw'>end</span>
478
+ <span class='kw'>end</span>
479
+ <span class='kw'>end</span>
480
+
481
+ <span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>DetailsEdit</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Edit.html" title="Compony::Components::Edit (class)">Edit</a></span></span>
482
+ <span class='id identifier rubyid_include'>include</span> <span class='const'>OrderWizard</span>
483
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
484
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>orders/:id/details</span><span class='tstring_end'>&#39;</span></span>
485
+ <span class='id identifier rubyid_on_updated_redirect_path'>on_updated_redirect_path</span> <span class='lbrace'>{</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='period'>.</span><span class='id identifier rubyid_path'><span class='object_link'><a href="Compony.html#path-class_method" title="Compony.path (method)">path</a></span></span><span class='lparen'>(</span><span class='symbol'>:shipping_edit</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span> <span class='rbrace'>}</span> <span class='comment'># → next step
486
+ </span> <span class='kw'>end</span>
487
+ <span class='kw'>end</span>
488
+
489
+ <span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>ShippingEdit</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Edit.html" title="Compony::Components::Edit (class)">Edit</a></span></span>
490
+ <span class='id identifier rubyid_include'>include</span> <span class='const'>OrderWizard</span>
491
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
492
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>orders/:id/shipping</span><span class='tstring_end'>&#39;</span></span>
493
+ <span class='id identifier rubyid_on_updated_redirect_path'>on_updated_redirect_path</span> <span class='lbrace'>{</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='period'>.</span><span class='id identifier rubyid_path'><span class='object_link'><a href="Compony.html#path-class_method" title="Compony.path (method)">path</a></span></span><span class='lparen'>(</span><span class='symbol'>:confirm_edit</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
494
+ <span class='kw'>end</span>
495
+ <span class='kw'>end</span>
496
+ <span class='comment'># …ConfirmEdit redirects to Show when done.
497
+ </span></code></pre>
498
+ <ul><li>
499
+ <p>Each step is a normal resourceful component on the same model — partial validation per step is just per-step <code>schema_field</code>s in each step’s Form.</p>
500
+ </li><li>
501
+ <p>For a <em>non-persistent</em> wizard (nothing saved until the end), back the components with a <a href="/doc/guide/virtual_models_md.html">VirtualModel</a> and carry state in its attributes (§12).</p>
502
+ </li><li>
503
+ <p><code>comp_name</code> drives the active-step highlight, so the mixin needs no per-step config.</p>
504
+ </li></ul>
505
+
506
+ <h2 id="label-17.+Inline+PATCH+without+a+form+-28reorder+-2F+quick+toggle-29">17. Inline PATCH without a form (reorder / quick toggle)</h2>
507
+
508
+ <p>A JS front-end (drag-to-sort, an inline checkbox) issues a small PATCH that mutates state and returns no body. Add a <strong>named</strong> extra <code>standalone</code> with <code>verb :patch</code>, validate with Schemacop directly, and <code>head :ok</code>. No Form component involved.</p>
509
+
510
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Orders</span><span class='op'>::</span><span class='const'>Show</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/Show.html" title="Compony::Components::Show (class)">Show</a></span></span>
511
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
512
+ <span class='comment'># Main route inherited from Show. Companion endpoint for reordering line items:
513
+ </span> <span class='id identifier rubyid_standalone'>standalone</span> <span class='symbol'>:reorder</span><span class='comma'>,</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>orders/:id/reorder</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
514
+ <span class='id identifier rubyid_verb'>verb</span> <span class='symbol'>:patch</span> <span class='kw'>do</span>
515
+ <span class='id identifier rubyid_authorize'>authorize</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_can?'>can?</span><span class='lparen'>(</span><span class='symbol'>:update</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
516
+ <span class='id identifier rubyid_respond'>respond</span> <span class='kw'>do</span> <span class='comment'># overriding respond skips default authorize…
517
+ </span> <span class='id identifier rubyid_can?'>can?</span><span class='lparen'>(</span><span class='symbol'>:update</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='rparen'>)</span> <span class='kw'>or</span> <span class='id identifier rubyid_raise'>raise</span> <span class='const'>CanCan</span><span class='op'>::</span><span class='const'>AccessDenied</span> <span class='comment'># …so re-check here
518
+ </span> <span class='id identifier rubyid_params'>params</span> <span class='op'>=</span> <span class='const'>Schemacop</span><span class='op'>::</span><span class='const'>Schema3</span><span class='period'>.</span><span class='id identifier rubyid_new'>new</span><span class='lparen'>(</span><span class='symbol'>:hash</span><span class='rparen'>)</span> <span class='kw'>do</span>
519
+ <span class='id identifier rubyid_ary!'>ary!</span> <span class='symbol'>:ordered_ids</span> <span class='kw'>do</span>
520
+ <span class='id identifier rubyid_list'>list</span> <span class='symbol'>:integer</span>
521
+ <span class='kw'>end</span>
522
+ <span class='kw'>end</span><span class='period'>.</span><span class='id identifier rubyid_validate!'>validate!</span><span class='lparen'>(</span><span class='id identifier rubyid_controller'>controller</span><span class='period'>.</span><span class='id identifier rubyid_request'>request</span><span class='period'>.</span><span class='id identifier rubyid_params'>params</span><span class='rparen'>)</span>
523
+ <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_line_items'>line_items</span><span class='period'>.</span><span class='id identifier rubyid_reorder_by!'>reorder_by!</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span><span class='lbracket'>[</span><span class='symbol'>:ordered_ids</span><span class='rbracket'>]</span><span class='rparen'>)</span>
524
+ <span class='id identifier rubyid_controller'>controller</span><span class='period'>.</span><span class='id identifier rubyid_head'>head</span> <span class='symbol'>:ok</span>
525
+ <span class='kw'>end</span>
526
+ <span class='kw'>end</span>
527
+ <span class='kw'>end</span>
528
+ <span class='kw'>end</span>
529
+ <span class='kw'>end</span>
530
+ </code></pre>
531
+
532
+ <p>The route is <code>reorder_show_orders_comp</code> (see <a href="/doc/guide/standalone_md.html#naming-of-exposed-routes">standalone naming</a>); point your Stimulus controller’s PATCH at <code>Compony.path(:show, @data, standalone_name: :reorder)</code>.</p>
533
+ <ul><li>
534
+ <p>This is the <a href="/doc/guide/gotchas_md.html#3-overriding-respond-skips-authorization">gotchas.md #3</a> case: the custom <code>respond</code> replaces the default that runs <code>authorize</code>, so authorize again inside it.</p>
535
+ </li><li>
536
+ <p>Keep companion endpoints in the <em>same</em> component as the screen they serve — what extra named <code>standalone</code>s are for (<a href="/doc/guide/standalone_md.html#exposing-multiple-paths-in-the-same-component-calling-standalone-multiple-times">standalone.md</a>), not a reason for a new component.</p>
537
+ </li><li>
538
+ <p>Return <code>head :ok</code> (or small JSON) — no Compony content to render for an ajax-only verb.</p>
539
+ </li></ul>
540
+
541
+ <h2 id="label-18.+Signed-token+capability+links+-28auth-less+onboarding+-2F+magic+links-29">18. Signed-token capability links (auth-less onboarding / magic links)</h2>
542
+
543
+ <p>Goal: an emailed link that lets an unauthenticated visitor perform one bounded action — invite acceptance, magic login, password reset, email confirmation — without a session. The trick: override Compony’s <code>path do … end</code> to <strong>mint a signed JWT</strong> and carry it as a <code>token</code> query param, then gate a <code>skip_authentication!</code> standalone with <code>authorize { token_valid?(params) }</code>. A small mixin centralizes encode/decode.</p>
544
+
545
+ <blockquote>
546
+ <p><strong>Security — read before copying.</strong> Such a link <em>is</em> the capability; anyone holding the URL can perform the action. It is only safe if every one of these holds: - <strong>Expiry is mandatory.</strong> Put <code>exp</code> in the payload and verify it. A capability link without a TTL is a permanent account-takeover primitive (it leaks via referrer headers, proxy logs, mail forwarding, browser history). Pair short TTLs with a resend flow. - <strong>Pin the algorithm and verify the signature</strong> — <code>JWT.decode(token, secret, true, { algorithm: &#39;HS512&#39; })</code>. Never accept <code>alg: none</code>; never leave verification off. - <strong>Fail closed.</strong> Rescue <code>JWT::DecodeError</code> (its subclasses cover bad signature, malformed token and expiry) and return <code>nil</code>/<code>false</code> so <code>authorize</code> denies with 403 — not a 500. - <strong>Use a dedicated signing secret</strong>, not <code>secret_key_base</code>, so rotating it doesn’t also invalidate every session (and vice-versa). - Still provide an <code>authorize</code> block: <code>skip_authentication!</code> removes <em>authentication</em>, not authorization (#14[/doc/guide/gotchas.md#14-public-endpoint-still-401redirecting]).</p>
547
+ </blockquote>
548
+
549
+ <pre class="code ruby"><code class="ruby"><span class='comment'># app/component_mixins/with_token.rb
550
+ </span><span class='kw'>module</span> <span class='const'>WithToken</span>
551
+ <span class='id identifier rubyid_extend'>extend</span> <span class='const'>ActiveSupport</span><span class='op'>::</span><span class='const'>Concern</span>
552
+ <span class='const'>TOKEN_TTL</span> <span class='op'>=</span> <span class='int'>14</span><span class='period'>.</span><span class='id identifier rubyid_days'>days</span>
553
+
554
+ <span class='kw'>def</span> <span class='id identifier rubyid_encode_token'>encode_token</span><span class='lparen'>(</span><span class='id identifier rubyid_payload'>payload</span><span class='rparen'>)</span>
555
+ <span class='const'>JWT</span><span class='period'>.</span><span class='id identifier rubyid_encode'>encode</span><span class='lparen'>(</span><span class='id identifier rubyid_payload'>payload</span><span class='period'>.</span><span class='id identifier rubyid_merge'>merge</span><span class='lparen'>(</span><span class='label'>exp:</span> <span class='const'>TOKEN_TTL</span><span class='period'>.</span><span class='id identifier rubyid_from_now'>from_now</span><span class='period'>.</span><span class='id identifier rubyid_to_i'>to_i</span><span class='rparen'>)</span><span class='comma'>,</span> <span class='id identifier rubyid_token_secret'>token_secret</span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>HS512</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span>
556
+ <span class='kw'>end</span>
557
+
558
+ <span class='comment'># Memoized; returns the payload (indifferent access) or nil. Fails closed.
559
+ </span> <span class='kw'>def</span> <span class='id identifier rubyid_token_data'>token_data</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span> <span class='op'>=</span> <span class='kw'>nil</span><span class='rparen'>)</span>
560
+ <span class='kw'>return</span> <span class='ivar'>@token_data</span> <span class='kw'>if</span> <span class='ivar'>@token_data</span>
561
+ <span class='kw'>return</span> <span class='kw'>nil</span> <span class='kw'>if</span> <span class='id identifier rubyid_params'>params</span><span class='period'>.</span><span class='id identifier rubyid_blank?'>blank?</span>
562
+ <span class='ivar'>@token</span> <span class='op'>=</span> <span class='id identifier rubyid_params'>params</span><span class='lbracket'>[</span><span class='symbol'>:token</span><span class='rbracket'>]</span>
563
+ <span class='kw'>return</span> <span class='kw'>nil</span> <span class='kw'>if</span> <span class='ivar'>@token</span><span class='period'>.</span><span class='id identifier rubyid_blank?'>blank?</span>
564
+ <span class='ivar'>@token_data</span> <span class='op'>=</span> <span class='const'>JWT</span><span class='period'>.</span><span class='id identifier rubyid_decode'>decode</span><span class='lparen'>(</span><span class='ivar'>@token</span><span class='comma'>,</span> <span class='id identifier rubyid_token_secret'>token_secret</span><span class='comma'>,</span> <span class='kw'>true</span><span class='comma'>,</span> <span class='lbrace'>{</span> <span class='label'>algorithm:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>HS512</span><span class='tstring_end'>&#39;</span></span> <span class='rbrace'>}</span><span class='rparen'>)</span>
565
+ <span class='period'>.</span><span class='id identifier rubyid_first'>first</span><span class='period'>.</span><span class='id identifier rubyid_with_indifferent_access'>with_indifferent_access</span>
566
+ <span class='kw'>rescue</span> <span class='const'>JWT</span><span class='op'>::</span><span class='const'>DecodeError</span> <span class='comment'># bad sig / malformed / expired — all subclasses
567
+ </span> <span class='kw'>nil</span>
568
+ <span class='kw'>end</span>
569
+
570
+ <span class='kw'>def</span> <span class='id identifier rubyid_token_secret'>token_secret</span>
571
+ <span class='const'>Rails</span><span class='period'>.</span><span class='id identifier rubyid_application'>application</span><span class='period'>.</span><span class='id identifier rubyid_credentials'>credentials</span><span class='period'>.</span><span class='id identifier rubyid_capability_token_secret'>capability_token_secret</span><span class='period'>.</span><span class='id identifier rubyid_presence'>presence</span> <span class='op'>||</span>
572
+ <span class='const'>Rails</span><span class='period'>.</span><span class='id identifier rubyid_application'>application</span><span class='period'>.</span><span class='id identifier rubyid_credentials'>credentials</span><span class='period'>.</span><span class='id identifier rubyid_secret_key_base'>secret_key_base</span> <span class='comment'># fallback until set
573
+ </span> <span class='kw'>end</span>
574
+ <span class='kw'>end</span>
575
+ </code></pre>
576
+
577
+ <p>Override <code>path</code> so links self-mint a token (callers pass the subject, not the token):</p>
578
+
579
+ <pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'><span class='object_link'><a href="Components.html" title="Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'>Invites</span><span class='op'>::</span><span class='const'>Accept</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components.html" title="Compony::Components (module)">Components</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/Components/New.html" title="Compony::Components::New (class)">New</a></span></span>
580
+ <span class='id identifier rubyid_include'>include</span> <span class='const'>WithToken</span>
581
+
582
+ <span class='kw'>class</span> <span class='const'>VirtualModel</span> <span class='op'>&lt;</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Compony/VirtualModel.html" title="Compony::VirtualModel (class)">VirtualModel</a></span></span>
583
+ <span class='id identifier rubyid_attribute'>attribute</span> <span class='symbol'>:password</span><span class='comma'>,</span> <span class='symbol'>:string</span>
584
+ <span class='id identifier rubyid_attribute'>attribute</span> <span class='symbol'>:account_id</span> <span class='comment'># carried for validation only
585
+ </span> <span class='id identifier rubyid_field'>field</span> <span class='symbol'>:password</span><span class='comma'>,</span> <span class='symbol'>:string</span>
586
+ <span class='kw'>def</span> <span class='id identifier rubyid_label'>label</span> <span class='op'>=</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Invite</span><span class='tstring_end'>&#39;</span></span>
587
+ <span class='kw'>end</span>
588
+
589
+ <span class='id identifier rubyid_setup'>setup</span> <span class='kw'>do</span>
590
+ <span class='comment'># Building a path to this component mints the token from the given account.
591
+ </span> <span class='id identifier rubyid_path'>path</span> <span class='kw'>do</span> <span class='op'>|</span><span class='op'>*</span><span class='id identifier rubyid_args'>args</span><span class='comma'>,</span> <span class='label'>account:</span> <span class='kw'>nil</span><span class='comma'>,</span> <span class='label'>token:</span> <span class='kw'>nil</span><span class='comma'>,</span> <span class='op'>**</span><span class='id identifier rubyid_kwargs'>kwargs</span><span class='op'>|</span>
592
+ <span class='kw'>if</span> <span class='id identifier rubyid_token'>token</span><span class='period'>.</span><span class='id identifier rubyid_blank?'>blank?</span>
593
+ <span class='id identifier rubyid_fail'>fail</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>Missing kwarg :account in path</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span> <span class='kw'>if</span> <span class='id identifier rubyid_account'>account</span><span class='period'>.</span><span class='id identifier rubyid_nil?'>nil?</span>
594
+ <span class='id identifier rubyid_token'>token</span> <span class='op'>=</span> <span class='id identifier rubyid_encode_token'>encode_token</span><span class='lparen'>(</span><span class='label'>account_id:</span> <span class='id identifier rubyid_account'>account</span><span class='period'>.</span><span class='id identifier rubyid_id'>id</span><span class='rparen'>)</span>
595
+ <span class='kw'>end</span>
596
+ <span class='kw'>next</span> <span class='const'>Rails</span><span class='period'>.</span><span class='id identifier rubyid_application'>application</span><span class='period'>.</span><span class='id identifier rubyid_routes'>routes</span><span class='period'>.</span><span class='id identifier rubyid_url_helpers'>url_helpers</span>
597
+ <span class='period'>.</span><span class='id identifier rubyid_send'>send</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>&quot;</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_path_helper_name'>path_helper_name</span><span class='embexpr_end'>}</span><span class='tstring_content'>_path</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='op'>*</span><span class='id identifier rubyid_args'>args</span><span class='comma'>,</span> <span class='label'>token:</span><span class='comma'>,</span> <span class='op'>**</span><span class='id identifier rubyid_kwargs'>kwargs</span><span class='rparen'>)</span>
598
+ <span class='kw'>end</span>
599
+
600
+ <span class='id identifier rubyid_standalone'>standalone</span> <span class='label'>path:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>/invites/accept</span><span class='tstring_end'>&#39;</span></span> <span class='kw'>do</span>
601
+ <span class='id identifier rubyid_skip_authentication!'>skip_authentication!</span>
602
+ <span class='id identifier rubyid_verb'>verb</span> <span class='symbol'>:get</span> <span class='kw'>do</span> <span class='id identifier rubyid_authorize'>authorize</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_token_valid?'>token_valid?</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span><span class='rparen'>)</span> <span class='rbrace'>}</span> <span class='kw'>end</span>
603
+ <span class='id identifier rubyid_verb'>verb</span> <span class='symbol'>:post</span> <span class='kw'>do</span> <span class='id identifier rubyid_authorize'>authorize</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_token_valid?'>token_valid?</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span><span class='rparen'>)</span> <span class='rbrace'>}</span> <span class='kw'>end</span>
604
+ <span class='kw'>end</span>
605
+
606
+ <span class='id identifier rubyid_data_class'>data_class</span> <span class='const'>VirtualModel</span>
607
+ <span class='id identifier rubyid_form_cancancan_action'>form_cancancan_action</span> <span class='kw'>nil</span>
608
+ <span class='id identifier rubyid_submit_path'>submit_path</span> <span class='lbrace'>{</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='period'>.</span><span class='id identifier rubyid_path'><span class='object_link'><a href="Compony.html#path-class_method" title="Compony.path (method)">path</a></span></span><span class='lparen'>(</span><span class='kw'>self</span><span class='period'>.</span><span class='id identifier rubyid_class'>class</span><span class='comma'>,</span> <span class='ivar'>@data</span><span class='comma'>,</span> <span class='label'>token:</span> <span class='ivar'>@token</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
609
+ <span class='id identifier rubyid_after_assign_attributes'>after_assign_attributes</span> <span class='lbrace'>{</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_account_id'>account_id</span> <span class='op'>=</span> <span class='id identifier rubyid_token_data'>token_data</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span><span class='rparen'>)</span><span class='lbracket'>[</span><span class='symbol'>:account_id</span><span class='rbracket'>]</span> <span class='rbrace'>}</span>
610
+
611
+ <span class='id identifier rubyid_store_data'>store_data</span> <span class='kw'>do</span>
612
+ <span class='ivar'>@create_succeeded</span> <span class='op'>=</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_validate'>validate</span>
613
+ <span class='kw'>next</span> <span class='kw'>unless</span> <span class='ivar'>@create_succeeded</span>
614
+ <span class='const'>Account</span><span class='period'>.</span><span class='id identifier rubyid_find'>find</span><span class='lparen'>(</span><span class='id identifier rubyid_token_data'>token_data</span><span class='lbracket'>[</span><span class='symbol'>:account_id</span><span class='rbracket'>]</span><span class='rparen'>)</span><span class='period'>.</span><span class='id identifier rubyid_update!'>update!</span><span class='lparen'>(</span><span class='label'>password:</span> <span class='ivar'>@data</span><span class='period'>.</span><span class='id identifier rubyid_password'>password</span><span class='rparen'>)</span>
615
+ <span class='kw'>end</span>
616
+ <span class='id identifier rubyid_on_created_respond'>on_created_respond</span> <span class='lbrace'>{</span> <span class='id identifier rubyid_redirect_to'>redirect_to</span> <span class='const'><span class='object_link'><a href="Compony.html" title="Compony (module)">Compony</a></span></span><span class='period'>.</span><span class='id identifier rubyid_path'><span class='object_link'><a href="Compony.html#path-class_method" title="Compony.path (method)">path</a></span></span><span class='lparen'>(</span><span class='symbol'>:show</span><span class='comma'>,</span> <span class='symbol'>:sessions</span><span class='rparen'>)</span> <span class='rbrace'>}</span>
617
+ <span class='kw'>end</span>
618
+
619
+ <span class='comment'># Shape-check the decoded payload; anything off → false → 403 (never 500).
620
+ </span> <span class='kw'>def</span> <span class='id identifier rubyid_token_valid?'>token_valid?</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span><span class='rparen'>)</span>
621
+ <span class='id identifier rubyid_data'>data</span> <span class='op'>=</span> <span class='id identifier rubyid_token_data'>token_data</span><span class='lparen'>(</span><span class='id identifier rubyid_params'>params</span><span class='rparen'>)</span>
622
+ <span class='kw'>return</span> <span class='kw'>false</span> <span class='kw'>if</span> <span class='id identifier rubyid_data'>data</span><span class='period'>.</span><span class='id identifier rubyid_blank?'>blank?</span>
623
+ <span class='const'>Schemacop</span><span class='op'>::</span><span class='const'>Schema3</span><span class='period'>.</span><span class='id identifier rubyid_new'>new</span><span class='lparen'>(</span><span class='symbol'>:hash</span><span class='rparen'>)</span> <span class='kw'>do</span>
624
+ <span class='id identifier rubyid_int!'>int!</span> <span class='symbol'>:account_id</span><span class='comma'>,</span> <span class='label'>cast_str:</span> <span class='kw'>true</span>
625
+ <span class='id identifier rubyid_int?'>int?</span> <span class='symbol'>:exp</span>
626
+ <span class='kw'>end</span><span class='period'>.</span><span class='id identifier rubyid_validate!'>validate!</span><span class='lparen'>(</span><span class='id identifier rubyid_data'>data</span><span class='rparen'>)</span>
627
+ <span class='kw'>true</span>
628
+ <span class='kw'>rescue</span> <span class='const'>Schemacop</span><span class='op'>::</span><span class='const'>Exceptions</span><span class='op'>::</span><span class='const'>ValidationError</span>
629
+ <span class='kw'>false</span> <span class='comment'># token signed for a different flow
630
+ </span> <span class='kw'>end</span>
631
+ <span class='kw'>end</span>
632
+ </code></pre>
633
+
634
+ <p>Notes:</p>
635
+ <ul><li>
636
+ <p><code>Compony.path(:accept, :invites, account: some_account)</code> returns the full tokenized URL — email that. The token, not a session, authorizes the request.</p>
637
+ </li><li>
638
+ <p><code>path do</code> runs outside the request context; build URLs via <code>Rails.application.routes.url_helpers</code>, not <code>controller</code>/<code>helpers</code> (see <a href="/doc/guide/standalone_md.html#customizing-path-generation">standalone.md</a>).</p>
639
+ </li><li>
640
+ <p>Reuse the mixin for every link flow (magic login, password reset, email confirm); encode a flow discriminator or rely on the per-component payload shape-check to stop a token minted for one flow being replayed against another.</p>
641
+ </li><li>
642
+ <p>One signed boolean in the payload (e.g. <code>confirmed: true</code>) is tamper-proof since the client cannot re-sign — handy for multi-hop confirm flows.</p>
643
+ </li></ul>
644
+
645
+ <h2 id="label-Good+habits">Good habits</h2>
646
+ <ul><li>
647
+ <p><strong>CanCanCan everywhere:</strong> <code>authorize { can?(...) }</code>, scope with <code>Model.accessible_by(current_ability)</code>, bang form <code>authorize!(:read, record)</code> for ad-hoc checks in <code>load_data</code>.</p>
648
+ </li><li>
649
+ <p><strong>Always <code>Compony.path</code> / <code>render_intent</code>,</strong> never hardcoded routes or <code>button_to</code> (<a href="/doc/guide/gotchas_md.html#11-redirect_to-with-a-hardcoded-path">gotchas.md #11</a>, <a href="/doc/guide/gotchas_md.html#15-hand-rolled-endpoint-where-a-pre-built-crud-component-exists">#15</a>).</p>
650
+ </li><li>
651
+ <p><strong>Place a resourceful component in the family of the model it acts on,</strong> not the family it is reached from; pass parent context via path params.</p>
652
+ </li><li>
653
+ <p><strong>Keep virtual/form-only fields off models</strong> — use ActiveType/VirtualModel (<a href="/doc/guide/gotchas_md.html#16-attr_accessor-on-a-model-for-form-only-fields">gotchas.md #16</a>).</p>
654
+ </li><li>
655
+ <p><strong><code>concat</code></strong> around every <code>render_intent</code>/<code>render_sub_comp</code>/<code>field</code> in a block.</p>
656
+ </li></ul>
657
+
658
+ <p><a href="/README_md.html#guide--documentation">Guide index</a></p>
659
+ </div></div>
660
+
661
+ <div id="footer">
662
+ Generated on Mon May 18 13:55:34 2026 by
663
+ <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
664
+ 0.9.34 (ruby-3.3.5).
665
+ </div>
666
+
667
+ </div>
668
+ </body>
669
+ </html>