Skip to content

has_many Stimulus controller on create/edit does not run #4588

Description

@rctneil

Configuration

Avo version

Beta 58

Rails version

8.1.3

Ruby version

3.3.4

License type
  • Community
  • Pro
  • [ x] Advanced
Are you using Avo monkey patches, overriding views or view components?
  • Yes — please post code samples below
  • [ x] No

Description

It seems as though stimulus controllers that run, do not get run when in nested mode on create/edit.

Additional context

            tab title: "Companies" do
                field :companyables, as: :has_many,
                    nested: { on: :forms, limit: 10 }
            end

Not sure if you need the full controller but thought i'd post it to save you having to ask:

import { Controller } from "@hotwired/stimulus";

const LOADER_CLASSES = "absolute bg-gray-100 opacity-10 w-full h-full";

export default class extends Controller {
  static targets = ["companyFieldInput", "modelFieldInput", "submodelFieldInput", "companyHasManyWrapper", "modelBelongsToWrapper", "submodelBelongsToWrapper"];

  static values = {
    view: String,
  };

  static initialModelValue;
  static initialSubmodelValue;

  // get placeholder() {
  //   return this.modelFieldInputTarget.options[0];
  // }

  set loading(isLoading) {
    if (isLoading) {
      // create a loader overlay
      const loadingDiv = document.createElement("div");
      loadingDiv.className = LOADER_CLASSES;
      loadingDiv.dataset.target = "model-loader";

      // add the loader overlay
      this.modelBelongsToWrapperTarget.prepend(loadingDiv);
      this.modelBelongsToWrapperTarget.classList.add("opacity-50");
    } else {
      // remove the loader overlay
      this.modelBelongsToWrapperTarget
        .querySelector('[data-target="model-loader"]')
        .remove();
      this.modelBelongsToWrapperTarget.classList.remove("opacity-50");
    }
  }

  async connect() {
    // Add the controller functionality only on forms
    if (["edit", "new"].includes(this.viewValue)) {
      if (this.hasModelFieldInputTarget) this.captureTheInitialModelValue();
      if (this.hasSubmodelFieldInputTarget) this.captureTheInitialSubmodelValue();

      // Trigger the change on load
      await this.onCompanyChange();
      await this.onModelChange();
    }
  }

  // Read the company select.
  // If there's any value selected show the models and prefill them.
  async onCompanyChange() {
    if (this.hasCompanyFieldInputTarget && this.hasModelFieldInputTarget) {
      // Get the company
      const company = this.companyFieldInputTarget.value;

      // Dynamically fetch the models for this company
      const models = await this.fetchModelsForCompany(company);

      // Clear the select of options
      Object.keys(this.modelFieldInputTarget.options).forEach(() => {
        this.modelFieldInputTarget.options.remove(0);
      });

      // Add blank option
      this.modelFieldInputTarget.add(new Option("Select a Model", ""));

      // Add the new models
      models.forEach((model) => {
        this.modelFieldInputTarget.add(new Option(model.name, model.id));
      });

      // Check if the initial value is present in the models array and select it.
      // If not, select the first item
      const currentOptions = Array.from(this.modelFieldInputTarget.options).map(
        (item) => item.value
      );

      if (currentOptions.includes(this.initialModelValue)) {
        this.modelFieldInputTarget.value = this.initialModelValue;
      } else {
        // Select the first item
        this.modelFieldInputTarget.value = this.modelFieldInputTarget.options[0].value;
      }
    }
  }

  // Read the model select.
  // If there's any value selected show the submodels and prefill them.
  async onModelChange() {
    if (this.hasCompanyFieldInputTarget && this.hasModelFieldInputTarget && this.hasSubmodelFieldInputTarget) {
      // Get the company
      const company = this.companyFieldInputTarget.value;

      // Get the model
      const model = this.modelFieldInputTarget.value;

      // Dynamically fetch the models for this company
      const submodels = await this.fetchSubmodelsForModel(model);

      // Clear the select of options
      Object.keys(this.submodelFieldInputTarget.options).forEach(() => {
        this.submodelFieldInputTarget.options.remove(0);
      });

      // Add blank option
      this.submodelFieldInputTarget.add(new Option("Select a Submodel", ""));

      // Add the new models
      submodels.forEach((submodel) => {
        this.submodelFieldInputTarget.add(new Option(submodel.name, submodel.id));
      });

      // Check if the initial value is present in the models array and select it.
      // If not, select the first item
      const currentOptions = Array.from(this.submodelFieldInputTarget.options).map(
        (item) => item.value
      );

      if (currentOptions.includes(this.initialSubmodelValue)) {
        this.submodelFieldInputTarget.value = this.initialSubmodelValue;
      } else {
        // Select the first item
        this.submodelFieldInputTarget.value = this.submodelFieldInputTarget.options[0].value;
      }
    }
  }

  captureTheInitialModelValue() {
    this.initialModelValue = this.modelFieldInputTarget.value;
  }

  captureTheInitialSubmodelValue() {
    this.initialSubmodelValue = this.submodelFieldInputTarget.value;
  }

  async fetchModelsForCompany(company) {
    if (!company) {
      return [];
    }

    this.loading = true;

    const response = await fetch(
        `${window.Avo.configuration.root_path}/companies/models?company=${encodeURIComponent(company)}`
    );

    const data = await response.json();

    this.loading = false;

    return data;
  }

  async fetchSubmodelsForModel(model) {
    if (!model) {
      return [];
    }

    this.loading = true;

    const response = await fetch(
      `${window.Avo.configuration.root_path}/companies/models/submodels?model=${model}`
    );
    const data = await response.json();

    this.loading = false;

    return data;
  }
}

Screenshots or screen recordings

In the screenshot, that select box should already have filtered down to a much smaller set of options. Works fine when not in nested mode.

Image

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions