# Dynamic Forms and Dependent Fields Bullet Train introduces two new concepts to make your Hotwire-powered forms update dynamically on field changes. 1. Dependent Fields Pattern 2. Dependent Fields Frame ## Dependent Fields Pattern Let's say we have a `super_select` for a "Where have you heard from us?" field. And we'll have a `text_field` for "Other", `disabled` by default. ```erb <%= render 'shared/fields/super_select', method: :heard_from, options: {include_blank: true}, other_options: {search: true} %> <%= render 'shared/fields/text_field', method: :heard_from_other, options: {disabled: true} %> ``` Our goal: if `other` is selected, enable the "Other" field. We'll wire the `super_select` field with the `dependable` Stimulus controller. We'll also tie both fields using the `dependable-dependents-selector-value`. In this case, the `id` of the the `heard_from_other` field. ```erb <%= render 'shared/fields/super_select', method: :heard_from, options: {include_blank: true}, other_options: {search: true}, wrapper_options: { data: { 'controller': "dependable", 'action': '$change->dependable#updateDependents', 'dependable-dependents-selector-value': "##{form.field_id(:heard_from_other)}" } } %> <%= render 'shared/fields/text_field', method: :heard_from_other, id: form.field_id(:heard_from_other), options: {disabled: true} %> ``` On `$change` ([See `super_select` dispatched events](/docs/field-partials/super-select#events)), a custom `dependable:updated` event will be dispatched to all elements matching the `dependable-dependents-selector-value`. This gives us flexibility: disparate form fields don't need to be wrapped with a common Stimulus controlled-wrapper. This approach is favored over Stimulus `outlets` because here we're not coupling the functionality of the `dependable` and `dependent` fields. We're just dispatching Custom Events and using CSS selectors, preferably good old `form.field_id`'s. To let our `:heard_from_other` field handle the `dependable:updated` event, we'll assume we have created a custom `field-availability` Stimulus controller, with a `#toggle` method, looking for the `expected` value on the incoming event `target` element, in this case the `dependable` field. ```erb <%= render 'shared/fields/text_field', method: :heard_from_other, id: form.field_id(:heard_from_other), options: {disabled: true}, data: { controller: "field-availability", action: "dependable:updated->field-availability#toggle", field_availability_expected_value: "other" } %> ``` Note: `field-availability` here is not implemented in Bullet Train. It serves as an example. Next, we'll find a way to only serve the `:heard_from_other` field to the user if "other" is selected, this time by using server-side conditionals in a `turbo_frame`. ## Dependent Fields Frame What if you'd instead want to: * Not rely on a custom Stimulus controller to control the `disabled` state of the "Other" field * Show/hide multiple dependent fields based on the value of the `dependable` field. * Update more than the field itself, but also the value of its `label`. As an example, the [`address_field`](/docs/field-partials/address-field.md) partial shows an empty "State / Province / Region" sub-field by default, and on changing the `:country_id` field to the United States, changes the whole `:region_id` to "State or Territory" as its label and with all US States and territories as its choices. For these situations, Bullet Train has a `dependent_fields_frame` partial that's made to listen to `dependable:updated` events by default. ```erb # update the super-select `dependable-dependents-selector-value` to "##{form.field_id(:heard_from, :dependent_fields)}" to match <%= render "shared/fields/dependent_fields_frame", id: form.field_id(:heard_from, :dependent_fields), form: form, dependable_fields: [:heard_from] do %> <% if form.object&.heard_from == "other" %> <%# no need for a custom `id` or the `disabled` attribute %> <%= render 'shared/fields/text_field', method: :heard_from_other %> <% end %> <%# include additional fields if "other" is selected %> <% end %> ``` This `dependent_fields_frame` serves two purposes: 1. Handle the `dependable:updated` event, so that the frame can... 2. Re-fetch the current form URL (it could be for a `#new` or a `#edit`, it works in both situations) with a GET request (not a submit) that contains the `heard_from` value as a `query_string` param. It then ensures that our `form.object.heard_from` value gets populated with the value found in the `query_string` param automatically, with **no changes needed to the resource controller**. That's all handled by the `dependent_fields_frame` partial by reading its `dependable_fields` param. With this functionality, the contents of the underlying `turbo_frame` will be populated with the updated fields. --- Now let's say we want to come back to the `disabled` use case above, while using the `dependent_fields_frame` approach. We'll move the conditional on the `disabled` property. And we'll also let the `dependent_fields_frame` underlying controller handle disabling the field automatically when the `turbo_frame` awaits updates. ```erb <%= render "shared/fields/dependent_fields_frame", id: form.field_id(:heard_from, :dependent_fields), form: form, dependable_fields: [:heard_from] do |dependent_fields_controller_name| %> <%= render 'shared/fields/text_field', method: :heard_from_other, options: {disabled: form.object&.heard_from != "other"}, data: {"#{dependent_fields_controller_name}-target": "field"} %> <% end %> ``` To learn more about its inner functionality, search the `bullet-train-core` repo for `dependable_controller.js`, `dependent_fields_frame_controller.js` and `_dependent_fields_frame.html.erb`. You can also see an implementation by looking at the `_address_field.html.erb` partial.