superglue 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 825363825f0e6cf14431bd47cdc43b6be0dfcfb6723d19004ee99175bd98dda3
4
- data.tar.gz: 27a52faf2563be8e0242c2ed68a5e02c531137e03c5e67e5993e06b615139266
3
+ metadata.gz: e4b39909974d5f15cf6353b2a5748e4e31878b3dee8622d8874cc18395bf684c
4
+ data.tar.gz: c375f0687470c2afb7de11fb1b566bd87e6a5e51d200faea291f42a7bb7b7b95
5
5
  SHA512:
6
- metadata.gz: 8dc5c1de22f7d525e82b3e00f49cd774a814f689c8f0e5018e5f1192d694ae036af07b14c162b113e91911bfc918019784863adc7cde2bc9b9786784709d357d
7
- data.tar.gz: 5a9618185af23be3406af0153d54af83633cdc0a927337c7cd7d33f257150e6def65471b19c186ffe9d385312f979f9624f61b29c3c4ce9266c8a62820ca6b0c
6
+ metadata.gz: 4fa8626cc3b719c3df4f0c521e22a7bf7857bce6871c2b43bab714ab1ae019677bb8a854b257aa9a7181b928a399d518a6350f0b3aafcbe3237e3cd58ca4d8ea
7
+ data.tar.gz: 93798fbd725b53a4353415bc648fc7e8ffb0dffc046b10daa2a436398ea9c6d2e678a4ca0662d9e20ed001b1b5591830c81b982cfc7f6d9e6d69851e589c8ebb
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ <div align="center" style="padding: 30px 0px 20px 0px;">
2
+ <img src="https://thoughtbot.github.io/superglue/images/superglue.svg" data-origin="images/superglue.svg" alt="Logo" width=250>
3
+ </div>
4
+
5
+ # Superglue Rails
6
+ Use classic Rails to build rich React Redux applications with **NO APIs** and
7
+ **NO client-side routing**.
8
+
9
+ This is the officially supported Rails adapter to [Superglue]. This gem will
10
+ add install and scaffold generators to your rails project.
11
+
12
+ To get started, visit the [documentation] and add this to your Gemfile
13
+
14
+ [documentation]: https://thoughtbot.github.io/superglue/
15
+
16
+ ```
17
+ gem 'superglue'
18
+ ```
19
+
20
+ Then run `bundle`
21
+
22
+ [![Test superglue_rails](https://github.com/thoughtbot/superglue_rails/actions/workflows/build_rails.yml/badge.svg)](https://github.com/thoughtbot/superglue_rails/actions/workflows/build_rails.yml)
23
+
24
+ ## Documentation
25
+
26
+ Documentation is hosted on [Github pages](https://thoughtbot.github.io/superglue).
27
+
28
+ ## Contributing
29
+
30
+ Thank you, [contributors]!
31
+
32
+ [contributors]: https://github.com/thoughtbot/superglue_rails/graphs/contributors
@@ -16,6 +16,8 @@ module Superglue
16
16
  remove_file "#{app_js_path}/application.js"
17
17
 
18
18
  use_typescript = options["typescript"]
19
+ copy_erb_files
20
+
19
21
  if use_typescript
20
22
  copy_ts_files
21
23
  else
@@ -31,6 +33,9 @@ module Superglue
31
33
  say "Adding required member methods to ApplicationRecord"
32
34
  add_member_methods
33
35
 
36
+ say "Enabling jsx rendering defaults"
37
+ insert_jsx_rendering_defaults
38
+
34
39
  say "Installing Superglue and friends"
35
40
  run "yarn add react react-dom @reduxjs/toolkit react-redux @thoughtbot/superglue"
36
41
 
@@ -43,6 +48,45 @@ module Superglue
43
48
 
44
49
  private
45
50
 
51
+ def insert_jsx_rendering_defaults
52
+ inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::Base\n" do
53
+ <<-RUBY
54
+ # Enables Superglue rendering defaults for sensible view directories.
55
+ #
56
+ # without `use_jsx_rendering_defaults`:
57
+ #
58
+ # ```
59
+ # app/views/posts/
60
+ # - index.jsx
61
+ # - index.json.props
62
+ # - index.html.erb
63
+ # ```
64
+ #
65
+ # with `use_jsx_rendering_defaults`:
66
+ #
67
+ # ```
68
+ # app/views/posts/
69
+ # - index.jsx
70
+ # - index.json.props
71
+ # ```
72
+ #
73
+ # before_action :use_jsx_rendering_defaults
74
+ #
75
+ #
76
+ # The html template used when `use_jsx_rendering_defaults` is enabled.
77
+ # Defaults to "application/superglue".
78
+ #
79
+ # superglue_template "application/superglue"
80
+
81
+ RUBY
82
+ end
83
+ end
84
+
85
+ def copy_erb_files
86
+ say "Copying superglue.html.erb file to app/views/application/"
87
+ copy_file "#{__dir__}/templates/erb/superglue.html.erb", "app/views/application/superglue.html.erb"
88
+ end
89
+
46
90
  def copy_ts_files
47
91
  say "Copying application.tsx file to #{app_js_path}"
48
92
  copy_file "#{__dir__}/templates/ts/application.tsx", "#{app_js_path}/application.tsx"
@@ -0,0 +1,5 @@
1
+ <script type="text/javascript">
2
+ window.SUPERGLUE_INITIAL_PAGE_STATE=<%= render_props %>;<%# erblint:disable ErbSafety %>
3
+ </script>
4
+
5
+ <div id="app"></div>
@@ -7,14 +7,25 @@
7
7
  * There is no style and structured with bare necessities. You should modify
8
8
  * these components to fit your design needs.
9
9
  */
10
- import React, { useContext, createContext, useMemo } from "react";
11
- export const ValidationContext = createContext({});
12
- export const useErrorKeyValidation = ({ errorKey, }) => {
13
- const errors = useContext(ValidationContext);
14
- return useMemo(() => {
15
- return errors[errorKey];
16
- }, [errors, errorKey]);
17
- };
10
+ import React, { useContext, createContext, useMemo } from 'react'
11
+ export const ValidationContext = createContext({})
12
+ export const useErrorMessage = (errorKey) => {
13
+ const errors = useContext(ValidationContext)
14
+ return useMemo(() => {
15
+ if (!errorKey) {
16
+ return null
17
+ }
18
+ const validationError = errors[errorKey]
19
+ const hasErrors = errorKey && validationError
20
+ if (!hasErrors) {
21
+ return null
22
+ }
23
+ const errorMessages = Array.isArray(validationError)
24
+ ? validationError
25
+ : [validationError]
26
+ return errorMessages.join(' ')
27
+ }, [errors, errorKey])
28
+ }
18
29
  /**
19
30
  * Extras renders the hidden inputs generated by form_props.
20
31
  *
@@ -22,10 +33,12 @@ export const useErrorKeyValidation = ({ errorKey, }) => {
22
33
  * utf8, crsf_token, _method
23
34
  */
24
35
  export const Extras = (hiddenInputAttributes) => {
25
- const hiddenProps = Object.values(hiddenInputAttributes);
26
- const hiddenInputs = hiddenProps.map((props) => (<input {...props} type="hidden" key={props.name}/>));
27
- return <>{hiddenInputs}</>;
28
- };
36
+ const hiddenProps = Object.values(hiddenInputAttributes)
37
+ const hiddenInputs = hiddenProps.map((props) => (
38
+ <input {...props} type="hidden" key={props.name} />
39
+ ))
40
+ return <>{hiddenInputs}</>
41
+ }
29
42
  /**
30
43
  * A basic form component that supports inline errors.
31
44
  *
@@ -33,13 +46,15 @@ export const Extras = (hiddenInputAttributes) => {
33
46
  * Rails forms are generated.
34
47
  */
35
48
  export const Form = ({ extras, validationErrors = {}, children, ...props }) => {
36
- return (<form {...props}>
49
+ return (
50
+ <form {...props}>
37
51
  <ValidationContext.Provider value={validationErrors}>
38
52
  <Extras {...extras}></Extras>
39
53
  {children}
40
54
  </ValidationContext.Provider>
41
- </form>);
42
- };
55
+ </form>
56
+ )
57
+ }
43
58
  /**
44
59
  * An inline error component.
45
60
  *
@@ -47,84 +62,110 @@ export const Form = ({ extras, validationErrors = {}, children, ...props }) => {
47
62
  * Please modify this to your liking.
48
63
  */
49
64
  export const FieldError = ({ errorKey }) => {
50
- const errors = useContext(ValidationContext);
51
- if (!errorKey || !errors) {
52
- return null;
53
- }
54
- const validationError = errors[errorKey];
55
- const hasErrors = errorKey && validationError;
56
- if (!hasErrors) {
57
- return null;
58
- }
59
- const errorMessages = Array.isArray(validationError)
60
- ? validationError
61
- : [validationError];
62
- return <span>{errorMessages.join(" ")}</span>;
63
- };
65
+ const errorMessage = useErrorMessage(errorKey)
66
+ return <span>{errorMessage}</span>
67
+ }
64
68
  /**
65
69
  * A Field component.
66
70
  *
67
71
  * Combines a label, input and a FieldError. Please modify this to your liking.
68
72
  */
