# ============================================================================= # AshFormBuilder - Complete Integration Guide # ============================================================================= # # This comprehensive guide demonstrates how to integrate AshFormBuilder into # your Phoenix + Ash application with the "Invisible UI" philosophy: # # "The developer defines the business logic in Ash, and the UI is a side effect." # # ## Table of Contents # # 1. Installation & Setup # 2. Resource Definition with Form DSL # 3. Domain Configuration (Code Interfaces) # 4. Phoenix LiveView Integration # 5. Creatable Combobox (Create on-the-fly) # 6. Theme Customization & Adapters # 7. Search Handling for Combobox # 8. Areas of Enhancement # # ============================================================================= # ============================================================================= # 1. INSTALLATION & SETUP # ============================================================================= # ───────────────────────────────────────────────────────────────────────────── # 1.1 Add Dependencies (mix.exs) # ───────────────────────────────────────────────────────────────────────────── # defp deps do # [ # {:ash, "~> 3.0"}, # {:ash_phoenix, "~> 2.0"}, # {:phoenix_live_view, "~> 1.0"}, # {:phoenix, "~> 1.7"}, # {:ash_form_builder, path: "../ash_form_builder"}, # Or from hex when published # # # Optional: UI Component Libraries # {:mishka_chelekom, "~> 0.0.8"}, # For MishkaTheme # {:daisy_ui, "~> 4.0"} # For Tailwind components (optional) # ] # end # ───────────────────────────────────────────────────────────────────────────── # 1.2 Configure Theme (config/config.exs) # ───────────────────────────────────────────────────────────────────────────── # config :ash_form_builder, :theme, AshFormBuilder.Theme.MishkaTheme # # or # config :ash_form_builder, :theme, AshFormBuilder.Themes.Default # # or your custom theme # config :ash_form_builder, :theme, MyAppWeb.CustomTheme # ───────────────────────────────────────────────────────────────────────────── # 1.3 Add Extension to Resource # ───────────────────────────────────────────────────────────────────────────── # In your Ash Resource, add the extension: # # use Ash.Resource, # domain: MyApp.Healthcare, # extensions: [AshFormBuilder] # ← Add this # ============================================================================= # 2. RESOURCE DEFINITION WITH FORM DSL # ============================================================================= defmodule MyApp.Healthcare.Clinic do @moduledoc """ Clinic resource demonstrating AshFormBuilder with many-to-many relationships. ## Form Auto-Inference Fields are automatically inferred from the action's `accept` list: | Ash Type | UI Type | |-------------------|-------------------------| | `:string` | `:text_input` | | `:integer` | `:number` | | `:boolean` | `:checkbox` | | `:date` | `:date` | | `:datetime` | `:datetime` | | `:enum` | `:select` | | `many_to_many` | `:multiselect_combobox` | """ use Ash.Resource, domain: MyApp.Healthcare, extensions: [AshFormBuilder] # ─────────────────────────────────────────────────────────────────────────── # Attributes # ─────────────────────────────────────────────────────────────────────────── attributes do uuid_primary_key :id attribute :name, :string do allow_nil? false description "The clinic's official name" end attribute :address, :string do allow_nil? false end attribute :phone, :string do constraints match: ~r/^\+?[\d\s-]+$/ end attribute :email, :string attribute :website, :string attribute :is_active, :boolean, default: true timestamps() end # ─────────────────────────────────────────────────────────────────────────── # Relationships # ─────────────────────────────────────────────────────────────────────────── relationships do # Many-to-many: Auto-rendered as :multiselect_combobox many_to_many :specialties, MyApp.Healthcare.Specialty do through MyApp.Healthcare.ClinicSpecialty source_attribute_on_join_resource :clinic_id destination_attribute_on_join_resource :specialty_id end # Many-to-many with creatable support (create new items on-the-fly) many_to_many :tags, MyApp.Healthcare.Tag do through MyApp.Healthcare.ClinicTag source_attribute_on_join_resource :clinic_id destination_attribute_on_join_resource :tag_id end end # ─────────────────────────────────────────────────────────────────────────── # Actions # ─────────────────────────────────────────────────────────────────────────── actions do defaults [:create, :read, :update, :destroy] create :create do accept [:name, :address, :phone, :email, :website, :is_active] # Manage relationships separately manage_relationship :specialties, :specialties, type: :append_and_remove manage_relationship :tags, :tags, type: :append_and_remove end update :update do accept [:name, :address, :phone, :email, :website, :is_active] manage_relationship :specialties, :specialties, type: :append_and_remove manage_relationship :tags, :tags, type: :append_and_remove end end # ─────────────────────────────────────────────────────────────────────────── # Validations & Policies # ─────────────────────────────────────────────────────────────────────────── validations do validate present([:name, :address]) validate string_length(:name, min: 3, max: 100) end policies do policy action_type(:create) do authorize_if actor_present() end policy action_type(:update) do authorize_if actor_present() end end # =========================================================================== # ASH FORM BUILDER DSL - Declarative Form Configuration # =========================================================================== # # The `form` block defines how your form should render. Fields not declared # here are auto-inferred from the action's `accept` list. # # =========================================================================== form do # Required: Target action action :create # Optional: Customize submit button submit_label "Create Clinic" # Optional: CSS class for fields wrapper wrapper_class "space-y-6" # Optional: HTML id for the
element # form_id "clinic-create-form" # ──────────────────────────────────────────────────────────────────────── # Field Overrides # ──────────────────────────────────────────────────────────────────────── # # Declare fields to override auto-inferred settings or add customization. # Fields not declared here are still rendered with defaults. # ──────────────────────────────────────────────────────────────────────── field :name do label "Clinic Name" placeholder "e.g., Downtown Medical Center" required true # Optional: CSS classes # class "input-lg" # wrapper_class "mb-6" end field :address do label "Street Address" type :textarea # Override inferred :text_input placeholder "Full address including city and zip" # Additional textarea options # rows 4 required true end field :phone do label "Contact Phone" placeholder "+1 555-123-4567" # type :tel # Could override to :tel for mobile keyboards end field :email do label "Email Address" type :email # Override to email input type placeholder "contact@clinic.com" end field :website do label "Website URL" type :url placeholder "https://www.clinic.com" end field :is_active do label "Clinic Active" type :checkbox hint "Uncheck to mark as temporarily closed" end # ──────────────────────────────────────────────────────────────────────── # Many-to-Many: Searchable Combobox (NON-CREATABLE) # ──────────────────────────────────────────────────────────────────────── # # Use when users should only select from existing records. # ──────────────────────────────────────────────────────────────────────── field :specialties do type :multiselect_combobox label "Medical Specialties" placeholder "Search specialties..." required false opts [ # LiveView search event handler search_event: "search_specialties", # Debounce delay in milliseconds debounce: 300, # Resource field mappings label_key: :name, # Field to display in dropdown value_key: :id, # Field to use as value # Optional: Preload options (for small datasets) # preload_options: [{"Cardiology", "uuid-1"}, {"Pediatrics", "uuid-2"}] # Hint text hint: "Search and select all applicable specialties" ] end # ──────────────────────────────────────────────────────────────────────── # Many-to-Many: CREATABLE Combobox ⭐ NEW FEATURE # ──────────────────────────────────────────────────────────────────────── # # Use when users should be able to create new items on-the-fly. # Example: Tags, categories, or labels that don't exist yet. # ──────────────────────────────────────────────────────────────────────── field :tags do type :multiselect_combobox label "Tags" placeholder "Search or create tags..." required false opts [ # ★ Enable creatable functionality creatable: true, # Action to call on destination resource (default: :create) create_action: :create, # Label template for create button # The empty quotes "" will be replaced with user's input create_label: "Create \"\"", # LiveView search event handler search_event: "search_tags", # Debounce delay in milliseconds debounce: 300, # Resource field mappings label_key: :name, value_key: :id, # Hint text hint: "Type to search existing tags or create a new one" ] end # ──────────────────────────────────────────────────────────────────────── # Nested Forms (has_many relationships) # ──────────────────────────────────────────────────────────────────────── # # For managing child records inline (e.g., clinic departments). # ──────────────────────────────────────────────────────────────────────── # nested :departments do # label "Departments" # cardinality :many # or :one # add_label "Add Department" # remove_label "Remove" # create_action :create # update_action :update # # # Nested fields # field :name do # label "Department Name" # required true # end # # field :budget do # label "Annual Budget" # type :number # end # end end # =========================================================================== # UPDATE FORM - Separate configuration for update action # =========================================================================== # # You can define multiple `form` blocks in the same resource. # Each targets a different action (create, update, destroy, etc.) # # Update forms automatically: # - Preload many_to_many relationships to show existing selections # - Populate all fields with current record values # =========================================================================== form do # Target the update action action :update # Customize submit button for update submit_label "Save Changes" # Optional: Different wrapper class for update forms # wrapper_class "space-y-6 update-form" # Field customizations specific to update forms field :name do label "Clinic Name" placeholder "e.g., Downtown Medical Center" required true # Update-specific hint hint "Changing the name will require re-verification" end field :address do label "Street Address" type :textarea placeholder "Full address including city and zip" required true end field :phone do label "Contact Phone" placeholder "+1 555-123-4567" end field :email do label "Email Address" type :email placeholder "contact@clinic.com" end field :website do label "Website URL" type :url placeholder "https://www.clinic.com" end field :is_active do label "Clinic Active" type :checkbox hint "Uncheck to mark as temporarily closed" end # Many-to-Many: Searchable Combobox # In update forms, existing selections are automatically preloaded field :specialties do type :multiselect_combobox label "Medical Specialties" placeholder "Search specialties..." required false opts [ search_event: "search_specialties", debounce: 300, label_key: :name, value_key: :id, hint: "Search and select all applicable specialties" ] end # Many-to-Many: Creatable Combobox field :tags do type :multiselect_combobox label "Tags" placeholder "Search or create tags..." required false opts [ creatable: true, create_action: :create, create_label: "Create \"\"", search_event: "search_tags", debounce: 300, label_key: :name, value_key: :id, hint: "Type to search existing tags or create a new one" ] end end end # ============================================================================= # 3. SUPPORTING RESOURCES # ============================================================================= defmodule MyApp.Healthcare.Specialty do @moduledoc "Medical specialty (e.g., Cardiology, Pediatrics)" use Ash.Resource, domain: MyApp.Healthcare, data_layer: Ash.DataLayer.Ets # Replace with your data layer attributes do uuid_primary_key :id attribute :name, :string, allow_nil?: false attribute :code, :string, allow_nil?: false attribute :description, :string end actions do defaults [:read, :create] end end defmodule MyApp.Healthcare.Tag do @moduledoc "Tag for categorization (creatable on-the-fly)" use Ash.Resource, domain: MyApp.Healthcare, data_layer: Ash.DataLayer.Ets attributes do uuid_primary_key :id attribute :name, :string, allow_nil?: false attribute :color, :string, default: "gray" end actions do defaults [:read, :create] end end # Join resources for many-to-many relationships defmodule MyApp.Healthcare.ClinicSpecialty do use Ash.Resource, domain: MyApp.Healthcare attributes do uuid_primary_key :id attribute :clinic_id, :uuid, allow_nil?: false attribute :specialty_id, :uuid, allow_nil?: false end relationships do belongs_to :clinic, MyApp.Healthcare.Clinic belongs_to :specialty, MyApp.Healthcare.Specialty end end defmodule MyApp.Healthcare.ClinicTag do use Ash.Resource, domain: MyApp.Healthcare attributes do uuid_primary_key :id attribute :clinic_id, :uuid, allow_nil?: false attribute :tag_id, :uuid, allow_nil?: false end relationships do belongs_to :clinic, MyApp.Healthcare.Clinic belongs_to :tag, MyApp.Healthcare.Tag end end # ============================================================================= # 4. DOMAIN CONFIGURATION (CODE INTERFACES) # ============================================================================= # # Domain Code Interfaces generate clean helper functions that integrate # seamlessly with AshFormBuilder. This is the recommended pattern. # # ───────────────────────────────────────────────────────────────────────────── # Why Use Code Interfaces? # ───────────────────────────────────────────────────────────────────────────── # # • Zero boilerplate - No manual AshPhoenix.Form calls # • Type-safe - Generated functions with proper specs # • Policy-aware - Automatically includes actor context # • Consistent - Standardized form creation across your app # # ───────────────────────────────────────────────────────────────────────────── defmodule MyApp.Healthcare do @moduledoc """ Healthcare domain with Code Interfaces for form generation. """ use Ash.Domain resources do # ──────────────────────────────────────────────────────────────────────── # Clinic Resource with Form Code Interfaces # ──────────────────────────────────────────────────────────────────────── resource MyApp.Healthcare.Clinic do # Standard CRUD operations define :list_clinics, action: :read define :get_clinic, action: :read, get_by: [:id] # ★ Form Code Interfaces - Auto-generates helper functions # # Generates: # - MyApp.Healthcare.Clinic.Form.for_create/1 # - MyApp.Healthcare.Clinic.Form.for_update/2 # define :form_to_create_clinic, action: :create define :form_to_update_clinic, action: :update end # ──────────────────────────────────────────────────────────────────────── # Supporting Resources # ──────────────────────────────────────────────────────────────────────── resource MyApp.Healthcare.Specialty do define :list_specialties, action: :read define :search_specialties, action: :read end resource MyApp.Healthcare.Tag do define :list_tags, action: :read define :search_tags, action: :read end end end # ============================================================================= # 5. PHOENIX LIVEVIEW INTEGRATION # ============================================================================= # # The LiveView is remarkably simple thanks to Domain Code Interfaces. # No manual AshPhoenix.Form setup required! # # ───────────────────────────────────────────────────────────────────────────── defmodule MyAppWeb.ClinicLive.Form do @moduledoc """ LiveView for creating/editing Clinics. ## Key Features Demonstrated - Zero manual AshPhoenix.Form calls - Domain Code Interface usage - MishkaTheme with searchable combobox - Creatable combobox for tags - Automatic policy enforcement """ use MyAppWeb, :live_view alias MyApp.Healthcare # ─────────────────────────────────────────────────────────────────────────── # MOUNT - Form Creation via Domain Code Interface # ─────────────────────────────────────────────────────────────────────────── @impl true def mount(%{"id" => id} = _params, _session, socket) do # EDIT MODE: Update existing clinic # # Note: for_update/2 automatically preloads required many_to_many relationships # so the combobox displays current selections. Manual preloading shown here # is optional but useful if you need the loaded data for other purposes. # clinic = Healthcare.get_clinic!(id, load: [:specialties, :tags], actor: socket.assigns.current_user) # for_update/2 auto-preloads required relationships based on form configuration form = Healthcare.Clinic.Form.for_update(clinic, actor: socket.assigns.current_user) {:ok, socket |> assign(:page_title, "Edit Clinic") |> assign(:form, form) |> assign(:mode, :edit) |> assign(:specialty_options, load_options(clinic.specialties)) |> assign(:tag_options, load_options(clinic.tags))} end def mount(_params, _session, socket) do # CREATE MODE: New clinic # # The Form.for_create/1 helper is generated by the Domain Code Interface # and configured via the form DSL block in the resource. # form = Healthcare.Clinic.Form.for_create(actor: socket.assigns.current_user) {:ok, socket |> assign(:page_title, "New Clinic") |> assign(:form, form) |> assign(:mode, :create) |> assign(:specialty_options, []) |> assign(:tag_options, [])} end # ─────────────────────────────────────────────────────────────────────────── # RENDER - Using FormComponent # ─────────────────────────────────────────────────────────────────────────── @impl true def render(assigns) do ~H"""

