# Cascading Dropdowns from json fetches

**URL:** https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639
**Category:** Developing Nodes
**Created:** [15 January 2025 10:32 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639 "2025-01-15T10:32:25Z")
**Posts on this page:** 8
**Page:** 1

<div class="post-metadata">

### Author: ![znetsixe](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/znetsixe/32/72588_2.png) [@znetsixe](https://discourse.nodered.org/u/znetsixe)
#### Post date: [15 January 2025 10:32 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/1 "2025-01-15T10:32:25Z")

</div>

Hey everyone,  
Im trying to build a cascading dropdown fetch for my custom created nodes. However after spending a day of failures Im lost.

Fetching something independent from something else works just fine.  
So here are partials of relevant code.

Im trying to make this modular so I can just copy paste the relevant stuff over all my nodes.  
My result is that I only see the depending dropdown change whenever I deselect and select the supplier for example first. After I save my changes and deploy my values of the selected supplier appears in the dropdown list but not for all the cascading dropdowns.

Perhaps I made this too complicated for it to work properly and would really appreciate it if somebody could guide me to an example that also does this properly. I don't mind crafting my helper functions again.

```auto
 oneditprepare: function () {
      const node = this;

      console.log("--------------Starting on edit prepare-----------------");
      console.log("Saved Supplier:", node.supplier);
      console.log("Saved SubType:", node.subType);
      console.log("Saved Model:", node.model);

      const configUrls = {
        cloud: {
          units: "https://example.com/api/units",
          assets: "https://example.com/api/assets",
          config: "https://example.com/api/config",
        },
        local: {
          units: "http://localhost:1880/generalFunctions/datasets/unitData.json",
          assets: "http://localhost:1880/generalFunctions/datasets/assetData.json",
          config: "http://localhost:1880/measurement/dependencies/measurement/measurementConfig.json",
        },
      };

      const elements = {
        scalingCheckbox: document.getElementById("node-input-scaling"),
        logCheckbox: document.getElementById("node-input-enableLog"),
        rowInputMin: document.getElementById("row-input-i_min"),
        rowInputMax: document.getElementById("row-input-i_max"),
        rowLogLevel: document.getElementById("row-logLevel"),
        logLevelSelect: document.getElementById("node-input-logLevel"),
        supplier: document.getElementById("node-input-supplier"),
        subType: document.getElementById("node-input-subType"),
        model: document.getElementById("node-input-model"),
        unit: document.getElementById("node-input-unit"),
        smoothMethod: document.getElementById("node-input-smooth_method"),
      };

      function initializeToggles() {
        const toggleVisibility = menuUtils.toggleElementVisibility;

        elements.scalingCheckbox.addEventListener("change", () =>
          toggleVisibility(elements.scalingCheckbox.checked, [elements.rowInputMin, elements.rowInputMax])
        );

        elements.logCheckbox.addEventListener("change", () =>
          toggleVisibility(elements.logCheckbox.checked, [elements.rowLogLevel])
        );

        toggleVisibility(elements.scalingCheckbox.checked, [elements.rowInputMin, elements.rowInputMax]);
        toggleVisibility(elements.logCheckbox.checked, [elements.rowLogLevel]);
      }

      function populateSmoothingMethods() {
        menuUtils.fetchData(configUrls.cloud.config, configUrls.local.config)
          .then((configData) => {
            const smoothingMethods = configData.smoothing.smoothMethod.rules.values.map(o => o.value) || [];
            menuUtils.populateDropdown(elements.smoothMethod, smoothingMethods, node, "smooth_method");
          })
          .catch((err) => {
            console.error("Error loading smoothing methods", err);
          });
      }

        populateSmoothingMethods();
        initializeToggles();
        fetchAndPopulateDropdowns();

        async function fetchAndPopulateDropdowns() {
          try {
              const assetDatabaseURL = "http://localhost:1880/generalFunctions/datasets/assetData";
              console.log("Asset Database URL: ", assetDatabaseURL);

              const configData = await menuUtils.fetchData(configUrls.cloud.config, configUrls.local.config);
              const assetType = configData.asset?.type?.default;
              console.log("Asset Type: ", assetType);

              // Construct URL for suppliers
              const localSuppliersUrl = menuUtils.constructUrl(assetDatabaseURL, `${assetType}s`, "suppliers.json");
              console.log("Suppliers URL: ", localSuppliersUrl);

              const supplierData = await menuUtils.fetchData(localSuppliersUrl, localSuppliersUrl);
              const suppliers = supplierData.map((supplier) => supplier.name);

              console.log("Suppliers: ", suppliers);

              menuUtils.populateDropdown(elements.supplier, suppliers, node, "supplier", async (selectedSupplier) => {
                  // Construct URL for the selected supplier folder
                  const supplierFolder = menuUtils.constructUrl(assetDatabaseURL, `${assetType}s`, selectedSupplier);
                  console.log("Supplier Folder: ", supplierFolder);

                  try {
                      // Construct URL for subtypes
                      const subTypesUrl = menuUtils.constructUrl(supplierFolder, "subtypes.json");
                      console.log("SubTypes URL: ", subTypesUrl);

                      const subTypeData = await menuUtils.fetchData(subTypesUrl, subTypesUrl);
                      const subTypes = subTypeData.map((subType) => subType.name);

                      console.log("SubTypes: ", subTypes);

                      menuUtils.populateDropdown(elements.subType, subTypes, node, "subType", async (selectedSubType) => {
                          // Construct URL for the selected subType folder
                          const subTypeFolder = menuUtils.constructUrl(supplierFolder, selectedSubType);
                          console.log("SubType Folder: ", subTypeFolder);

                          try {
                              // Construct URL for models
                              const modelsUrl = menuUtils.constructUrl(subTypeFolder, "models.json");
                              console.log("Models URL: ", modelsUrl);

                              const modelData = await menuUtils.fetchData(modelsUrl, modelsUrl);
                              const models = modelData.map((model) => model.name);

                              console.log("============= > Models: ", models);

                              menuUtils.populateDropdown(elements.model, models, node, "model");
                          } catch (modelError) {
                              console.error("Error fetching models: ", modelError);
                          }
                      });
                  } catch (subTypeError) {
                      console.error("Error fetching subTypes: ", subTypeError);
                  }
              });
          } catch (error) {
              console.error("Error fetching config or suppliers: ", error);
          }
      }

    },

    oneditsave: function () {
      const node = this;

      // Save basic properties
      ["name", "unit", "smooth_method", "supplier", "subType", "model"].forEach(
        (field) => (node[field] = document.getElementById(`node-input-${field}`).value || "")
      );

      // Save numeric and boolean properties
      ["scaling", "enableLog", "simulator"].forEach(
        (field) => (node[field] = document.getElementById(`node-input-${field}`).checked)
      );

      ["i_min", "i_max", "i_offset", "o_min", "o_max", "count"].forEach(
        (field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
      );

      node.logLevel = document.getElementById("node-input-logLevel").value || "info";

      // Validation checks
      if (node.scaling && (isNaN(node.i_min) || isNaN(node.i_max))) {
        RED.notify("Scaling enabled, but input range is incomplete!", "error");
      }

      if (!node.unit) {
        RED.notify("Unit selection is required.", "error");
      }

      if (node.subType && !node.unit) {
        RED.notify("Unit must be set when specifying a subtype.", "error");
      }
    },
  });

```

