action_form 0.3.0 → 0.5.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: 93e16fca08cba7ced7cf05dfe08e026f45d9ed6fd03d1210de7b82959ee13c13
4
- data.tar.gz: 21034909b984567b9ba419474c8afc296b44c86543d881881b69f2600560743d
3
+ metadata.gz: 0211bd706504ef28c1045cc72f33a2408b554ca0a2b3e922436491a1865a16b9
4
+ data.tar.gz: 87c0249ed1b139d6fea7d26fb9e9d2e220cafa3a045b129530e94b9afb8ef545
5
5
  SHA512:
6
- metadata.gz: 16162478587b84ae6cdccbc3d3f62c01c7b1c9fe2657e8f1f1ec3486df05f30d45798c46700c7790d49c68b8cade708d730dfbea2f57726e1d5e472ed87b9d78
7
- data.tar.gz: 064b00adb4bffe1b124eaf2e0e12f14028fb50648e161a98cfe933c9e19207b40f6e59fa69134018d1a540dce11c7c383a7ccc2781738a6b9aebd7097e9b49c4
6
+ metadata.gz: 817ea2221b6780ed40c769fd37416fae7e2051256b7084b4d4cb612c60fac7288a4dad38316a9af0eba543bf712e07afce8185f33e1d3768e7172cfd81de8103
7
+ data.tar.gz: 32a63869d603e1117ac6bacaf4cd526c7127ba23a703343f537507e31e5739ef9bdce5d6701529413f23f0cd9e8794c3d01b5a4d2954070051eecdcd10a7d881
data/README.md CHANGED
@@ -8,6 +8,7 @@ This library allows you to build complex forms in Ruby with a simple DSL. It pro
8
8
 
9
9
  - A clean, declarative syntax for defining form fields and validations
10
10
  - Support for nested forms
11
+ - Custom parameter validation with the `params` method
11
12
  - Automatic form rendering with customizable HTML/CSS
12
13
  - Built-in error handling and validation
13
14
  - Integration with Rails and other Ruby web frameworks
@@ -61,6 +62,7 @@ ActionForm is built around a modular architecture that separates form definition
61
62
 
62
63
  - **Declarative DSL**: Define forms with simple, readable syntax
63
64
  - **Nested Forms**: Support for complex nested structures with `subform` and `many`
65
+ - **Custom Parameter Validation**: Use the `params` method to add custom validation logic and schema modifications
64
66
  - **Dynamic Collections**: JavaScript-powered add/remove functionality for many relationships
65
67
  - **Flexible Rendering**: Each element can be configured with custom input types, labels, and HTML attributes
66
68
  - **Error Integration**: Built-in support for displaying validation errors
@@ -86,6 +88,7 @@ ActionForm follows a bidirectional data flow pattern that handles both form disp
86
88
  #### **Key Benefits:**
87
89
  - **Single Source of Truth**: The same form definition handles both displaying existing data and processing new data
