Compare commits

...

2 Commits

Author SHA1 Message Date
Paul Bottein 71e831a604 Resolve hidden fields until stable so they hold no value 2026-07-20 11:03:24 +02:00
Paul Bottein 8f48fdba12 Skip hidden fields in config flow submission 2026-07-17 15:55:05 +02:00
5 changed files with 82 additions and 12 deletions
+23
View File
@@ -71,3 +71,26 @@ export const isFieldHidden = (
const conditions = Array.isArray(hidden) ? hidden : [hidden];
return conditions.every((condition) => evaluateCondition(condition, data));
};
// A hidden field holds no value, so it reads as absent to the conditions of the
// other fields. Visibility is resolved until it is stable.
export const getHiddenFields = (
schema: readonly HaFormSchema[],
data: HaFormDataContainer | undefined
): Set<string> => {
const hidden = new Set<string>();
const evalData: HaFormDataContainer = { ...(data ?? {}) };
let changed = true;
while (changed) {
changed = false;
for (const field of schema) {
if (hidden.has(field.name) || !isFieldHidden(field, evalData)) {
continue;
}
hidden.add(field.name);
delete evalData[field.name];
changed = true;
}
}
return hidden;
};
+4 -2
View File
@@ -2,7 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, queryAll } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import { isFieldHidden } from "./conditions";
import { getHiddenFields } from "./conditions";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
@@ -68,9 +68,11 @@ export class HaFormGrid extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
const hiddenFields = getHiddenFields(this.schema.schema, this.data);
return html`
${this.schema.schema
.filter((item) => !isFieldHidden(item, this.data))
.filter((item) => !hiddenFields.has(item.name))
.map(
(item) => html`
<ha-form
+6 -3
View File
@@ -6,7 +6,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-selector/ha-selector";
import { isFieldHidden } from "./conditions";
import { getHiddenFields } from "./conditions";
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
const LOAD_ELEMENTS = {
@@ -99,8 +99,9 @@ export class HaForm extends LitElement implements HaFormElement {
let isValid = true;
let firstInvalidElement: HTMLElement | undefined;
const hiddenFields = getHiddenFields(this.schema, this.data);
const visibleSchema = this.schema.filter(
(item) => !isFieldHidden(item, this.data)
(item) => !hiddenFields.has(item.name)
);
visibleSchema.forEach((item, index) => {
@@ -157,6 +158,8 @@ export class HaForm extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
const renderHiddenFields = getHiddenFields(this.schema, this.data);
return html`
<div class="root" part="root">
${
@@ -169,7 +172,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""
}
${this.schema.map((item) => {
if (isFieldHidden(item, this.data)) {
if (renderHiddenFields.has(item.name)) {
return nothing;
}
+16 -6
View File
@@ -9,6 +9,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { isNavigationClick } from "../../common/dom/is-navigation-click";
import "../../components/ha-alert";
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
import { getHiddenFields } from "../../components/ha-form/conditions";
import "../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -226,14 +227,17 @@ class StepFlowForm extends LitElement {
const checkAllRequiredFields = (
schema: readonly HaFormSchema[],
data: Record<string, any>
) =>
schema.every(
) => {
const hidden = getHiddenFields(schema, data);
return schema.every(
(field) =>
(!field.required || !["", undefined].includes(data[field.name])) &&
(field.type !== "expandable" ||
(!field.required && data[field.name] === undefined) ||
checkAllRequiredFields(field.schema, data[field.name]))
hidden.has(field.name) ||
((!field.required || !["", undefined].includes(data[field.name])) &&
(field.type !== "expandable" ||
(!field.required && data[field.name] === undefined) ||
checkAllRequiredFields(field.schema, data[field.name])))
);
};
const allRequiredInfoFilledIn =
stepData === undefined
@@ -255,8 +259,14 @@ class StepFlowForm extends LitElement {
const flowId = this.step.flow_id;
const hiddenFields = getHiddenFields(this.step.data_schema, stepData);
const toSendData: Record<string, unknown> = {};
Object.keys(stepData).forEach((key) => {
if (hiddenFields.has(key)) {
// Hidden fields are not part of the submitted config
return;
}
const value = stepData[key];
const isEmpty = [undefined, ""].includes(value);
const field = this.step.data_schema?.find((f) => f.name === key);
+33 -1
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { isFieldHidden } from "../../../src/components/ha-form/conditions";
import {
getHiddenFields,
isFieldHidden,
} from "../../../src/components/ha-form/conditions";
import type { HaFormSchema } from "../../../src/components/ha-form/types";
const field = (hidden: HaFormSchema["hidden"]): HaFormSchema =>
@@ -132,3 +135,32 @@ describe("isFieldHidden", () => {
);
});
});
describe("getHiddenFields", () => {
const named = (name: string, hidden?: HaFormSchema["hidden"]): HaFormSchema =>
({ name, selector: { text: {} }, hidden }) as HaFormSchema;
it("collects the fields whose condition matches", () => {
const schema = [
named("mode"),
named("token", { field: "mode", value: "simple" }),
];
expect([...getHiddenFields(schema, { mode: "simple" })]).toEqual(["token"]);
expect([...getHiddenFields(schema, { mode: "advanced" })]).toEqual([]);
});
it("treats a hidden field as absent to the other conditions", () => {
// "token" is hidden, so its value must not satisfy the condition on "extra"
const schema = [
named("mode"),
named("token", { field: "mode", value: "simple" }),
named("extra", { field: "token", operator: "not_exists" }),
];
expect([
...getHiddenFields(schema, { mode: "simple", token: "stale" }),
]).toEqual(["token", "extra"]);
expect([
...getHiddenFields(schema, { mode: "advanced", token: "kept" }),
]).toEqual([]);
});
});