and my menuUtils.js

```auto
async function fetchData(url, fallbackUrl) {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return await response.json();
  } catch (err) {
    console.warn(`Primary URL failed: ${url}. Trying fallback URL: ${fallbackUrl}`, err);
    try {
      const response = await fetch(fallbackUrl);
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      return await response.json();
    } catch (fallbackErr) {
      console.error("Both primary and fallback URLs failed:", fallbackErr);
      return [];
    }
  }
}

function constructUrl(base, ...paths) {
  // Remove trailing slash from base and leading slashes from paths
  const sanitizedBase = base.replace(/\/+$/, "");
  const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));

  // Join sanitized base and paths
  const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
  console.log("Base:", sanitizedBase);
  console.log("Paths:", sanitizedPaths);
  console.log("Constructed URL:", url);
  return url;
}

function toggleElementVisibility(condition, elements) {
  elements.forEach(elem => {
    elem.style.display = condition ? "block" : "none";
  });
}

function generateHtml(htmlElement, options, savedValue) {
  htmlElement.innerHTML = options.length
    ? `<option value="">Select...</option>${options.map(opt => `<option value="${opt}">${opt}</option>`).join("")}`
    : "<option value=''>No options available</option>";

  if (savedValue && options.includes(savedValue)) {
    htmlElement.value = savedValue;
  }
}

async function populateDropdown(htmlElement, options, node, property, callback) {
  generateHtml(htmlElement, options, node[property]);

  htmlElement.addEventListener("change", async (e) => {
    const newValue = e.target.value;
    console.log(`Dropdown changed: ${property} = ${newValue}`);
    node[property] = newValue;
    if (callback) await callback(newValue); // Ensure async callback completion
  });
}

function handleSpecificDropdownChange(htmlElement, node) {
  switch (htmlElement.id) {
    case "node-input-supplier":
      handleSupplierChange(node);
      break;
    case "node-input-subType":
      handleSubTypeChange(node);
      break;
    case "node-input-model":
      handleModelChange(node);
      break;
    case "node-input-unit":
      handleUnitChange(node);
      break;
    default:
      console.log("No specific logic for this dropdown");
  }
}

function validateData(data, expectedKey) {
  if (!Array.isArray(data) || !data.every(item => expectedKey in item)) {
    console.error(`Invalid data format. Expected array with key "${expectedKey}".`, data);
    return [];
  }
  return data.map(item => item[expectedKey]);
}

function handleAssetTypeChange(node, suppliersDropdown, subTypeDropdown, modelDropdown, unitDropdown) {
  node.supplier = "";
  node.subType = "";
  node.model = "";
  node.unit = "";

  generateHtml(suppliersDropdown, [], "");
  generateHtml(subTypeDropdown, [], "");
  generateHtml(modelDropdown, [], "");
  generateHtml(unitDropdown, [], "");

  // Fetch and populate suppliers based on the selected asset type
  const suppliers = assetData[node.assetType]?.suppliers || [];
  populateDropdown(suppliersDropdown, suppliers, node, "supplier");
}

function handleSupplierChange(node, subTypeDropdown, modelDropdown, unitDropdown) {
  node.subType = "";
  node.model = "";
  node.unit = "";

  generateHtml(subTypeDropdown, [], "");
  generateHtml(modelDropdown, [], "");
  generateHtml(unitDropdown, [], "");

  // Fetch and populate subTypes based on the selected supplier
  const subTypes = node.supplier?.subTypes || [];
  populateDropdown(subTypeDropdown, subTypes, node, "subType");
}

function handleSubTypeChange(node, modelDropdown, unitDropdown) {
  node.model = "";
  node.unit = "";

  generateHtml(modelDropdown, [], "");
  generateHtml(unitDropdown, [], "");

  // Fetch and populate models based on the selected subType
  const models = node.subType?.models || [];
  populateDropdown(modelDropdown, models, node, "model");
}

function handleModelChange(node, unitDropdown) {
  node.unit = "";

  // Units are only updated when the subType changes, so no action here
  console.log(`Model selected: ${node.model}`);
}

function handleUnitChange(node) {
  console.log(`Unit selected: ${node.unit}`);
}

/* Generic dropdowns */
/* Asset dropdown functions */
function populateLogLevelOptions(logLevelSelect, configData, node) {

  // debug log level 
  //console.log("Displaying configData => ", configData) ;

  const logLevels = configData?.general?.logging?.logLevel?.rules?.values?.map(l => l.value) || [];

  //console.log("Displaying logLevels => ", logLevels);

  // Reuse your existing generic populateDropdown helper
  populateDropdown(logLevelSelect, logLevels, node.logLevel);
}

/* measurement specific */
function populateSmoothingOptions(smoothMethodSelect, configData, node) {
  const methods = configData?.smoothing?.smoothMethod?.rules?.values.map(m => m.value) || [];
  generateHtml(smoothMethodSelect, methods, node.smooth_method);
}

export {
  fetchData,
  toggleElementVisibility,
  generateHtml,
  populateDropdown,
  handleSpecificDropdownChange,
  handleAssetTypeChange,
  handleSupplierChange,
  handleSubTypeChange,
  handleModelChange,
  handleUnitChange,
  populateLogLevelOptions,
  populateSmoothingOptions,
  constructUrl
};

```