<%= @page_title %>

<%!-- The FormComponent: - Uses the configured theme (MishkaTheme) - Renders all fields from the form DSL - Handles validation errors automatically - Manages nested forms --%> <.live_component module={AshFormBuilder.FormComponent} id="clinic-form" resource={MyApp.Healthcare.Clinic} form={@form} />
""" end # ─────────────────────────────────────────────────────────────────────────── # SEARCH HANDLERS - For Combobox Many-to-Many # ─────────────────────────────────────────────────────────────────────────── # # These handlers respond to combobox search events and push # updated options back to the client via LiveView events. # ─────────────────────────────────────────────────────────────────────────── @impl true def handle_event("search_specialties", %{"query" => query}, socket) do specialties = MyApp.Healthcare.Specialty |> Ash.Query.filter(contains(name: ^query)) |> Healthcare.read!(actor: socket.assigns.current_user) options = Enum.map(specialties, &{&1.name, &1.id}) {:noreply, push_event(socket, "update_combobox_options", %{ field: "specialties", options: options })} end @impl true def handle_event("search_tags", %{"query" => query}, socket) do # For creatable combobox, search existing tags # Users can still create new ones via the create button tags = MyApp.Healthcare.Tag |> Ash.Query.filter(contains(name: ^query)) |> Healthcare.read!(actor: socket.assigns.current_user) options = Enum.map(tags, &{&1.name, &1.id}) {:noreply, push_event(socket, "update_combobox_options", %{ field: "tags", options: options })} end # ─────────────────────────────────────────────────────────────────────────── # SUCCESS HANDLER - Form Submission # ─────────────────────────────────────────────────────────────────────────── @impl true def handle_info({:form_submitted, MyApp.Healthcare.Clinic, clinic}, socket) do message = case socket.assigns.mode do :create -> "Clinic created successfully!" :update -> "Clinic updated successfully!" end {:noreply, socket |> put_flash(:info, message) |> push_navigate(to: ~p"/clinics/#{clinic.id}")} end # ─────────────────────────────────────────────────────────────────────────── # PRIVATE HELPERS # ─────────────────────────────────────────────────────────────────────────── defp load_options(records) do Enum.map(records, &{&1.name, &1.id}) end end # ============================================================================= # 6. THEME CUSTOMIZATION & ADAPTERS # ============================================================================= # # AshFormBuilder supports theming via the `AshFormBuilder.Theme` behaviour. # You can use built-in themes or create custom adapters. # # ───────────────────────────────────────────────────────────────────────────── # 6.1 Built-in Themes # ───────────────────────────────────────────────────────────────────────────── # # • AshFormBuilder.Themes.Default - Semantic HTML with minimal styling # • AshFormBuilder.Theme.MishkaTheme - MishkaChelekom component integration # # ───────────────────────────────────────────────────────────────────────────── # 6.2 Creating a Custom Theme # ───────────────────────────────────────────────────────────────────────────── defmodule MyAppWeb.CustomTheme do @moduledoc """ Custom theme example using Tailwind CSS directly. To use: 1. Create this module in your app 2. Implement render_field/2 for each field type 3. Configure: config :ash_form_builder, :theme, MyAppWeb.CustomTheme """ @behaviour AshFormBuilder.Theme use Phoenix.Component @impl AshFormBuilder.Theme def render_field(assigns, opts) do # Add your custom assigns assigns = Map.put(assigns, :custom_opts, opts) case assigns.field.type do :text_input -> render_text_input(assigns) :textarea -> render_textarea(assigns) :select -> render_select(assigns) :multiselect_combobox -> render_combobox(assigns) :checkbox -> render_checkbox(assigns) :number -> render_number(assigns) :email -> render_email(assigns) :password -> render_password(assigns) :date -> render_date(assigns) :datetime -> render_datetime(assigns) :url -> render_url(assigns) :tel -> render_tel(assigns) :hidden -> render_hidden(assigns) _ -> render_text_input(assigns) end end @impl AshFormBuilder.Theme def render_nested(_assigns) do # Return nil to use default nested form rendering nil end # ─────────────────────────────────────────────────────────────────────────── # Example: Custom Text Input with Tailwind # ─────────────────────────────────────────────────────────────────────────── defp render_text_input(assigns) do ~H"""