69
73
  export const FieldBase = ({ label, errorKey, children, ...props }) => {
70
- return (<>
74
+ return (
75
+ <>
71
76
  <label htmlFor={props.id}>{label}</label>
72
- {children || <input {...props}/>}
73
- <FieldError errorKey={errorKey}/>
74
- </>);
75
- };
76
- export const Checkbox = ({ type: _type, includeHidden, uncheckedValue, errorKey, ...rest }) => {
77
- const { name } = rest;
78
- return (<FieldBase {...rest} errorKey={errorKey}>
79
- {includeHidden && (<input type="hidden" name={name} defaultValue={uncheckedValue} autoComplete="off"/>)}
77
+ {children || <input {...props} />}
78
+ <FieldError errorKey={errorKey} />
79
+ </>
80
+ )
81
+ }
82
+ export const Checkbox = ({
83
+ type: _type,
84
+ includeHidden,
85
+ uncheckedValue,
86
+ errorKey,
87
+ ...rest
88
+ }) => {
89
+ const { name } = rest
90
+ return (
91
+ <FieldBase {...rest} errorKey={errorKey}>
92
+ {includeHidden && (
93
+ <input
94
+ type="hidden"
95
+ name={name}
96
+ defaultValue={uncheckedValue}
97
+ autoComplete="off"
98
+ />
99
+ )}
80
100
  <input type="checkbox" {...rest}></input>
81
- </FieldBase>);
82
- };
101
+ </FieldBase>
102
+ )
103
+ }
83
104
  /**
84
105
  * A collection checkbox component.
85
106
  *
86
107
  * Designed to work with a payload form_props's [collection_check_boxes helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#collection-select).
87
108
  * Mimics the rails equivalent. Please modify to your liking.
88
109
  */
