-
Notifications
You must be signed in to change notification settings - Fork 92
/
rapid_forms.dryml
943 lines (714 loc) · 39.3 KB
/
rapid_forms.dryml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
<!-- Rapid Forms provides various tags that make it quick and easy to produce working new or edit forms.
### Overview
The main tags are:
- `<form>`, which acts like the dumb HTML tag if you provide the `action` attribute, and picks up various Rapid smarts
otherwise.
- `<input>`, which automatically choses an appropriate form control based on the type of the date.
### Ajax Attributes
Several of the tags in this taglib support the following set of ajax attributes:
- update: one or more DOM ID's (comma separated string or an array) to be updated as part of the ajax call. Default - no
update.
NOTE: yes that's *DOM ID's* not part-names. A common source of confusion because by default the part name and DOM ID are
the same.
- params: a hash of name/value pairs that will be converted to HTTP parameters in the ajax request
- confirm: a message to be displayed in a JavaScript confirm dialog. By default there is no confirm dialog
- message: a message to be displayed in the Ajax progress spinner. Default: "Saving..."
- spinner-next-to: DOM ID of an element to position the ajax progress spinner next to.
### Ajax Callbacks
The following attributes are also supported by all the ajax tags. Set them to fragments of javascript to have that script
executed at various points in the ajax request cycle:
- before: script to run before the request
- success: script to run on successful completion of the request
- failure: script to run on a request failure
- complete: script to run on completion, regardless of success or failure.
-->
<!-- nodoc. -->
<def tag="hidden-fields" attrs="fields, for-query-string, skip"><%=
pairs = if for_query_string
query_params.to_a
else
hiddens = case fields
when '*', nil
# TODO: Need a better (i.e. extensible) way to eleminate certain fields
this.class.column_names - ['type', 'created_at', 'updated_at']
else
comma_split(fields)
end
hiddens.map do |field|
val = this.send(field)
param_name = param_name_for(form_field_path + [field])
[param_name, val] unless val.nil? ||
field.to_sym.in?(this.class.attr_protected) ||
(this.new_record? && val == this.class.column(field).default)
end.compact
end
skip = comma_split skip
pairs.reject! { |p| p.first.in?(skip) }
pairs.map { |n, v| hidden_field_tag(n, v.to_s) if v && n.not_in?(scope.form_field_names) }.compact.join("\n")
%></def>
<!--
`<form>` has been extended in Rapid to make it easier to construct and use forms with Hobo models. In addition to the base
`<form>` tag, a form with contents is generated for each Hobo model. These are found in
`app/views/taglibs/auto/rapid/forms.dryml`.
### Usage
`<form>` can be used as a regular HTML tag:
<form action="/blog_posts/1" method="POST">...</form>
If no `action` attribute is provided then the context is used to construct an appropriate action using restful routing:
* If the context is a new record then the form action will be a `POST` to the create action:
<form with="&BlogPost.new">...</form> -> <form action="/blog_posts" method="POST">...</form>
* If the context is a saved record then the form action will be a `PUT` to the update action. This is handled in a special
way by Rails due to current browsers not supporting `PUT`, the method is set to `POST` with a hidden input called `_method`
with a value of `PUT`. Hobo adds this automatically:
<% blog_post = BlogPost.find(1) %>
<form with="&blog_post">...</form> ->
<form action="/blog_posts/1" method="POST">
<input id="_method" type="hidden" value="PUT" name="_method"/>
...
</form>
AJAX based submission can be enabled by simply adding an `update` attribute. e.g.
<div part="comments"><collection:comments/></div>
<form with="&Comment.new" update="comments"/>
`<form>` support all of the standard ajax attributes.
### Additional Notes
- Hobo automatically inserts an `auth_token` hidden field if forgery protection is enabled
- Hobo inserts a `page_path` hidden field in create / update forms which it uses to re-render the correct page if a
validation error occurs.
- `<form>` supports all of the standrd ajax attributes - (see the main taglib docs for Rapid Forms)
### Attributes
- reset-form: Clear the form after submission (only makes sense for ajax forms)
- refocus-form: Refocus the first form-field after submission (only makes sense for ajax forms)
-->
<def tag="form" polymorphic attrs="update, hidden-fields, action, method, web-method, lifecycle, owner, multipart"><%=
ajax_attrs, html_attrs = attributes.partition_hash(Hobo::RapidHelper::AJAX_ATTRS)
html_attrs[:enctype] ||= "multipart/form-data" if multipart
new_record = this.try.new_record?
method = if method.nil?
(action || web_method || new_record) ? "post" : "put"
else
method.downcase
end
html_attrs[:action] = action || begin
target = if owner
collection_name = this.class.reverse_reflection(owner).name
this.send(owner).send(collection_name)
else
this
end
action = web_method || lifecycle
object_url(target, action, :method => method)
end
if action.nil? && (html_attrs[:action].nil? ||
(lifecycle.nil? && new_record && !this.creatable_by?(current_user)) ||
(lifecycle.nil? && !new_record && !can_edit?))
Hobo::Dryml.last_if = false
""
else
if method == "put"
# browsers don't support put -- use post and add the Rails _method hack
http_method_hidden = hidden_field_tag("_method", "PUT")
html_attrs[:method] = "post"
else
html_attrs[:method] = method
end
if update || !ajax_attrs.empty?
# add an onsubmit to convert to an ajax form if `update` is given
function = ajax_updater(:post_form, update, ajax_attrs)
html_attrs[:onsubmit] = [html_attrs[:onsubmit], "#{function}; return false;"].compact.join("; ")
end
hiddens = nil
body = with_form_context do
# It is important to evaluate parameters.default first, in order to populate scope.form_field_names
b = parameters.default
hiddens = self.hidden_fields(:fields => hidden_fields) if new_record
b
end
auth_token = if method.nil? || method == 'get' || !protect_against_forgery?
''
else
element(:input, {:type => "hidden",
:name => request_forgery_protection_token.to_s,
:value => form_authenticity_token}, nil, true, true)
end
unless method == "get"
page_path = if (request.post? || request.put?) && params[:page_path]
params[:page_path]
else
view_name.sub(Hobo::Dryml::EMPTY_PAGE, params[:action] || '')
end
page_path_hidden = hidden_field_tag("page_path", page_path)
end
hiddens_div = element(:div, {:class => "hidden-fields"}, [http_method_hidden, page_path_hidden, auth_token, hiddens].join)
body = [hiddens_div, body].join
if action.nil? # don't add automatic css classes if the action was specified
if web_method
add_classes!(html_attrs, "#{type_id.dasherize}-#{web_method}-form")
else
add_classes!(html_attrs, "#{'new ' if new_record}#{type_id.dasherize}")
end
end
Hobo::Dryml.last_if = true
element("form", html_attrs, body)
end
%></def>
<!-- A shortcut for generating a submit button.
### Usage
<submit label="Go!"/> -> <input type="submit" value="Go!" class="button submit-button"/>
<submit image="/images/go.png"/> -> <input type="image" src="/images/go.png" class="button submit-button"/>
-->
<def tag="submit" attrs="label, image">
<input if="&image" type="image" src="&image" merge-attrs class="image-button submit-button"/>
<else>
<input type="submit" value="#{label}" merge-attrs class="button submit-button"/>
</else>
</def>
<!--
Provides an editable control tailored to the type of the object in context. `<input>` tags should be used within a
`<form>`. `<input>` is a _polymorphic_ tag which means that there are a variety of definitions, each one written for a
particular type. For example there are inputs for `text`, `boolean`, `password`, `date`, `datetime`, `integer`,
`float`, `string` and more.
### Usage
The tag behaves as a regular HTML input if the type attribute is given:
<input type="text" name="my_input"/> -> Output is exactly as provided, untouched by Rapid
If no type attribute is given then the _context_ is used. For example if the context is a blog post:
<input:title/> ->
<input id="blog_post[name]" class="string blog-post-name" type="text" value="My Blog Post" name="blog_post[name]"/>
<input:created_at/> ->
<select id="blog_post_created_at_year" name="blog_post[created_at][year]">...</select>
<select id="blog_post_created_at_month" name="blog_post[created_at][month]">...</select>
<select id="blog_post_created_at_day" name="blog_post[created_at][day]">...</select>
<input:description/> ->
<textarea class="text blog-post-description" id="blog_post[description]" name="blog_post[description]">...</textarea>
If the context is a `belongs_to` association, the `<select-one>` tag is used.
If the context is a `has_many :through` association, the polymorphic `<collection-input>` tag is used.
### Attributes
- no-edit: control what happens if `can_edit?` is false. Can be one of:
- view: render the current value using the `<view>` tag
- disable: render the input as normal, but add HTML's `disabled` attribute
- skip: render nothing at all
- ignore: render the input normally. That is, don't even perform the edit check.
-->
<def tag="input" attrs="no-edit"><%=
if attributes[:type]
element :input, attributes, nil, true, true
else
no_edit ||= :view
no_edit = no_edit.to_sym
no_edit_permission = !can_edit? unless no_edit == :ignore
if no_edit_permission && no_edit == :view
view
elsif no_edit_permission && no_edit == :skip
""
else
attrs = add_classes(attributes, type_id.dasherize, type_and_field.dasherize)
attrs[:name] ||= param_name_for_this
attrs[:disabled] = true if no_edit_permission && no_edit == :disable
the_input = if (refl = this_field_reflection)
if refl.macro == :belongs_to
call_polymorphic_tag('input', attrs) or select_one(attrs)
elsif refl.macro == :has_many
if refl.options[:through]
collection_input(attrs)
else
input_many(attrs)
end
end
else
call_polymorphic_tag('input', attrs) or
(call_polymorphic_tag('input', HoboFields.to_class(this_type::COLUMN_TYPE), attrs) if defined?(this_type::COLUMN_TYPE)) or
raise HoboError, ("No input tag for #{this_field}:#{this_type} (this=#{this.inspect})")
end
if this_parent.errors[this_field]
"<span class='field-with-errors'>#{the_input}</span>"
else
the_input
end
end
end
%></def>
<!-- This tag is called by `<input>` when the context is a `has_many :through` collection. By default a `<select-many>`
is used, but this can be customised on a per-type basis. For example, say you would like the `<check-many>` tag used to
edit collections a `Category` model in your application:
<def tag="collection-input" for="Category"><check-many merge/></def>
-->
<def tag="collection-input" polymorphic></def>
<!-- The default `<collection-input>` - calls `<select-many>` -->
<def tag="collection-input" for="ActiveRecord::Base"><select-many merge/></def>
<!-- A `<textarea>` input -->
<def tag="input" for="text" attrs="name">
<%= text_area_tag(name, this, attributes) %>
</def>
<!-- A checkbox plus a hidden-field. The hidden field trick comes from Rails - it means that when the checkbox is not checked, the parameter name is still submitted, with a '0' value (the value is '1' when the checkbox is checked) -->
<def tag="input" for="boolean" attrs="name">
<%= unless attributes[:disabled]
cb_tag = check_box_tag(name, '1', this, attributes)
cb_hidden_tag = hidden_field_tag(name, '0', :id => nil)
HoboSupport::RAILS_AT_LEAST_23 ? cb_hidden_tag + cb_tag : cb_tag + cb_hidden_tag
end %>
</def>
<!-- A password input - `<input type='password'>` -->
<def tag="input" for="password" attrs="name">
<%= password_field_tag(name, this, attributes) %>
</def>
<!-- A date picker, using the `select_date` helper from Rails
### Attributes
- order: The order of the year, month and day menus. A comma separated string or an array. Default: "year, month, day"
Any other attributes are passed through to the `select_date` helper.
The menus default to the current date if the current value is nil.
-->
<def tag="input" for="date" attrs="order">
<% order = order.nil? ? [:year, :month, :day] : comma_split(order).*.to_sym -%>
<%= select_date(this || current_time, attributes.merge(:prefix => param_name_for_this, :order => order)) %>
</def>
<!-- A date/time picker, using the `select_date` helper from Rails
### Attributes
- order: The order of the year, month and date menus. A comma separated string or an array. Default: "year, month,
day, hour, minute, second"
Any other attributes are passed through to the `select_date` helper.
The menus default to the current time if the current value is nil.
-->
<def tag="input" for="time" attrs="order">
<% order = order.nil? ? [:year, :month, :day, :hour, :minute, :second] : comma_split(order).*.to_sym -%>
<%= select_date(this || current_time, attributes.merge(:prefix => param_name_for_this, :order => order)) %>
</def>
<!-- A date/time picker, using the `select_datetime` helper from Rails
### Attributes
- order: The order of the year, month and date menus. A comma separated string or an array. Default: "year, month,
day, hour, minute"
Any other attributes are passed through to the `select_datetime` helper.
The menus default to the current time if the current value is nil.
-->
<def tag="input" for="datetime" attrs="order">
<% if ! order.nil?
order = comma_split(order).*.to_sym
attributes.merge!(:order => order)
end -%>
<%= select_datetime(this || current_time, attributes.merge(:prefix => param_name_for_this)) %>
</def>
<!-- An `<input type='text'>` input. -->
<def tag="input" for="integer" attrs="name">
<%= text_field_tag(name, this, attributes) %>
</def>
<!-- An `<input type='text'>` input. -->
<def tag="input" for="BigDecimal" attrs="name">
<%= text_field_tag(name, this, attributes) %>
</def>
<!-- An `<input type='text'>` input. -->
<def tag="input" for="float" attrs="name">
<%= text_field_tag(name, this, attributes) %>
</def>
<!-- An `<input type='text'>` input. -->
<def tag="input" for="string" attrs="name">
<%= text_field_tag(name, this, attributes) %>
</def>
<!-- A `<select>` menu containing the values of an 'enum string'.
### Attributes
- `labels` - A hash that gives custom labels for the values of the enum.
Any values that do not have corresponding keys in this hash will get `value.titleize` as the label.
- `titleize` - Set to false to have the value itself (rather than `value.titleize`) be the default label. Default: true
- `first-option` - a string to be used for an extra option in the first position. E.g. "Please choose..."
- `first-value` - the value to be used with the `first-option`. Typically not used, meaning the option has a blank value.
-->
<def tag="input" for="HoboFields::EnumString" attrs="labels, titleize, first-option, first-value"><%
labels ||= {}
titleize = true if titleize.nil?
options = this_type.values.map {|v| [labels.fetch(v.to_sym, titleize ? v.titleize : v), v] }
%>
<select name="#{param_name_for_this}" merge-attrs>
<option value="#{first_value}" unless="&first_option.nil?"><first-option/></option>
<%= options_for_select(options, this) %>
</select>
</def>
<!-- Provides either an ajax or non-ajax button to invoke a "remote method" or "web method" declared in the controller.
Web Methods provide support for the RPC model of client-server interaction, in contrast to the REST model. The
preference in Rails is to use REST as much as possible, but we are pragmatists, and sometimes you just to need a remote
procedure call.
The URL that the call is POSTed to is the `object_url` of `this`, plus the method name
`<remote-method-button>` supports all of the standard ajax attributes (see the main taglib documention for Rapid
Forms). If any ajax attributes are given, the button becomes an ajax button, if not,
### Attributes
- method: the name of the web-method to call
- label: the label on the button
-->
<def tag="remote-method-button" attrs="method, update, label, confirm, url"><%=
ajax_attributes, html_attributes = attributes.partition_hash(Hobo::RapidHelper::AJAX_ATTRS)
url ||= object_url(this, method.to_s.gsub('-', '_'), :method => :post)
raise ArgumentError, "no such web method '#{method}' on #{this.typed_id}" unless url
add_classes!(html_attributes, "button remote-method-button #{method}-button")
label ||= method.titleize
if update || !ajax_attributes.empty?
ajax_attributes[:message] ||= label
func = ajax_updater(url, update, ajax_attributes.merge(:confirm => confirm))
html_attributes.update(:onclick => "var e = this; " + func, :type =>'button', :value => label)
element(:input, html_attributes, nil, true, true)
else
button_to(label, url, html_attributes.merge(:confirm => confirm))
end
%></def>
<!-- Provides an ajax button to send a RESTful update or "PUT" to the server. i.e. to udate one or more fields of a
record.
Note that unlike simliar tags, `<update-button>` does not support both ajax and non-ajax modes at this time. It only
does ajax.
`<update-button>` supports all of the standard ajax attributes (see the main taglib documention for Rapid Forms).
### Attributes
- label: The label on the button.
- fields: A hash with new field values pairs to update the resource with. The items in the hash will be converted to
HTTP parameters.
- params: Another hash with additional HTTP parameters to include in the ajax request
-->
<def tag="update-button" attrs="label, update, fields, params"><%=
raise HoboError.new("no update specified") unless update
ajax_attributes, html_attributes = attributes.partition_hash(Hobo::RapidHelper::AJAX_ATTRS)
params = (params || {}).merge(this.class.name.underscore => fields)
ajax_attributes.reverse_merge!(:message => label, :params => params, :method => :put)
func = ajax_updater(object_url(this), update, ajax_attributes)
html_attributes.reverse_merge!(:type =>'button', :onclick => func, :value => label)
element :input, add_classes(html_attributes, "button update-button update-#{this.class.name.underscore}-button"), nil, true, true %>
</def>
<!-- Provides either an ajax or non-ajax delete button to send a RESTful "DELETE". The context should be a record for
which you to want provide a delete button.
The Rapid Library has a convention of marking (in the output HTML, using a special CSS class) elements as "object
elements", with the class and ID of the ActiveRecord object that they represent. `<delete-button>` assumes it is placed
inside such an element, and will automatically find the right element to remove (fade out) from the DOM. The
`<collection>` tag adds this metadata (CSS class) automatically, so `<delete-button>` works well when used inside a
`<collection>`. This is a Clever Trick which needs to be revisted and perhaps simplified.
If used within a `<collection>`, `<delete-button>` also knows how to add an "empty message" such as "no comments to
display" when you delete the last item. Clever Tricks abound.
Current limitation: There is no support for the ajax callbacks at this time.
All the standard ajax attributes *except the callbacks* are supported (see the main taglib documention for Rapid Forms).
### Attributes
- label: The label for the button. Default: "Remove"
- in-place: delete in place (ajax)? Default: true, or false if the record to be deleted is the same as the top level
context of the page
- image: URL of an image for the button. Changes the rendered tag from `<input type='button'>` to `<input type='image'
src='...'>`
- fade: Perform the fade effect (true/false)? Default: true
-->
<def tag="delete-button" attrs="label, update, in-place, image, confirm, fade, subsite"><%=
in_place = false if in_place.nil? && this == @this && request.method == :get
url = object_url(this, :method => :delete, :subsite => subsite)
if (Hobo::Dryml.last_if = url && can_delete?)
attributes = attributes.merge(if image
{ :type => "image", :src => "#{base_url}/images/#{image}" }
else
{ :type => "button" }
end)
label ||= ht("hobo.actions.remove", :default=>"Remove")
confirm = ht("hobo.messages.confirm", :default=>"Are you sure?") if confirm.nil?
add_classes!(attributes,
image ? "image-button" : "button",
"delete-button delete-#{this.class.name.underscore.dasherize}-button")
if url
if in_place == false
attributes[:confirm] = confirm if confirm
attributes[:method] = :delete
button_to(label, url, attributes)
else
fade = true if fade.nil?
attributes[:value] = label
attributes[:onclick] = "Hobo.removeButton(this, '#{url}', #{js_updates(update)}, {fade:#{fade}, confirm: #{confirm.inspect}})"
element(:input, attributes, nil, true, true)
end
end
else
""
end
%></def>
<!-- Provides an ajax create button that will send a RESTful "POST" to the server to create a new resource.
All of the standard ajax attributes are supported (see the main taglib documention for Rapid Forms).
### Attributes
- model: The class to instantiate, pass either the class name or the class object.
-->
<def tag="create-button" attrs="model, update, label, fields, message"><%=
raise HoboError.new("no update specified") unless update
fields ||= {}
class_or_assoc = if model
model.is_a?(String) ? model.constantize : model
elsif Hobo.simple_has_many_association?(this)
fields[this_field_reflection.primary_key_name] = this.proxy_owner.id
this
else
raise HoboError.new("invalid context for <create-button>")
end
new = class_or_assoc.new(fields)
new.set_creator(current_user)
if can_create?(new)
label ||= ht("#{new.class.name.pluralize.underscore}.actions.new", :default=>"New #{new.class.name.titleize}")
ajax_attributes = { :message => message }
class_name = new.class.name.underscore
ajax_attributes[:params] = { class_name => fields } unless fields.empty?
func = ajax_updater(object_url(new.class, :method => :post), update, ajax_attributes)
element :input, add_classes(attributes.merge(:type =>'button', :onclick => func, :value => label),
"button create-button create-#{class_name}-button"), nil, true, true
end
%></def>
<!-- A `<select>` menu from which the user can choose the target record for a `belongs_to` association.
This is the default input that Rapid uses for `belongs_to` associations. The menu is constructed using the `to_s` representation of the records.
### Attributes
- `include-none` - whether to include a 'none' option (i.e. set the foreign key to null). If this value is not supplied, the default is "true" if the current value is nil; otherwise the default is "false". One implication of this is that the default may change when the form is re-rendered due to a validation failure. Setting this value explicitly is recommended.
- `blank-message` - the message for the 'none' option. Defaults to "(No `<model-name>`)", e.g. "(No Product)"
- `options` - an array of records to include in the menu. Defaults to the all the records in the target table that match any `:conditions` declared on the `belongs_to` (subject to `limit`)
- `limit` - if `options` is not specified, this limits the number of records. Default: 100
- `text_method` - The method to call on each record to get the text for the option. Multiple methods are supported ie "institution.name"
### See Also
For situations where there are too many target records to practically include in a menu, `<name-one>` provides an autocompleter which would be more suitable.
-->
<def tag="select-one" attrs="include-none, blank-message, options, sort, limit, text-method"><%
raise HoboError.new("Not allowed to edit #{this_field}") if !attributes[:disabled] && !can_edit?
blank_message ||= ht("#{this_type.name.underscore}.message.no", :default=>"(No #{this_type.name.to_s.titleize})")
limit ||= 100
options ||= begin
conditions = ActiveRecord::Associations::BelongsToAssociation.new(this_parent, this_field_reflection).conditions
this_field_reflection.klass.all(:conditions => conditions, :limit => limit).select {|x| can_view?(x)}
end
if text_method.nil?
select_options = options.map { |x| [x.to_s, x.id] }
else
select_options = options.map do |x|
[ text_method.split(".").inject(x) { |v, method| v.send(method) },
x.id ]
end
end
select_options = select_options.sort if sort
select_options.insert(0, [blank_message, ""]) if include_none || (this.nil? && include_none != false)
attributes = add_classes(attributes, "input", "belongs_to", type_and_field)
-%>
<select name="#{param_name_for_this(true)}" merge-attrs="&attributes.except :name">
<%= options_for_select(select_options, this ? this.id : "") %>
</select>
</def>
<!-- An `<input type="text">` with auto-completion. Allows the user to chose the target of a `belongs_to` association by name.
This tag relies on an autocompleter being defined in a controller. A simple example:
<form with="&ProjectMembership.new">
<name-one:user>
</form>
class ProjectMembership < ActiveRecord::Base
hobo_model
belongs_to :user
end
class User < ActiveRecord::Base
hobo_user_model
has_many :project_memberships, :accessible => true, :dependent => :destroy
end
class UsersController < ApplicationController
autocomplete
end
The route used by the autocompleter looks something like `/users/complete_name`. The first part of this route is specified by the `complete-target` attribute, and the second part is specified by the `completer` attribute.
`complete-target` specifies the controller for the route. It can be specified by either supplying a model class or a model. If a model is supplied, the id of the model is passed as a parameter to the controller. (`?id=7`, for example) The default for this attribute is the class of the context. In other words, the class that contains the `has_many / has_one`, not the class with the `belongs_to`.
`completer` specifies the action for the route. `name-one` prepends `complete_` to the value given here. This should be exactly the same as the first parameter to `autocomplete` in your controller. As an example: `autocomplete :email_address` would correspond to `completer="email_address"`. The default for this attribute is the name field for the model being searched, which is usually `name`, but not always.
The query string is passed to the controller in the `query` parameter. (`?query=hello` for example).
For more information on how to customize the controller, see the [controller manual](http://cookbook.hobocentral.net/manual/controllers#autocompleters)
Here's a more complex example. This used to be a part of [agility](http://cookbook.hobocentral.net/tutorials/agility) until it was simplified.
class ProjectsController < ApplicationController
autocomplete :new_member_name do
project = find_instance
hobo_completions :name, User.without_project(project).is_not(project.owner)
end
end
Note that this was added to the projects controller, rather than the users controller as in the first example. You can read this as: create an auto-complete action called new_member_name that finds users that are not already members of the project, and not the owner of the project, and completes the :name field.
<name-one:user complete-target="&@project" completer="new_member_name"/>
We're using an object as the complete-target rather than a class. This allows the `find_instance` in our controller action to function.
There's another example of `<name-one>` use in the [recipes](http://cookbook.hobocentral.net/recipes/36-using-a-name-one-one-a).
-->
<def tag="name-one" attrs="complete-target, completer"><%
complete_target ||= this_field_reflection.klass
completer ||= (complete_target.is_a?(Class) ? complete_target : complete_target.class).name_attribute
-%>
<input type="text" name="#{param_name_for_this}"
class="autocompleter #{type_and_field.dasherize} #{css_data :complete_on, typed_id(complete_target), completer}"
value="&name :no_wrapper => true, :if_present => true"
merge-attrs/>
<div class="completions-popup" style="display:none"></div>
</def>
<!-- nodoc. -->
<def tag="sti-type-input">
<select name="#{param_name_for(form_field_path + ['type'])}">
<%= options_for_select(this.class.send(:subclasses).map{|x| [x.name.titleize, x.name]}, this.class.name) %>
</select>
</def>
<!-- A `<select>` menu input. This tag differes from `<select-menu>` only in that it adds the correct `name` attribute for the current field, and `selected` default to `this`.
### Attributes
- `options` - an array of options suitable to be passed to the Rails `options_for_select` helper.
- `selected` - the value (from the `options` array) that should be initially selected. Defaults to `this`
- `first-option` - a string to be used for an extra option in the first position. E.g. "Please choose..."
- `first-value` - the value to be used with the `first-option`. Typically not used, meaning the option has a blank value.
-->
<def tag="select-input">
<select-menu name="#{param_name_for_this}" selected="&this" merge/>
</def>
<!-- Renders a readable list of error messages following a form submission. Expects the errors to be in `this.errors`. Renders nothing if there are no errors.
-->
<def tag="error-messages">
<section class="error-messages" merge-attrs if="&this.errors.length > 0">
<h2 param="heading">To proceed please correct the following:</h2>
<ul param>
<% this.errors.each do |attr, message|; next if message == "..." -%>
<li param><%= this.class.human_attribute_name(attr) %> <%= message %></li>
<% end -%>
</ul>
</section>
</def>
<!--
An input for `has_many :through` associations that lets the user chose the items from a `<select>` menu.
To use this tag, the model of the items the user is chosing *must* have unique names, and the
-->
<def tag="select-many" attrs="options, targets, remove-label, prompt, disabled, name"><%
prompt ||= "Add #{this_field.titleize.singularize}"
options ||= this_field_reflection.klass.all(:conditions =>this.conditions).select {|x| can_view?(x)}
name ||= param_name_for_this
values = this
remove_label ||= ht("hobo.actions.remove", :default=>'Remove')
-%>
<div class="input select-many" merge-attrs>
<div style="display:none" class="item-proto">
<div class="item" param="proto-item">
<span></span>
<input type="hidden" name="#{name}[]" param="proto-hidden"/>
<input type="button" class="remove-item" value="#{remove_label}" param="proto-remove-button"/>
</div>
</div>
<div class="items">
<div class="item" param="item" repeat>
<span><%= h this.to_s %></span>
<input type="hidden" name="#{name}[]" value="@#{h this.id}" disabled="&disabled"
param="hidden"/>
<input type="button" class="remove-item" value="#{remove_label}" disabled="&disabled"
param="remove-button"/>
</div>
</div>
<select merge-attrs="&{:disabled => disabled}">
<option value=""><prompt/></option>
<repeat with="&options">
<if test="&this.in?(values)">
<optgroup class="disabled-option" label="#{h this.to_s}" alt="@#{this.id}"> </optgroup>
</if>
<else>
<option value="@#{this.id}"><%= h this.to_s %></option>
</else>
</repeat>
</select>
</div>
</def>
<!--
Used inside a form to specify where to redirect after successful submission. This works by inserting a hidden field called `after_submit` which is used by Hobo if present to perform a redirect after the form submission.
### Usage
Use the `stay-here` attribute to remain on the current page:
<form>
<after-submit stay-here/>
...
</form>
Use the `go-back` option to return to the previous page:
<form>
<after-submit go-back/>
...
</form>
Use the `uri` option to specify a redirect location:
<form>
<after-submit uri="/admin"/>
...
</form>
-->
<def tag="after-submit" attrs="uri, stay-here, go-back"><%
uri = "stay-here" if stay_here
uri = session[:previous_uri] if go_back
-%>
<input type="hidden" value="¶ms[:after_submit] || uri" name="after_submit" if="&uri"/>
</def>
<!-- A simple wrapper around the `<select>` tag and `options_for_select` helper
### Attributes
- `options` - an array of options suitable to be passed to the Rails `options_for_select` helper.
- `selected` - the value (from the `options` array) that should be initially selected. Defaults to `this`
- `first-option` - a string to be used for an extra option in the first position. E.g. "Please choose..."
- `first-value` - the value to be used with the `first-option`. Typically not used, meaning the option has a blank value.
-->
<def tag="select-menu" attrs="options, selected, first-option, first-value">
<select merge-attrs param="default">
<% selected=this if selected.nil? %>
<option value="#{first_value}" unless="&first_option.nil?"><first-option/></option>
<do param="options"><% options_for_select(options, selected) %></do>
</select>
</def>
<!-- Renders a `<ul>` list of checkboxes, one for each of the potential targt in a `has_many` association. The user can check the items they wish to have associated. A typical use might be selecting categories for a blog post.
### Attributes
- `options` - an array of models that may be added to the collection
- `disabled` - if true, sets the disabled flag on all check boxes.
-->
<def tag="check-many" attrs="options, disabled"><%
collection = this
param_name = param_name_for_this
options ||= begin
conditions = ActiveRecord::Associations::BelongsToAssociation.new(this_parent, this_field_reflection).conditions
this_field_reflection.klass.all(:conditions => conditions, :limit => 100).select {|x| can_view?(x)}
end
-%>
<ul class="check-many" param="default" merge-attrs>
<input type="hidden" name="#{param_name}[]" value=""/><% # ensure all items are removed when nothing checked %>
<li repeat="&options" param>
<input type="checkbox" name="#{param_name}[]" value="@#{this.id}" checked="&this.in?(collection)" disabled="&disabled"/>
<name param/>
</li>
</ul>
</def>
<!-- Renders an `<input type='hidden'>` for the `id` field of the current context -->
<def tag="hidden-id-field">
<if:id><input type="hidden" name="#{param_name_for_this}" value="#{this}" /></if>
</def>
<!-- Creates a sub-section of the form which the user can repeat using (+) and (-) buttons, in order to allow an entire `has_many` collection to be created/edited in a single form.
This tag is very different from tags like `<select-many>` and `<check-many>` in that:
- Those tags are used to *chose existing records* to include in the association, while `<input-many>` is used to actually create or edit the records in the association.
### Example
Say you are creating a new `Category` in your online shop, and you want to create some initial products *in the same form*, you can add the following to your form:
<input-many:products><field-list fields="name, price"/></input-many>
The body of the tag will be repeated for each of the current records in the collection, or will just appear once (with blank fields) if the colleciton is empty.
### Attributes
- fields: If you do not specify any content for the input-many, a `<field-list>` is rendered. This attribute is passed through to the `<field-list>`
- skip: Passed through to the `<field-list>`. If not specified, it defaults to the parent association.
-->
<def tag="input-many" attrs="fields,skip" polymorphic>
<set empty="&this.empty?"/>
<ul class="input-many #{this_field.dasherize} #{css_data :input_many_prefix, param_name_for_this}">
<li repeat class="#{'record-with-errors' unless this.errors.empty?}">
<error-messages without-heading class="sub-record"/>
<hidden-id-field/>
<div class="input-many-item" param="default">
<field-list merge-attrs="fields" />
</div>
<div class="buttons">
<button type="button" class="remove-item" merge-attrs="disabled" param="remove-item">-</button>
<button type="button" class="add-item" if="&last_item?" merge-attrs="disabled" param="add-item">+</button>
</div>
</li>
<li if="&empty">
<% skip ||= this.proxy_reflection.klass.reflect_on_all_associations.detect {|p| p.primary_key_name==this.proxy_reflection.primary_key_name}.try.name.to_s %>
<fake-field-context fake-field="0" context="&this.try.new_candidate || this.member_class.new">
<div class="input-many-item" param="default">
<field-list merge-attrs="fields" skip="&skip"/>
</div>
</fake-field-context>
<div class="buttons">
<button type="button" class="add-item" merge-attrs="disabled" param="add-item">+</button>
</div>
</li>
</ul>
</def>
<!-- Renders a sub-section of a form with fields for every record in a `has_many` association. This is similar to `<input-many>` except there is no ability to add and remove items (i.e. no (+) and (-) buttons).
-->
<def tag="input-all">
<% association_fkey = this_field_reflection.primary_key_name -%>
<ul class="input-all #{this_field.dasherize}">
<li repeat class="#{'record-with-errors' unless this.errors.empty?}">
<set-scoped form-field-names="&[]">
<hidden-id-field/>
<do param="default"/>
<hidden-fields skip="&association_fkey"/>
</set-scoped>
</li>
</ul>
</def>
<!-- Renders the common "or (Cancel)" for a form. Attributes are merged into the link (`<a>Cancel</a>`), making it easy to customise the destination of the cancel link. By default it will link to `this` or `this.class`.
-->
<def tag="or-cancel">
<if test="&linkable?"><ht key="hobo.support.or">or</ht> <a merge-attrs><ht key="hobo.actions.cancel">Cancel</ht></a></if>
<else>
<if test="&linkable?(this.class)"><ht key="hobo.support.or">or</ht> <a to="&this.class" merge-attrs><ht key="hobo.actions.cancel">Cancel</ht></a></if>
</else>
</def>