{@field.hint}

<.errors :for={err <- (@form[@field.name] || %{errors: []}).errors} message={elem(err, 0)} />
""" end # ─────────────────────────────────────────────────────────────────────────── # Example: Custom Combobox with Creatable Support # ─────────────────────────────────────────────────────────────────────────── defp render_combobox(assigns) do opts = assigns.field.opts || [] creatable? = Keyword.get(opts, :creatable, false) assigns = assign(assigns, :creatable?, creatable?) ~H"""
<%!-- Your combobox implementation here --%>
<%!-- Creatable button (if enabled) --%>

{@field.hint}

""" end # ─────────────────────────────────────────────────────────────────────────── # Error Component # ─────────────────────────────────────────────────────────────────────────── attr(:message, :string, required: true) defp errors(assigns) do ~H"""

{@message}

""" end # Add more render_* functions for other field types... end # ───────────────────────────────────────────────────────────────────────────── # 6.3 Theme Adapter Pattern # ───────────────────────────────────────────────────────────────────────────── # # For large applications, you might want to create theme adapters that # compose multiple UI libraries or provide consistent design tokens. # # Example structure: # # lib/my_app_web/form_builder/ # ├── theme.ex # Base theme behaviour # ├── default_theme.ex # Default implementation # ├── mishka_theme.ex # MishkaChelekom adapter # ├── daisy_theme.ex # DaisyUI/Tailwind adapter # └── custom_theme.ex # Your app's custom theme # # ───────────────────────────────────────────────────────────────────────────── # ============================================================================= # 7. AREAS OF ENHANCEMENT # ============================================================================= # # The following enhancements could improve the ash_form_builder library: # # ───────────────────────────────────────────────────────────────────────────── # 7.1 Creatable Combobox Improvements # ───────────────────────────────────────────────────────────────────────────── # # • Better value extraction: Currently uses regex to extract value from # create_label. Could pass the raw input value directly from the combobox. # # • Loading states: Show loading indicator while creating new items. # # • Error handling: Display inline errors when creation fails (e.g., duplicate # name validation). # # • Confirmation dialog: Optional confirmation before creating new items. # # • Bulk create: Allow creating multiple new items at once. # # ───────────────────────────────────────────────────────────────────────────── # 7.2 Search Optimization # ───────────────────────────────────────────────────────────────────────────── # # • Server-side pagination: For large datasets, paginate search results. # # • Caching: Cache frequently searched results to reduce database queries. # # • Debounce configuration: Make debounce configurable per-field. # # • Minimum search length: Configure minimum characters before searching. # # ───────────────────────────────────────────────────────────────────────────── # 7.3 Nested Form Enhancements # ───────────────────────────────────────────────────────────────────────────── # # • Sortable lists: Drag-and-drop reordering for has_many relationships. # # • Collapsible sections: Collapse/expand nested form sections. # # • Conditional rendering: Show/hide nested forms based on parent field values. # # • Deep nesting: Support for nested forms within nested forms. # # ───────────────────────────────────────────────────────────────────────────── # 7.4 Validation & UX # ───────────────────────────────────────────────────────────────────────────── # # • Inline validation: Real-time validation as users type. # # • Field-level permissions: Hide/disable fields based on user permissions. # # • Conditional fields: Show/hide fields based on other field values. # # • Multi-step forms: Wizard-style forms with progress indicators. # # • Form drafts: Auto-save form state to localStorage or database. # # ───────────────────────────────────────────────────────────────────────────── # 7.5 Internationalization (i18n) # ───────────────────────────────────────────────────────────────────────────── # # • Translatable labels: Support for GetText or similar i18n libraries. # # • Locale-aware formatting: Date, number, and currency formatting. # # • RTL support: Right-to-left language support. # # ───────────────────────────────────────────────────────────────────────────── # 7.6 Performance # ───────────────────────────────────────────────────────────────────────────── # # • Lazy loading: Load combobox options on demand. # # • Virtual scrolling: For large option lists in combobox. # # • Form optimization: Reduce LiveView payload size for large forms. # # ============================================================================= # 8. TESTING YOUR FORMS # ============================================================================= # # ───────────────────────────────────────────────────────────────────────────── # 8.1 Unit Tests for Form Schema # ───────────────────────────────────────────────────────────────────────────── # # test "clinic form has correct fields" do # schema = AshFormBuilder.Infer.infer_schema(MyApp.Healthcare.Clinic, :create) # # assert Enum.any?(schema.fields, &(&1.name == :name)) # assert Enum.any?(schema.fields, &(&1.type == :multiselect_combobox)) # end # # ───────────────────────────────────────────────────────────────────────────── # 8.2 Component Tests # ───────────────────────────────────────────────────────────────────────────── # # test "form submission creates clinic", %{conn: conn} do # {:ok, view, _html} = live_isolated(conn, MyAppWeb.ClinicLive.Form) # # html = # view # |> form("#clinic-form", clinic: %{name: "Test Clinic"}) # |> render_submit() # # assert html =~ "Clinic created successfully!" # end # # ───────────────────────────────────────────────────────────────────────────── # 8.3 Creatable Combobox Tests # ───────────────────────────────────────────────────────────────────────────── # # test "creating new tag from combobox", %{conn: conn} do # {:ok, view, _html} = live_isolated(conn, MyAppWeb.ClinicLive.Form) # # # Trigger create event # {:noreply, _updated_socket} = # AshFormBuilder.FormComponent.handle_event( # "create_combobox_item", # %{ # "field" => "tags", # "resource" => "Elixir.MyApp.Healthcare.Tag", # "action" => "create", # "creatable_value" => "New Tag" # }, # socket # ) # # # Verify tag was created # assert %MyApp.Healthcare.Tag{name: "New Tag"} = # Ash.read_one!(MyApp.Healthcare.Tag, name: "New Tag") # end # # ============================================================================= # END OF GUIDE # ============================================================================= # # For more information: # • Ash Framework: https://hexdocs.pm/ash # • Phoenix LiveView: https://hexdocs.pm/phoenix_live_view # • MishkaChelekom: https://github.com/mishkacelekom/mishka_chelekom # # =============================================================================