88
90
  - **Automatic Parameter Handling**: [EasyParams](https://github.com/andriy-baran/easy_params) classes are automatically generated to mirror your form structure
91
+ - **Custom Parameter Validation**: Use the `params` method to add custom validation logic and schema modifications
89
92
  - **Error Integration**: Failed validations can re-render the form with submitted data and error messages
90
93
  - **Nested Support**: Both phases support complex nested structures through `subform` and `many` relationships
91
94
 
@@ -250,6 +253,681 @@ class UserForm < ActionForm::Base
250
253
  end
251
254
  ```
252
255
 
256
+ ### Custom Parameter Validation
257
+
258
+ ActionForm provides a `params` method that allows you to add custom validation logic and schema modifications to your form's parameter handling. This is particularly useful for complex validation rules that depend on context or require cross-field validation.
259
+
260
+ #### **Basic Usage**
261
+
262
+ Use the `params` method to define custom validation logic:
263
+
264
+ ```ruby
265
+ class UserForm < ActionForm::Base
266
+ element :email do
267
+ input type: :email
268
+ output type: :string, presence: true
269
+ end
270
+
271
+ element :password do
272
+ input type: :password
273
+ output type: :string
274
+ end
275
+
276
+ element :password_confirmation do
277
+ input type: :password
278
+ output type: :string
279
+ end
280
+
281
+ # Custom parameter validation
282
+ params do
283
+ validates :password, presence: true
284
+ validates :password_confirmation, presence: true
285
+ validates :password, confirmation: true
286
+ end
287
+ end
288
+ ```
289
+
290
+ #### **Conditional Validation**
291
+
292
+ You can add conditional validation logic based on context:
293
+
294
+ ```ruby
295
+ class UserForm < ActionForm::Base
296
+ element :email do
297
+ input type: :email
298
+ output type: :string, presence: true
299
+ end
300
+
301
+ element :password do
302
+ input type: :password
303
+ output type: :string
304
+ end
305
+
306
+ # Conditional validation based on external context
307
+ params do
308
+ secure_mode = true # This could come from configuration or request context
309
+
310
+ validates :password, presence: true, if: -> { secure_mode }
311
+ validates :password, length: { minimum: 8 }, if: -> { secure_mode }
312
+ end
313
+ end
314
+ ```
315
+
316
+ #### **Nested Form Validation**
317
+
318
+ The `params` method also supports validation for nested forms using schema blocks:
319
+
320
+ ```ruby
321
+ class UserForm < ActionForm::Base
322
+ element :email do
323
+ input type: :email
324
+ output type: :string, presence: true
325
+ end
326
+
327
+ subform :profile, default: {} do
328
+ element :name do
329
+ input type: :text
330
+ output type: :string
331
+ end
332
+ end
333
+
334
+ many :addresses, default: [{}] do
335
+ subform do
336
+ element :street do
337
+ input type: :text
338
+ output type: :string
339
+ end
340
+
341
+ element :city do
342
+ input type: :text
343
+ output type: :string
344
+ end
345
+ end
346
+ end
347
+
348
+ params do
349
+ # Validate nested subform
350
+ profile_attributes_schema do
351
+ validates :name, presence: true
352
+ end
353
+
354
+ # Validate nested collection
355
+ addresses_attributes_schema do
356
+ validates :street, presence: true
357
+ validates :city, presence: true
358
+ end
359
+ end
360
+ end
361
+ ```
362
+
363
+ #### **Dynamic Form Classes**
364
+
365
+ You can create dynamic form classes with different validation rules:
366
+
367
+ ```ruby
368
+ class BaseUserForm < ActionForm::Base
369
+ element :email do
370
+ input type: :email
371
+ output type: :string, presence: true
372
+ end
373
+
374
+ element :password do
375
+ input type: :password
376
+ output type: :string
377
+ end
378
+ end
379
+
380
+ # Create a secure version with additional validation
381
+ SecureUserForm = Class.new(BaseUserForm)
382
+ SecureUserForm.params do
383
+ validates :password, presence: true, length: { minimum: 8 }
384
+ validates :password, format: { with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/ }
385
+ end
386
+
387
+ # Create a basic version with minimal validation
388
+ BasicUserForm = Class.new(BaseUserForm)
389
+ BasicUserForm.params do
390
+ validates :password, presence: true
391
+ end
392
+ ```
393
+
394
+ #### **Integration with Controllers**
395
+
396
+ The custom parameter validation integrates seamlessly with your controllers:
397
+
398
+ ```ruby
399
+ class UsersController < ApplicationController
400
+ def create
401
+ user_params = UserForm.params_definition.new(params)
402
+
403
+ if user_params.valid?
404
+ @user = User.create!(user_params.to_h)
405
+ redirect_to @user
406
+ else
407
+ @form = @form.with_params(user_params)
408
+ render :new
409
+ end
410
+ end
411
+ end
412
+ ```
413
+
414
+ The `params` method provides a powerful way to extend ActionForm's parameter handling with custom validation logic while maintaining the declarative nature of form definition.
415
+
416
+ ### Modifying Element Definitions
417
+
418
+ ActionForm allows you to modify existing element, subform, and `many` definitions after they have been declared. This feature enables you to extend or customize form definitions without altering the original class structure, making it perfect for conditional modifications, inheritance patterns, and dynamic form customization.
419
+
420
+ #### **Modifying Elements**
421
+
422
+ Use the `{element_name}_element` method to modify an existing element definition:
423
+
424
+ ```ruby
425
+ class UserForm < ActionForm::Base
426
+ element :email do
427
+ input type: :email
428
+ output type: :string
429
+ end
430
+
431
+ element :password do
432
+ input type: :password
433
+ output type: :string
434
+ end
435
+ end
436
+
437
+ # Modify existing element definitions
438
+ UserForm.email_element do
439
+ output type: :string, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
440
+ end
441
+
442
+ UserForm.password_element do
443
+ output type: :string, presence: true, length: { minimum: 8 }
444
+ end
445
+ ```
446
+
447
+ #### **Modifying Subforms**
448
+
449
+ Use the `{subform_name}_subform` method to modify an existing subform definition:
450
+
451
+ ```ruby
452
+ class UserForm < ActionForm::Base
453
+ subform :profile do
454
+ element :bio do
455
+ input type: :textarea
456
+ output type: :string
457
+ end
458
+ end
459
+ end
460
+
461
+ # Modify the subform to add validation or change defaults
462
+ UserForm.profile_subform default: {} do
463
+ bio_element do
464
+ output type: :string, presence: true
465
+ end
466
+
467
+ # You can also add new elements
468
+ element :avatar do
469
+ input type: :file
470
+ output type: :string
471
+ end
472
+ end
473
+ ```
474
+
475
+ #### **Modifying Many Relationships**
476
+
477
+ Use the `{many_name}_subforms` method to modify an existing `many` definition:
478
+
479
+ ```ruby
480
+ class OrderForm < ActionForm::Base
481
+ many :items do
482
+ subform do
483
+ element :name do
484
+ input type: :text
485
+ output type: :string
486
+ end
487
+
488
+ element :quantity do
489
+ input type: :number
490
+ output type: :integer
491
+ end
492
+ end
493
+ end
494
+ end
495
+
496
+ # Modify the many relationship to add validation or change defaults
497
+ OrderForm.items_subforms default: [{}] do
498
+ subform do
499
+ name_element do
500
+ output type: :string, presence: true
501
+ end
502
+
503
+ quantity_element do
504
+ output type: :integer, presence: true, inclusion: { in: 1..100 }
505
+ end
506
+
507
+ # Add new elements to existing many subforms
508
+ element :price do
509
+ input type: :number
510
+ output type: :float, presence: true
511
+ end
512
+ end
513
+ end
514
+ ```
515
+
516
+ #### **Inheritance and Modifications**
517
+
518
+ Element modifications work seamlessly with class inheritance:
519
+
520
+ ```ruby
521
+ class BaseForm < ActionForm::Base
522
+ element :name do
523
+ input type: :text
524
+ output type: :string
525
+ end
526
+ end
527
+
528
+ class UserForm < BaseForm
529
+ element :email do
530
+ input type: :email
531
+ output type: :string
532
+ end
533
+ end
534
+
535
+ # Modify inherited elements
536
+ UserForm.name_element do
537
+ output type: :string, presence: true
538
+ end
539
+
540
+ # Modify elements defined in subclass
541
+ UserForm.email_element do
542
+ output type: :string, presence: true
543
+ end
544
+ ```
545
+
546
+ Here's a complete example showing element modifications in action:
547
+
548
+ ```ruby
549
+ class OrderForm < ActionForm::Base
550
+ element :name do
551
+ input(type: :text)
552
+ output(type: :string)
553
+ end
554
+
555
+ subform :customer do
556
+ element :name do
557
+ input(type: :text)
558
+ output(type: :string)
559
+ end
560
+ end
561
+
562
+ many :items do
563
+ subform do
564
+ element :name do
565
+ input(type: :text)
566
+ output(type: :string)
567
+ end
568
+
569
+ element :quantity do
570
+ input(type: :number)
571
+ output(type: :integer)
572
+ end
573
+
574
+ element :price do
575
+ input(type: :number)
576
+ output(type: :float)
577
+ end
578
+ end
579
+ end
580
+ end
581
+
582
+ # Apply modifications to add validation
583
+ secure = true
584
+
585
+ OrderForm.name_element do
586
+ output(type: :string, presence: true, if: -> { secure })
587
+ end
588
+
589
+ OrderForm.customer_subform default: {} do
590
+ name_element do
591
+ output(type: :string, presence: true, if: -> { secure })
592
+ end
593
+ end
594
+
595
+ OrderForm.items_subforms default: [{}] do
596
+ subform do
597
+ name_element do
598
+ output(type: :string, presence: true, if: -> { secure })
599
+ end
600
+ quantity_element do
601
+ output(type: :integer, presence: true, if: -> { secure })
602
+ end
603
+ price_element do
604
+ output(type: :float, presence: true, if: -> { secure })
605
+ end
606
+ end
607
+ end
608
+
609
+ # Now the form has validation enabled
610
+ params = OrderForm.params_definition.new({})
611
+ expect(params).to be_invalid
612
+ ```
613
+
614
+ #### **Key Benefits**
615
+
616
+ - **Non-Destructive**: Modify form definitions without changing the original class
617
+ - **Conditional Logic**: Apply modifications based on runtime conditions
618
+ - **Inheritance Support**: Works seamlessly with class inheritance
619
+ - **Flexible Extension**: Add validation, change defaults, or add new elements to existing definitions
620
+ - **Reusability**: Create base forms and customize them for specific use cases
621
+
622
+ This feature provides a powerful way to customize and extend form definitions while maintaining the declarative nature of ActionForm.
623
+
624
+ ### Composition and Owner Delegation
625
+
626
+ ActionForm includes a composition system that allows form components (elements, subforms, and `many` collections) to access methods from their owner (typically the parent form or a custom host object). This promotes code reuse and reduces redundancy by allowing shared logic to be defined in a central location and accessed by multiple form components.
627
+
628
+ #### **How Composition Works**
629
+
630
+ When you create a form, all nested elements and subforms automatically have access to their owner through the `owner` accessor. Methods called on elements or subforms that are not defined locally are automatically delegated up the ownership chain, allowing you to access methods from the parent form or a custom host object.
631
+
632
+ #### **Automatic Ownership Chain**
633
+
634
+ Ownership is automatically established when forms are built:
635
+
636
+ ```ruby
637
+ class ProductForm < ActionForm::Base
638
+ element :name do
639
+ input(type: :text)
640
+ output(type: :string)
641
+
642
+ # This method can call methods on the owner (ProductForm)
643
+ def render?
644
+ name_render? # Delegates to owner.name_render?
645
+ end
646
+ end
647
+
648
+ many :variants, default: [{}] do
649
+ subform do
650
+ element :name do
651
+ input(type: :text)
652
+ output(type: :string)
653
+
654
+ def render?
655
+ variants_name_render? # Delegates to owner.variants_name_render?
656
+ end
657
+ end
658
+
659
+ def render?
660
+ variants_subform_render? # Delegates to owner.variants_subform_render?
661
+ end
662
+ end
663
+ end
664
+
665
+ subform :manufacturer, default: {} do
666
+ element :name do
667
+ input(type: :text)
668
+ output(type: :string)
669
+
670
+ def render?
671
+ manufacturer_name_render? # Delegates to owner.manufacturer_name_render?
672
+ end
673
+ end
674
+ end
675
+
676
+ # Methods accessible by nested components
677
+ def name_render?
678
+ true
679
+ end
680
+
681
+ def variants_name_render?
682
+ true
683
+ end
684
+
685
+ def variants_subform_render?
686
+ true
687
+ end
688
+
689
+ def manufacturer_name_render?
690
+ true
691
+ end
692
+ end
693
+ ```
694
+
695
+ #### **Custom Host Object**
696
+
697
+ You can pass a custom host object when initializing a form, allowing you to separate form logic from business logic:
698
+
699
+ ```ruby
700
+ class HostObject
701
+ def name_render?
702
+ current_user.admin? || form_context == :edit
703
+ end
704
+
705
+ def variants_subform_render?
706
+ feature_enabled?(:variants)
707
+ end
708
+
709
+ def variants_name_render?
710
+ true
711
+ end
712
+
713
+ def variants_price_render?
714
+ pricing_enabled?
715
+ end
716
+
717
+ def manufacturer_name_render?
718
+ manufacturer_feature_enabled?
719
+ end
720
+ end
721
+
722
+ class ProductForm < ActionForm::Base
723
+ element :name do
724
+ input(type: :text)
725
+ output(type: :string)
726
+
727
+ def render?
728
+ name_render? # Calls HostObject#name_render?
729
+ end
730
+ end
731
+
732
+ many :variants, default: [{}] do
733
+ subform do
734
+ element :name do
735
+ input(type: :text)
736
+ output(type: :string)
737
+
738
+ def render?
739
+ variants_name_render? # Calls HostObject#variants_name_render?
740
+ end
741
+ end
742
+
743
+ element :price do
744
+ input(type: :number)
745
+ output(type: :float)
746
+
747
+ def render?
748
+ variants_price_render? # Calls HostObject#variants_price_render?
749
+ end
750
+ end
751
+
752
+ def render?
753
+ variants_subform_render? # Calls HostObject#variants_subform_render?
754
+ end
755
+ end
756
+ end
757
+
758
+ subform :manufacturer, default: {} do
759
+ element :name do
760
+ input(type: :text)
761
+ output(type: :string)
762
+
763
+ def render?
764
+ manufacturer_name_render? # Calls HostObject#manufacturer_name_render?
765
+ end
766
+ end
767
+ end
768
+ end
769
+
770
+ # Use the form with a custom host object
771
+ host = HostObject.new
772
+ product = Product.new(name: "Product 1")
773
+ form = ProductForm.new(object: product, owner: host)
774
+ ```
775
+
776
+ #### **Ownership Chain Traversal**
777
+
778
+ The composition system supports multi-level ownership chains. When a method is called on an element or subform, it searches up the ownership chain until it finds the method:
779
+
780
+ ```ruby
781
+ class GrandparentForm < ActionForm::Base
782
+ def shared_helper
783
+ "grandparent"
784
+ end
785
+ end
786
+
787
+ class ParentForm < ActionForm::Base
788
+ def shared_helper
789
+ "parent"
790
+ end
791
+ end
792
+
793
+ class ChildForm < ActionForm::Base
794
+ element :field do
795
+ input(type: :text)
796
+
797
+ def render?
798
+ shared_helper # Will find ParentForm#shared_helper first
799
+ end
800
+ end
801
+ end
802
+
803
+ # If ChildForm has ParentForm as owner, and ParentForm has GrandparentForm as owner:
804
+ # The method lookup order is: ChildForm -> ParentForm -> GrandparentForm
805
+ ```
806
+
807
+ #### **Accessing Owner Directly**
808
+
809
+ You can access the owner directly using the `owner` accessor:
810
+
811
+ ```ruby
812
+ class ProductForm < ActionForm::Base
813
+ element :discount_code do
814
+ input(type: :text)
815
+ output(type: :string)
816
+
817
+ def disabled?
818
+ # Access owner's methods directly
819
+ owner.current_user && !owner.current_user.admin?
820
+ end
821
+
822
+ def placeholder
823
+ owner.discount_placeholder_text
824
+ end
825
+ end
826
+
827
+ def current_user
828
+ @current_user ||= User.find(session[:user_id])
829
+ end
830
+
831
+ def discount_placeholder_text
832
+ "Enter discount code"
833
+ end
834
+ end
835
+ ```
836
+
837
+ #### **Ownership Hierarchy**
838
+
839
+ The ownership hierarchy is automatically established as follows:
840
+
841
+ - **Top-level form**: Can have a custom `owner` passed during initialization
842
+ - **Elements**: Owner is the form or subform that contains them
843
+ - **Subforms**: Owner is the form that contains them
844
+ - **SubformsCollection (many)**: Owner is the form that contains them
845
+ - **Nested elements in subforms**: Owner is the subform that contains them
846
+ - **Nested subforms**: Owner is the parent form or subform
847
+
848
+ #### **Practical Use Cases**
849
+
850
+ **Conditional Rendering Based on Context:**
851
+ ```ruby
852
+ class OrderForm < ActionForm::Base
853
+ element :admin_notes do
854
+ input(type: :textarea)
855
+ output(type: :string)
856
+
857
+ def render?
858
+ owner.current_user&.admin?
859
+ end
860
+ end
861
+
862
+ def current_user
863
+ @current_user
864
+ end
865
+ end
866
+ ```
867
+
868
+ **Shared Validation Logic:**
869
+ ```ruby
870
+ class RegistrationForm < ActionForm::Base
871
+ element :email do
872
+ input(type: :email)
873
+ output(type: :string)
874
+
875
+ def disabled?
876
+ owner.email_locked?
877
+ end
878
+ end
879
+
880
+ element :email_confirmation do
881
+ input(type: :email)
882
+ output(type: :string)
883
+
884
+ def disabled?
885
+ owner.email_locked?
886
+ end
887
+ end
888
+
889
+ def email_locked?
890
+ @user.persisted? && @user.email_verified?
891
+ end
892
+ end
893
+ ```
894
+
895
+ **Feature Flags:**
896
+ ```ruby
897
+ class SettingsForm < ActionForm::Base
898
+ many :advanced_settings do
899
+ subform do
900
+ element :feature_flag do
901
+ input(type: :checkbox)
902
+ output(type: :bool)
903
+
904
+ def render?
905
+ owner.feature_enabled?(:advanced_settings)
906
+ end
907
+ end
908
+ end
909
+
910
+ def render?
911
+ owner.feature_enabled?(:advanced_settings)
912
+ end
913
+ end
914
+
915
+ def feature_enabled?(feature)
916
+ FeatureFlags.enabled?(feature, current_user)
917
+ end
918
+ end
919
+ ```
920
+
921
+ #### **Benefits**
922
+
923
+ - **Code Reuse**: Share common logic across multiple form components
924
+ - **Separation of Concerns**: Keep business logic in host objects, form logic in forms
925
+ - **Flexibility**: Support conditional rendering and behavior based on context
926
+ - **Maintainability**: Centralize shared logic instead of duplicating it
927
+ - **Testability**: Test host objects separately from form definitions
928
+
929
+ The composition system provides a powerful mechanism for creating flexible, maintainable forms that can adapt to different contexts and requirements.
930
+
253
931
  ### Tagging system
254
932
 
255
933
  ActionForm includes a flexible tagging system that allows you to add custom metadata to form elements and control rendering behavior. Tags serve multiple purposes:
@@ -800,6 +1478,22 @@ class UserForm < ActionForm::Rails::Base
800
1478
  input type: :email
801
1479
  output type: :string, presence: true
802
1480
  end
1481
+
1482
+ element :password do
1483
+ input type: :password
1484
+ output type: :string
1485
+ end
1486
+
1487
+ element :password_confirmation do
1488
+ input type: :password
1489
+ output type: :string
1490
+ end
1491
+
1492
+ # Custom parameter validation for Rails integration
1493
+ params do
1494
+ validates :password, presence: true, length: { minimum: 6 }
1495
+ validates :password, confirmation: true
1496
+ end
803
1497
  end
804
1498
  ```
805
1499
 
@@ -910,6 +1604,7 @@ class UsersController < ApplicationController
910
1604
  @user = User.create!(user_params.user.to_h)
911
1605
  redirect_to @user
912
1606
  else
1607
+ # Custom validation errors are automatically available
913
1608
  @form = @form.with_params(user_params)
914
1609
  render :new
915
1610
  end
@@ -927,6 +1622,7 @@ class UsersController < ApplicationController
927
1622
  @user.update!(user_params.user.to_h)
928
1623
  redirect_to @user
929
1624
  else
1625
+ # Custom validation errors (like password confirmation) are displayed
930
1626
  @form = @form.with_params(user_params)
931
1627
  render :edit
932
1628
  end
@@ -7,6 +7,7 @@ module ActionForm
7
7
  include ActionForm::SchemaDSL
8
8
  include ActionForm::ElementsDSL
9
9
  include ActionForm::Rendering
10
+ include ActionForm::Composition
10
11
 
11
12
  attr_reader :elements_instances, :scope, :object, :html_options, :errors
12
13
 
@@ -30,13 +31,14 @@ module ActionForm
30
31
  end
31
32
  end
32
33
 
33
- def initialize(object: nil, scope: self.class.scope, params: nil, **html_options)
34
+ def initialize(object: nil, scope: self.class.scope, params: nil, owner: nil, **html_options)
34
35
  super()
35
36
  @object = object
36
37
  @scope ||= scope
37
38
  @params = @scope && params.respond_to?(@scope) ? params.public_send(@scope) : params
38
39
  @html_options = html_options
39
40
  @elements_instances = []
41
+ @owner = owner
40
42
  build_from_object
41
43
  end
42
44
 
@@ -48,7 +50,7 @@ module ActionForm
48
50
  elsif element_definition < ActionForm::Subform
49
51
  @elements_instances << build_subform(name, element_definition)
50
52
  elsif element_definition < ActionForm::Element
51
- @elements_instances << element_definition.new(name, @params || @object, parent_name: @scope)
53
+ @elements_instances << element_definition.new(name, @params || @object, parent_name: @scope, owner: self)
52
54
  end
53
55
  end
54
56
  end
@@ -73,6 +75,7 @@ module ActionForm
73
75
 
74
76
  def build_many_subforms(name, collection_definition)
75
77
  collection = collection_definition.new(name)
78
+ collection.owner = self
76
79
  Array(subform_value(name)).each.with_index do |item, index|
77
80
  collection << build_subform(name, collection_definition.subform_definition, value: item, index: index)
78
81
  end
@@ -93,7 +96,8 @@ module ActionForm
93
96
 
94
97
  def build_subform(name, form_definition, value: subform_value(name), index: nil)
95
98
  html_name = subform_html_name(name, index: index)
96
- form_definition.new(name: name, scope: html_name, model: value, index: index).tap do |subform|
99
+ form_definition.new(name: name, scope: html_name, model: value, index: index,
100
+ owner: self).tap do |subform|
97
101
  subform.helpers = helpers
98
102
  end
99
103
  end
@@ -103,7 +107,8 @@ module ActionForm
103
107
  elements_keys = form_definition.elements.keys.push(:persisted?)
104
108
  values = form_definition.elements.values.map(&:default)
105
109
  value = Struct.new(*elements_keys).new(*values)
106
- form_definition.new(name: name, scope: html_name, model: value, template: true).tap do |subform|
110
+ form_definition.new(name: name, scope: html_name, model: value, template: true,
111
+ owner: self).tap do |subform|
107
112
  subform.helpers = helpers
108
113
  end
109
114
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionForm
4
+ # Provides host–guest association helpers for ActionForm components.
5
+ # When included, it exposes `host_object` and `host_association_name` accessors
6
+ # and delegates `host_*` method calls to the associated host via `method_missing`.
7
+ module Composition
8
+ def self.included(base)
9
+ base.attr_accessor :owner
10
+ base.include(InstanceMethods)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods # rubocop:disable Style/Documentation
15
+ def part_of(owner_name)
16
+ @owner_name = owner_name
17
+ alias_method owner_name, :owner
18
+ end
19
+ end
20
+
21
+ module InstanceMethods # rubocop:disable Style/Documentation
22
+ def method_missing(name, *attrs, **kwargs, &block)
23
+ if (handler = owners_chain.lazy.detect { |o| o.public_methods.include?(name) })
24
+ handler.public_send(name, *attrs, **kwargs, &block)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def respond_to_missing?(method_name, _include_private = false)
31
+ public_methods.detect { |m| m == :owner } || super
32
+ end
33
+
34
+ def owners_chain
35
+ obj = self
36
+ Enumerator.new do |y|
37
+ y << obj = obj.owner while obj.public_methods.include?(:owner)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,10 +4,12 @@ module ActionForm
4
4
  # Represents a form element with input/output configuration and HTML attributes
5
5
  # rubocop:disable Metrics/ClassLength
6
6
  class Element
7
+ include ActionForm::Composition
8
+
7
9
  attr_reader :name, :input_options, :output_options, :html_name, :html_id, :select_options, :tags, :errors_messages
8
10
  attr_accessor :helpers
9
11
 
10
- def initialize(name, object, parent_name: nil)
12
+ def initialize(name, object, parent_name: nil, owner: nil)
11
13
  @name = name
12
14
  @object = object
13
15
  @html_name = build_html_name(name, parent_name)
@@ -15,6 +17,7 @@ module ActionForm
15
17
  @tags = self.class.tags_list.dup
16
18
  @errors_messages = extract_errors_messages(object, name)
17
19
  tags.merge!(errors: errors_messages.any?)
20
+ @owner = owner
18
21
  end
19
22
 
20
23
  class << self
@@ -26,6 +26,9 @@ module ActionForm
26
26
  def element(name, &block)
27
27
  elements[name] = Class.new(ActionForm::Element)
28
28
  elements[name].class_eval(&block)
29
+ define_singleton_method(:"#{name}_element") do |klass = nil, &block|
30
+ update_element_definition(name, klass, &block)
31
+ end
29
32
  end
30
33
 
31
34
  def many(name, default: nil, &block)
@@ -34,12 +37,26 @@ module ActionForm
34
37
  subform_definition.class_eval(&block) if block
35
38
  elements[name] = subform_definition
36
39
  elements[name].default = default if default
40
+ define_singleton_method(:"#{name}_subforms") do |klass = nil, default: nil, &block|
41
+ update_element_definition(name, klass, default: default, &block)
42
+ end
37
43
  end
38
44
 
39
45
  def subform(name, default: nil, &block)
40
46
  elements[name] = Class.new(subform_class)
41
47
  elements[name].class_eval(&block)
42
48
  elements[name].default = default if default
49
+ define_singleton_method(:"#{name}_subform") do |klass = nil, default: nil, &block|
50
+ update_element_definition(name, klass, default: default, &block)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def update_element_definition(name, klass = nil, default: nil, &block)
57
+ elements[name] = klass if klass
58
+ elements[name] = Class.new(elements[name], &block) if block
59
+ elements[name].default = default if default
43
60
  end
44
61
  end
45
62
  end
@@ -8,15 +8,23 @@ module ActionForm
8
8
  include ActionForm::Rendering
9
9
  include ActionForm::SchemaDSL
10
10
  include ActionForm::ElementsDSL
11
+ include ActionForm::Composition
11
12
 
12
13
  class << self
13
14
  attr_accessor :default
15
+ attr_writer :elements
16
+
17
+ def inherited(subclass)
18
+ super
19
+ subclass.elements = elements.dup
20
+ subclass.default = default.dup
21
+ end
14
22
  end
15
23
 
16
24
  attr_reader :elements_instances, :tags, :name, :object
17
25
  attr_accessor :helpers
18
26
 
19
- def initialize(name:, scope: nil, model: nil, params: nil, **tags)
27
+ def initialize(name:, scope: nil, model: nil, params: nil, owner: nil, **tags) # rubocop:disable Metrics/ParameterLists
20
28
  super()
21
29
  @name = name
22
30
  @scope = scope
@@ -24,6 +32,7 @@ module ActionForm
24
32
  @params = params
25
33
  @elements_instances = []
26
34
  @tags = tags
35
+ @owner = owner
27
36
  build_from_object
28
37
  end
29
38
 
@@ -31,6 +40,7 @@ module ActionForm
31
40
  self.class.elements.each do |element_name, element_definition|
32
41
  @elements_instances << element_definition.new(element_name, @params || @object, parent_name: @scope)
33
42
  @elements_instances.last.tags.merge!(subform: @name)
43
+ @elements_instances.last.owner = self
34
44
  end
35
45
  end
36
46
 
@@ -5,6 +5,7 @@ module ActionForm
5
5
  class SubformsCollection < ::Phlex::HTML
6
6
  extend Forwardable
7
7
  include ActionForm::Rendering
8
+ include ActionForm::Composition
8
9
 
9
10
  def_delegators :@subforms, :last, :first, :length, :size, :[], :<<
10
11
 
@@ -12,13 +13,19 @@ module ActionForm
12
13
  attr_accessor :helpers
13
14
 
14
15
  class << self
15
- attr_reader :subform_definition
16
- attr_accessor :default, :host_class
16
+ attr_accessor :default, :host_class, :subform_definition
17
17
 
18
18
  def subform(subform_class = nil, &block)
19
- @subform_definition = subform_class || Class.new(host_class.subform_class)
19
+ @subform_definition ||= subform_class || Class.new(host_class.subform_class)
20
20
  @subform_definition.class_eval(&block) if block
21
21
  end
22
+
23
+ def inherited(subclass)
24
+ super
25
+ subclass.subform_definition = subform_definition
26
+ subclass.default = default
27
+ subclass.host_class = host_class
28
+ end
22
29
  end
23
30
 
24
31
  def initialize(name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionForm
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/action_form.rb CHANGED
@@ -4,6 +4,7 @@ require "phlex"
4
4
  require "easy_params"
5
5
  require "forwardable"
6
6
  require_relative "action_form/version"
7
+ require_relative "action_form/composition"
7
8
  require_relative "action_form/schema_dsl"
8
9
  require_relative "action_form/elements_dsl"
9
10
  require_relative "action_form/input"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_form
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrii Baran
@@ -67,6 +67,7 @@ files:
67
67
  - Rakefile
68
68
  - lib/action_form.rb
69
69
  - lib/action_form/base.rb
70
+ - lib/action_form/composition.rb
70
71
  - lib/action_form/element.rb
71
72
  - lib/action_form/elements_dsl.rb
72
73
  - lib/action_form/input.rb