89
- export const CollectionCheckboxes = ({ includeHidden, collection, label, errorKey, }) => {
90
- if (collection.length == 0) {
91
- return null;
92
- }
93
- const checkboxes = collection.map((options) => {
94
- return <Checkbox {...options} key={options.id}/>;
95
- });
96
- const { name } = collection[0];
97
- return (<>
98
- {includeHidden && (<input type="hidden" name={name} defaultValue={""} autoComplete="off"/>)}
110
+ export const CollectionCheckboxes = ({
111
+ includeHidden,
112
+ collection,
113
+ label,
114
+ errorKey,
115
+ }) => {
116
+ if (collection.length == 0) {
117
+ return null
118
+ }
119
+ const checkboxes = collection.map((options) => {
120
+ return <Checkbox {...options} key={options.id} />
121
+ })
122
+ const { name } = collection[0]
123
+ return (
124
+ <>
125
+ {includeHidden && (
126
+ <input type="hidden" name={name} defaultValue={''} autoComplete="off" />
127
+ )}
99
128
  <label>{label}</label>
100
129
  {checkboxes}
101
- <FieldError errorKey={errorKey}/>
102
- </>);
103
- };
130
+ <FieldError errorKey={errorKey} />
131
+ </>
132
+ )
133
+ }
104
134
  /**
105
135
  * A collection radio button component.
106
136
  *
107
137
  * Designed to work with a payload form_props's [collection_radio_buttons helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#collection-select).
108
138
  * Mimics the rails equivalent. Please modify to your liking.
109
139
  */