---

<div class="post-metadata">

### Author: ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)
#### Post date: [15 January 2025 11:34 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/2 "2025-01-15T11:34:47Z")

</div>

If you are coding for the Editor, you are better off using jQuery which is built into the editor. Then you may wish to look for a jQuery library that does what you want - might save a lot of forced hair removal. 🙂

---

<div class="post-metadata">

### Author: ![znetsixe](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/znetsixe/32/72588_2.png) [@znetsixe](https://discourse.nodered.org/u/znetsixe)
#### Post date: [15 January 2025 11:39 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/3 "2025-01-15T11:39:20Z")

</div>

Knowning nothing about jQuery or there libs. You have anything you can send me (link/example)? In parallel Ill start reading into that 😛

---

<div class="post-metadata">

### Author: ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)
#### Post date: [15 January 2025 11:54 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/4 "2025-01-15T11:54:52Z")

</div>

No problem. The jQuery API documentation is generally very good. w3schools have a decent intro:

> **[W3Schools.com](https://www.w3schools.com/jquery/)**
>
> W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

But possibly the fastest way to get going is to look at the code for some well used nodes.

jQuery has been around a long time so is very mature and continues to be developed. It has a very rich plugins scene.

> **[jquery cascading dropdowns at DuckDuckGo](https://duckduckgo.com/?q=%2Bjquery%2Bcascading%2Bdropdowns&t=vivaldi&ia=web)**
>
> DuckDuckGo. Privacy, Simplified.

---

<div class="post-metadata">

### Author: ![znetsixe](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/znetsixe/32/72588_2.png) [@znetsixe](https://discourse.nodered.org/u/znetsixe)
#### Post date: [15 January 2025 14:05 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/5 "2025-01-15T14:05:27Z")

</div>

I solved it without jQuery in the end:

For people wanting to learn from it or searching for something similar:

menuUtils.js helper file

```auto
async function fetchData(url, fallbackUrl) {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return await response.json();
  } catch (err) {
    console.warn(`Primary URL failed: ${url}. Trying fallback URL: ${fallbackUrl}`, err);
    try {
      const response = await fetch(fallbackUrl);
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      return await response.json();
    } catch (fallbackErr) {
      console.error("Both primary and fallback URLs failed:", fallbackErr);
      return [];
    }
  }
}

function constructUrl(base, ...paths) {
  // Remove trailing slash from base and leading slashes from paths
  const sanitizedBase = base.replace(/\/+$/, "");
  const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));

  // Join sanitized base and paths
  const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
  console.log("Base:", sanitizedBase);
  console.log("Paths:", sanitizedPaths);
  console.log("Constructed URL:", url);
  return url;
}

//Adjust for API of PIM
function constructCloudURL(base, ...paths) {
  // Remove trailing slash from base and leading slashes from paths
  const sanitizedBase = base.replace(/\/+$/, "");
  const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));
  // Join sanitized base and paths
  const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
  return url;
}

function generateHtml(htmlElement, options, savedValue) {
  htmlElement.innerHTML = options.length
    ? `<option value="">Select...</option>${options.map(opt => `<option value="${opt}">${opt}</option>`).join("")}`
    : "<option value=''>No options available</option>";

  if (savedValue && options.includes(savedValue)) {
    htmlElement.value = savedValue;
  }
}

async function populateDropdown(htmlElement, options, node, property, callback) {
  generateHtml(htmlElement, options, node[property]);

  htmlElement.addEventListener("change", async (e) => {
    const newValue = e.target.value;
    console.log(`Dropdown changed: ${property} = ${newValue}`);
    node[property] = newValue;
    RED.nodes.dirty(true);
    if (callback) await callback(newValue); // Ensure async callback completion
  });
}

    // Define the initialize toggles function within scope
export function initializeToggles(elements) {
      // Toggle visibility for scaling inputs
      elements.scalingCheckbox.addEventListener("change", function() {
          elements.rowInputMin.style.display = this.checked ? "block" : "none";
          elements.rowInputMax.style.display = this.checked ? "block" : "none";
      });

      // Toggle visibility for log level
      elements.logCheckbox.addEventListener("change", function() {
          elements.rowLogLevel.style.display = this.checked ? "block" : "none";
      });

      // Set initial states
      elements.rowInputMin.style.display = elements.scalingCheckbox.checked ? "block" : "none";
      elements.rowInputMax.style.display = elements.scalingCheckbox.checked ? "block" : "none";
      elements.rowLogLevel.style.display = elements.logCheckbox.checked ? "block" : "none";
  }

        // Define the smoothing methods population function within scope
export function populateSmoothingMethods(configUrls, elements, node) {
          fetchData(configUrls.cloud.config, configUrls.local.config)
              .then((configData) => {
                  const smoothingMethods = configData.smoothing?.smoothMethod?.rules?.values?.map(o => o.value) || [];
                  populateDropdown(elements.smoothMethod, smoothingMethods, node, "smooth_method");
              })
              .catch((err) => {
                  console.error("Error loading smoothing methods", err);
              });
      }

/* Generic dropdowns */
/* Asset dropdown functions */
export function populateLogLevelOptions(logLevelSelect, configData, node) {

  // debug log level 
  //console.log("Displaying configData => ", configData) ;

  const logLevels = configData?.general?.logging?.logLevel?.rules?.values?.map(l => l.value) || [];

  //console.log("Displaying logLevels => ", logLevels);

  // Reuse your existing generic populateDropdown helper
  populateDropdown(logLevelSelect, logLevels, node.logLevel);
}

//cascade dropdowns
export function fetchAndPopulateDropdowns(configUrls, elements, node) {
  const assetDatabaseURL = "http://localhost:1880/generalFunctions/datasets/assetData";
  
  fetchData(configUrls.cloud.config, configUrls.local.config)
    .then((configData) => {
      const assetType = configData.asset?.type?.default;
      const localSuppliersUrl = constructUrl(assetDatabaseURL, `${assetType}s`, "suppliers.json");
      
      return fetchData(localSuppliersUrl, localSuppliersUrl)
        .then((supplierData) => {
          const suppliers = supplierData.map((supplier) => supplier.name);
          
          // Populate suppliers dropdown and set up its change handler
          return populateDropdown(elements.supplier, suppliers, node, "supplier", function(selectedSupplier) {
            if (selectedSupplier) {
              populateSubTypes(configUrls, elements, node, selectedSupplier);
            }
          });
        })
        .then(() => {
          // If we have a saved supplier, trigger subTypes population
          if (node.supplier) {
            populateSubTypes(configUrls, elements, node, node.supplier);
          }
        });
    })
    .catch((error) => {
      console.error("Error in initial dropdown population:", error);
    });
}

function populateSubTypes(configUrls,elements, node, selectedSupplier) {
  const assetDatabaseURL = "http://localhost:1880/generalFunctions/datasets/assetData";
  
  fetchData(configUrls.cloud.config, configUrls.local.config)
    .then((configData) => {
      const assetType = configData.asset?.type?.default;
      const supplierFolder = constructUrl(assetDatabaseURL, `${assetType}s`, selectedSupplier);
      const subTypesUrl = constructUrl(supplierFolder, "subtypes.json");
      
      return fetchData(subTypesUrl, subTypesUrl)
        .then((subTypeData) => {
          const subTypes = subTypeData.map((subType) => subType.name);
          
          return populateDropdown(elements.subType, subTypes, node, "subType", function(selectedSubType) {
            if (selectedSubType) {
              // When subType changes, update both models and units
              populateModels(configUrls,elements, node, selectedSupplier, selectedSubType);
              populateUnitsForSubType(configUrls,elements, node, selectedSubType);
            }
          });
        })
        .then(() => {
          // If we have a saved subType, trigger both models and units population
          if (node.subType) {
            populateModels(configUrls,elements, node, selectedSupplier, node.subType);
            populateUnitsForSubType(configUrls,elements, node, node.subType);
          }
        });
    })
    .catch((error) => {
      console.error("Error populating subtypes:", error);
    });
} 

function populateUnitsForSubType(configUrls,elements, node, selectedSubType) {
// Fetch the units data
fetchData(configUrls.cloud.units, configUrls.local.units)
.then((unitsData) => {
  // Find the category that matches the subType name
  const categoryData = unitsData.units.find(
    category => category.category.toLowerCase() === selectedSubType.toLowerCase()
  );

  if (categoryData) {
    // Extract just the unit values and descriptions
    const units = categoryData.values.map(unit => ({
      value: unit.value,
      description: unit.description
    }));

    // Create the options array with descriptions as labels
    const options = units.map(unit => ({
      value: unit.value,
      label: `${unit.value} - ${unit.description}`
    }));

    // Populate the units dropdown
    populateDropdown(
      elements.unit,
      options.map(opt => opt.value),
      node,
      "unit"
    );

    // If there's no currently selected unit but we have options, select the first one
    if (!node.unit && options.length > 0) {
      node.unit = options[0].value;
      elements.unit.value = options[0].value;
    }
  } else {
    // If no matching category is found, provide a default % option
    const defaultUnits = [{ value: "%", description: "Percentage" }];
    populateDropdown(
      elements.unit,
      defaultUnits.map(unit => unit.value),
      node,
      "unit"
    );
    console.warn(`No matching unit category found for subType: ${selectedSubType}`);
  }
})
.catch((error) => {
  console.error("Error fetching units:", error);
});
}

function populateModels(configUrls,elements, node, selectedSupplier, selectedSubType) {
  const assetDatabaseURL = "http://localhost:1880/generalFunctions/datasets/assetData";
  
  fetchData(configUrls.cloud.config, configUrls.local.config)
    .then((configData) => {
      const assetType = configData.asset?.type?.default;
      const supplierFolder = constructUrl(assetDatabaseURL, `${assetType}s`, selectedSupplier);
      const subTypeFolder = constructUrl(supplierFolder, selectedSubType);
      const modelsUrl = constructUrl(subTypeFolder, "models.json");
      
      return fetchData(modelsUrl, modelsUrl)
        .then((modelData) => {
          const models = modelData.map((model) => model.name);
          populateDropdown(elements.model, models, node, "model");
        });
    })
    .catch((error) => {
      console.error("Error populating models:", error);
    });
}

```

Complete HTML for my custom node:

```auto
<script type="module">
  import * as menuUtils from "/generalFunctions/helper/menuUtils.js";

  RED.nodes.registerType("measurement", {
    category: "digital twin",
    color: "#e4a363",

    defaults: {
      name: { value: "", required: true },
      scaling: { value: false },
      i_min: { value: 0, required: true },
      i_max: { value: 0, required: true },
      i_offset: { value: 0 },
      o_min: { value: 0, required: true },
      o_max: { value: 1, required: true },
      simulator: { value: false },
      unit: { value: "unit", required: true },
      smooth_method: { value: "" },
      count: { value: "10", required: true },
      enableLog: { value: false },
      logLevel: { value: "info" },
      supplier: { value: "" },
      subType: { value: "" },
      model: { value: "" },
    },

    inputs: 1,
    outputs: 4,
    inputLabels: ["Measurement Input"],
    outputLabels: ["process", "dbase", "upStreamParent", "downStreamParent"],
    icon: "font-awesome/fa-tachometer",

    label: function () {
      return this.name || "Measurement";
    },

    oneditprepare: function() {
    const node = this;

    const configUrls = {
      cloud: {
        units: "https://example.com/api/units",
        config: "https://example.com/api/config",
      },
      local: {
        units: "http://localhost:1880/generalFunctions/datasets/unitData.json",
        config: "http://localhost:1880/measurement/dependencies/measurement/measurementConfig.json",
      },
    };

    const elements = {
      scalingCheckbox: document.getElementById("node-input-scaling"),
      logCheckbox: document.getElementById("node-input-enableLog"),
      rowInputMin: document.getElementById("row-input-i_min"),
      rowInputMax: document.getElementById("row-input-i_max"),
      rowLogLevel: document.getElementById("row-logLevel"),
      logLevelSelect: document.getElementById("node-input-logLevel"),
      supplier: document.getElementById("node-input-supplier"),
      subType: document.getElementById("node-input-subType"),
      model: document.getElementById("node-input-model"),
      unit: document.getElementById("node-input-unit"),
      smoothMethod: document.getElementById("node-input-smooth_method"),
    };

    // UI elements 
    menuUtils.initializeToggles(elements);
    menuUtils.populateSmoothingMethods(configUrls, elements, node);
    menuUtils.fetchAndPopulateDropdowns(configUrls, elements, node); // function for all assets

  },

    oneditsave: function () {
      const node = this;

      // Save basic properties
      ["name", "unit", "smooth_method", "supplier", "subType", "model"].forEach(
        (field) => (node[field] = document.getElementById(`node-input-${field}`).value || "")
      );

      // Save numeric and boolean properties
      ["scaling", "enableLog", "simulator"].forEach(
        (field) => (node[field] = document.getElementById(`node-input-${field}`).checked)
      );

      ["i_min", "i_max", "i_offset", "o_min", "o_max", "count"].forEach(
        (field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
      );

      node.logLevel = document.getElementById("node-input-logLevel").value || "info";

      // Validation checks
      if (node.scaling && (isNaN(node.i_min) || isNaN(node.i_max))) {
        RED.notify("Scaling enabled, but input range is incomplete!", "error");
      }

      if (!node.unit) {
        RED.notify("Unit selection is required.", "error");
      }

      if (node.subType && !node.unit) {
        RED.notify("Unit must be set when specifying a subtype.", "error");
      }
    },
  });
</script>

<!-- Main UI Template -->
<script type="text/html" data-template-name="measurement">
  <!-- Node Name -->
  <div class="form-row">
    <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
    <input
      type="text"
      id="node-input-name"
      placeholder="Measurement Name"
      style="width:70%;"
    />
  </div>

  <!-- Scaling Checkbox -->
  <div class="form-row">
    <label for="node-input-scaling"
      ><i class="fa fa-compress"></i> Scaling</label
    >
    <input
      type="checkbox"
      id="node-input-scaling"
      style="width:20px; vertical-align:baseline;"
    />
    <span>Enable input scaling?</span>
  </div>

  <!-- Source Min/Max (only if scaling is true) -->
  <div class="form-row" id="row-input-i_min">
    <label for="node-input-i_min"
      ><i class="fa fa-arrow-down"></i> Source Min</label
    >
    <input type="number" id="node-input-i_min" placeholder="0" />
  </div>

  <div class="form-row" id="row-input-i_max">
    <label for="node-input-i_max"
      ><i class="fa fa-arrow-up"></i> Source Max</label
    >
    <input type="number" id="node-input-i_max" placeholder="3000" />
  </div>

  <!-- Offset -->
  <div class="form-row">
    <label for="node-input-i_offset"
      ><i class="fa fa-adjust"></i> Input Offset</label
    >
    <input type="number" id="node-input-i_offset" placeholder="0" />
  </div>

  <!-- Output / Process Min/Max -->
  <div class="form-row">
    <label for="node-input-o_min"><i class="fa fa-tag"></i> Process Min</label>
    <input type="number" id="node-input-o_min" placeholder="0" />
  </div>
  <div class="form-row">
    <label for="node-input-o_max"><i class="fa fa-tag"></i> Process Max</label>
    <input type="number" id="node-input-o_max" placeholder="1" />
  </div>

  <!-- Simulator Checkbox -->
  <div class="form-row">
    <label for="node-input-simulator"
      ><i class="fa fa-cog"></i> Simulator</label
    >
    <input
      type="checkbox"
      id="node-input-simulator"
      style="width:20px; vertical-align:baseline;"
    />
    <span>Activate internal simulation?</span>
  </div>

  <!-- Smoothing Method -->
  <div class="form-row">
    <label for="node-input-smooth_method"
      ><i class="fa fa-line-chart"></i> Smoothing</label
    >
    <select id="node-input-smooth_method" style="width:60%;">
      <!-- Filled dynamically from measurementConfig.json or fallback -->
    </select>
  </div>

  <!-- Smoothing Window -->
  <div class="form-row">
    <label for="node-input-count">Window</label>
    <input
      type="number"
      id="node-input-count"
      placeholder="10"
      style="width:60px;"
    />
    <div class="form-tips">Number of samples for smoothing</div>
  </div>

  <!-- Optional Extended Fields: supplier, type, subType, model -->
  <hr />
  <div class="form-row">
    <label for="node-input-supplier"
      ><i class="fa fa-industry"></i> Supplier</label
    >
    <select id="node-input-supplier" style="width:60%;">
      <option value="">(optional)</option>
    </select>
  </div>
  <div class="form-row">
    <label for="node-input-subType"
      ><i class="fa fa-puzzle-piece"></i> SubType</label
    >
    <select id="node-input-subType" style="width:60%;">
      <option value="">(optional)</option>
    </select>
  </div>
  <div class="form-row">
    <label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
    <select id="node-input-model" style="width:60%;">
      <option value="">(optional)</option>
    </select>
  </div>
  <div class="form-row">
    <label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
    <select id="node-input-unit" style="width:60%;"></select>
  </div>

  <hr />

  <!-- loglevel checkbox -->
  <div class="form-row">
    <label for="node-input-enableLog"
      ><i class="fa fa-cog"></i> Enable Log</label
    >
    <input
      type="checkbox"
      id="node-input-enableLog"
      style="width:20px; vertical-align:baseline;"
    />
    <span>Enable logging</span>
  </div>

<div class="form-row" id="row-logLevel">
  <label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
  <select id="node-input-logLevel" style="width:60%;">
    <option value="info">Info</option>
    <option value="debug">Debug</option>
    <option value="warn">Warn</option>
    <option value="error">Error</option>
  </select>
</div>

</script>

<script type="text/html" data-help-name="measurement">
  <p>
    <b>Measurement Node</b>: Scales, smooths, and simulates measurement data.
  </p>
  <ul>
    <li><b>Scaling:</b> Enable to map Source Min/Max → Process Min/Max.</li>
    <li><b>Offset:</b> Adds a fixed offset to your input data.</li>
    <li>
      <b>Simulator:</b> Generate pseudo-random input without a real sensor.
    </li>
    <li>
      <b>Smoothing:</b> Choose a method and a window size for averaging or
      filtering data.
    </li>
    <li>
      <b>Advanced:</b> Optionally specify
      <i>supplier / type / subType / model</i> if relevant to your asset.
    </li>
  </ul>
</script>

```

Perhaps one day I will have some loading text displaying awaiting the fetched data but for now (until tomorrow) this is fine 😛

---

<div class="post-metadata">

### Author: ![GogoVega](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/gogovega/32/71313_2.png) [@GogoVega](https://discourse.nodered.org/u/GogoVega)
#### Post date: [15 January 2025 14:42 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/6 "2025-01-15T14:42:19Z")

</div>

Why do you use `RED.nodes.dirty(true)`?

If you call the code below outside of `oneditsave` the node is not able to detect the change.  
`node[property] = newValue;`

---

<div class="post-metadata">

### Author: ![znetsixe](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/znetsixe/32/72588_2.png) [@znetsixe](https://discourse.nodered.org/u/znetsixe)
#### Post date: [15 January 2025 15:06 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/7 "2025-01-15T15:06:34Z")

</div>

If I dont do this my node doesnt detect the changes. Apparantly for some reason this works even though I also thought it wouldnt. I can test again if I fixed something that was otherwise broken before but before I needed to explicitely tell node red it was changed.

---

<div class="post-metadata">

### Author: ![system](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/1X/d073cd938eafa2e558d7c2cd59003b3ef4963033.png) [@system](https://discourse.nodered.org/u/system)
#### Post date: [29 January 2025 15:07 UTC](https://discourse.nodered.org/t/cascading-dropdowns-from-json-fetches/94639/8 "2025-01-29T15:07:32Z")

</div>

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.