110
- export const CollectionRadioButtons = ({ includeHidden, collection, label, errorKey, }) => {
111
- if (collection.length == 0) {
112
- return null;
113
- }
114
- const radioButtons = collection.map((options) => {
115
- return (<div key={options.value}>
116
- <input {...options} type="radio"/>
140
+ export const CollectionRadioButtons = ({
141
+ includeHidden,
142
+ collection,
143
+ label,
144
+ errorKey,
145
+ }) => {
146
+ if (collection.length == 0) {
147
+ return null
148
+ }
149
+ const radioButtons = collection.map((options) => {
150
+ return (
151
+ <div key={options.value}>
152
+ <input {...options} type="radio" />
117
153
  <label htmlFor={options.id}>{options.label}</label>
118
- </div>);
119
- });
120
- const { name } = collection[0];
121
- return (<>
122
- {includeHidden && (<input type="hidden" name={name} defaultValue={""} autoComplete="off"/>)}
154
+ </div>
155
+ )
156
+ })
157
+ const { name } = collection[0]
158
+ return (
159
+ <>
160
+ {includeHidden && (
161
+ <input type="hidden" name={name} defaultValue={''} autoComplete="off" />
162
+ )}
123
163
  <label>{label}</label>
124
164
  {radioButtons}
125
- <FieldError errorKey={errorKey}/>
126
- </>);
127
- };
165
+ <FieldError errorKey={errorKey} />
166
+ </>
167
+ )
168
+ }
128
169
  /**
129
170
  * A text field component.
130
171
  *
@@ -132,8 +173,8 @@ export const CollectionRadioButtons = ({ includeHidden, collection, label, error
132
173
  * Mimics the rails equivalent. Please modify to your liking.
133
174
  */
134
175
  export const TextField = ({ type: _type, ...rest }) => {
135
- return <FieldBase {...rest} type="text"/>;
136
- };
176
+ return <FieldBase {...rest} type="text" />
177
+ }
137
178
  /**
138
179
  * A email field component.
139
180
  *
@@ -141,8 +182,8 @@ export const TextField = ({ type: _type, ...rest }) => {
141
182
  * Mimics the rails equivalent. Please modify to your liking.
142
183
  */
143
184
  export const EmailField = ({ type: _type, ...rest }) => {
144
- return <FieldBase {...rest} type="email"/>;
145
- };
185
+ return <FieldBase {...rest} type="email" />
186
+ }
146
187
  /**
147
188
  * A color field component.
148
189
  *
@@ -150,8 +191,8 @@ export const EmailField = ({ type: _type, ...rest }) => {
150
191
  * Mimics the rails equivalent. Please modify to your liking.
151
192
  */
152
193
  export const ColorField = ({ type: _type, ...rest }) => {
153
- return <FieldBase {...rest} type="color"/>;
154
- };
194
+ return <FieldBase {...rest} type="color" />
195
+ }
155
196
  /**
156
197
  * A date field component.
157
198
  *
@@ -159,8 +200,8 @@ export const ColorField = ({ type: _type, ...rest }) => {
159
200
  * Mimics the rails equivalent. Please modify to your liking.
160
201
  */
161
202
  export const DateField = ({ type: _type, ...rest }) => {
162
- return <FieldBase {...rest} type="date"/>;
163
- };
203
+ return <FieldBase {...rest} type="date" />
204
+ }
164
205
  /**
165
206
  * A date field component.
166
207
  *
@@ -168,8 +209,8 @@ export const DateField = ({ type: _type, ...rest }) => {
168
209
  * Mimics the rails equivalent. Please modify to your liking.
169
210
  */
170
211
  export const DateTimeLocalField = ({ type: _type, ...rest }) => {
171
- return <FieldBase {...rest} type="datetime-local"/>;
172
- };
212
+ return <FieldBase {...rest} type="datetime-local" />
213
+ }
173
214
  /**
174
215
  * A search field component.
175
216
  *
@@ -177,8 +218,8 @@ export const DateTimeLocalField = ({ type: _type, ...rest }) => {
177
218
  * Mimics the rails equivalent. Please modify to your liking.
178
219
  */
179
220
  export const SearchField = ({ type: _type, ...rest }) => {
180
- return <FieldBase {...rest} type="search"/>;
181
- };
221
+ return <FieldBase {...rest} type="search" />
222
+ }
182
223
  /**
183
224
  * A tel field component.
184
225
  *
@@ -186,8 +227,8 @@ export const SearchField = ({ type: _type, ...rest }) => {
186
227
  * Mimics the rails equivalent. Please modify to your liking.
187
228
  */
188
229
  export const TelField = ({ type: _type, ...rest }) => {
189
- return <FieldBase {...rest} type="tel"/>;
190
- };
230
+ return <FieldBase {...rest} type="tel" />
231
+ }
191
232
  /**
192
233
  * A url field component.
193
234
  *
@@ -195,8 +236,8 @@ export const TelField = ({ type: _type, ...rest }) => {
195
236
  * Mimics the rails equivalent. Please modify to your liking.
196
237
  */
197
238
  export const UrlField = ({ type: _type, ...rest }) => {
198
- return <FieldBase {...rest} type="url"/>;
199
- };
239
+ return <FieldBase {...rest} type="url" />
240
+ }
200
241
  /**
201
242
  * A month field component.
202
243
  *
@@ -204,17 +245,17 @@ export const UrlField = ({ type: _type, ...rest }) => {
204
245
  * Mimics the rails equivalent. Please modify to your liking.
205
246
  */
206
247
  export const MonthField = ({ type: _type, ...rest }) => {
207
- return <FieldBase {...rest} type="month"/>;
208
- };
248
+ return <FieldBase {...rest} type="month" />
249
+ }
209
250
  /**
210
- * A month field component.
251
+ * A time field component.
211
252
  *
212
253
  * Designed to work with a payload form_props's [month_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#date-helpers).
213
254
  * Mimics the rails equivalent. Please modify to your liking.
214
255
  */
215
256
  export const TimeField = ({ type: _type, ...rest }) => {
216
- return <FieldBase {...rest} type="time"/>;
217
- };
257
+ return <FieldBase {...rest} type="time" />
258
+ }
218
259
  /**
219
260
  * A number field component.
220
261
  *
@@ -222,8 +263,8 @@ export const TimeField = ({ type: _type, ...rest }) => {
222
263
  * Mimics the rails equivalent. Please modify to your liking.
223
264
  */
224
265
  export const NumberField = ({ type: _type, ...rest }) => {
225
- return <FieldBase {...rest} type="number"/>;
226
- };
266
+ return <FieldBase {...rest} type="number" />
267
+ }
227
268
  /**
228
269
  * A range field component.
229
270
  *
@@ -231,8 +272,8 @@ export const NumberField = ({ type: _type, ...rest }) => {
231
272
  * Mimics the rails equivalent. Please modify to your liking.
232
273
  */
233
274
  export const RangeField = ({ type: _type, ...rest }) => {
234
- return <FieldBase {...rest} type="range"/>;
235
- };
275
+ return <FieldBase {...rest} type="range" />
276
+ }
236
277
  /**
237
278
  * A password field component.
238
279
  *
@@ -240,8 +281,8 @@ export const RangeField = ({ type: _type, ...rest }) => {
240
281
  * Mimics the rails equivalent. Please modify to your liking.
241
282
  */
242
283
  export const PasswordField = ({ type: _type, ...rest }) => {
243
- return <FieldBase {...rest} type="password"/>;
244
- };
284
+ return <FieldBase {...rest} type="password" />
285
+ }
245
286
  /**
246
287
  * A select component.
247
288
  *
@@ -250,26 +291,46 @@ export const PasswordField = ({ type: _type, ...rest }) => {
250
291
  *
251
292
  * Please modify to your liking.
252
293
  */
253
- export const Select = ({ includeHidden, name, id, children, options, multiple, type: _type, ...rest }) => {
254
- const addHidden = includeHidden && multiple;
255
- const optionElements = options.map((item) => {
256
- if ("options" in item) {
257
- return (<optgroup label={item.label} key={item.label}>
258
- {item.options.map((opt) => (<option key={opt.label} {...opt}/>))}
259
- </optgroup>);
260
- }
261
- else {
262
- return <option key={item.label} {...item}/>;
263
- }
264
- });
265
- return (<>
266
- {addHidden && (<input type="hidden" name={name} value={""} autoComplete="off"/>)}
267
- <select name={name} id={id} multiple={multiple} {...rest}>
268
- {children}
269
- {optionElements}
270
- </select>
271
- </>);
272
- };
294
+ export const Select = ({
295
+ includeHidden,
296
+ name,
297
+ id,
298
+ children,
299
+ options,
300
+ label,
301
+ errorKey,
302
+ multiple,
303
+ type: _type,
304
+ ...rest
305
+ }) => {
306
+ const addHidden = includeHidden && multiple
307
+ const optionElements = options.map((item) => {
308
+ if ('options' in item) {
309
+ return (
310
+ <optgroup label={item.label} key={item.label}>
311
+ {item.options.map((opt) => (
312
+ <option key={opt.label} {...opt} />
313
+ ))}
314
+ </optgroup>
315
+ )
316
+ } else {
317
+ return <option key={item.label} {...item} />
318
+ }
319
+ })
320
+ return (
321
+ <>
322
+ {addHidden && (
323
+ <input type="hidden" name={name} value={''} autoComplete="off" />
324
+ )}
325
+ <FieldBase label={label} errorKey={errorKey} id={id}>
326
+ <select name={name} id={id} multiple={multiple} {...rest}>
327
+ {children}
328
+ {optionElements}
329
+ </select>
330
+ </FieldBase>
331
+ </>
332
+ )
333
+ }
273
334
  /**
274
335
  * A text area component.
275
336
  *
@@ -277,11 +338,13 @@ export const Select = ({ includeHidden, name, id, children, options, multiple, t
277
338
  * Mimics the rails equivalent. Please modify to your liking.
278
339
  */
279
340
  export const TextArea = ({ type: _type, errorKey, ...rest }) => {
280
- const { label } = rest;
281
- return (<FieldBase label={label} errorKey={errorKey} id={rest.id}>
282
- <textarea {...rest}/>
283
- </FieldBase>);
284
- };
341
+ const { label } = rest
342
+ return (
343
+ <FieldBase label={label} errorKey={errorKey} id={rest.id}>
344
+ <textarea {...rest} />
345
+ </FieldBase>
346
+ )
347
+ }
285
348
  /**
286
349
  * A file field component.
287
350
  *
@@ -289,8 +352,8 @@ export const TextArea = ({ type: _type, errorKey, ...rest }) => {
289
352
  * Mimics the rails equivalent. Please modify to your liking.
290
353
  */
291
354
  export const FileField = ({ type: _type, ...rest }) => {
292
- return <FieldBase {...rest} type="file"/>;
293
- };
355
+ return <FieldBase {...rest} type="file" />
356
+ }
294
357
  /**
295
358
  * A SubmitButton component.
296
359
  *
@@ -298,5 +361,10 @@ export const FileField = ({ type: _type, ...rest }) => {
298
361
  * Mimics the rails equivalent. Please modify to your liking.
299
362
  */
300
363
  export const SubmitButton = ({ type: _type, text, ...rest }) => {
301
- return <button {...rest} type="submit"> {text} </button>;
302
- };
364
+ return (
365
+ <button {...rest} type="submit">
366
+ {' '}
367
+ {text}{' '}
368
+ </button>
369
+ )
370
+ }