diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index 4799fbc155..5075c3963f 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -7,7 +7,7 @@ import { css, } from "lit-element"; import "@material/mwc-button"; -import "../components/ha-form"; +import "../components/ha-form/ha-form"; import "../components/ha-markdown"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { AuthProvider } from "../data/auth"; diff --git a/src/components/ha-form.js b/src/components/ha-form.js deleted file mode 100644 index f6dfe613ee..0000000000 --- a/src/components/ha-form.js +++ /dev/null @@ -1,265 +0,0 @@ -import "@polymer/paper-checkbox/paper-checkbox"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-icon-button/paper-icon-button"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "./ha-paper-slider"; -import { EventsMixin } from "../mixins/events-mixin"; - -/* - * @appliesMixin EventsMixin - */ -class HaForm extends EventsMixin(PolymerElement) { - static get template() { - return html` - - - - `; - } - - static get properties() { - return { - data: { - type: Object, - notify: true, - }, - schema: Object, - error: Object, - - // A function that computes the label to be displayed for a given - // schema object. - computeLabel: { - type: Function, - value: () => (schema) => schema && schema.name, - }, - - // A function that computes the suffix to be displayed for a given - // schema object. - computeSuffix: { - type: Function, - value: () => (schema) => - schema && - schema.description && - schema.description.unit_of_measurement, - }, - - // A function that computes an error message to be displayed for a - // given error ID, and relevant schema object - computeError: { - type: Function, - value: () => (error, schema) => error, // eslint-disable-line no-unused-vars - }, - }; - } - - focus() { - const input = this.shadowRoot.querySelector( - "ha-form, paper-input, ha-paper-slider, paper-checkbox, paper-dropdown-menu" - ); - - if (!input) { - return; - } - - input.focus(); - } - - _isArray(val) { - return Array.isArray(val); - } - - _isRange(schema) { - return "valueMin" in schema && "valueMax" in schema; - } - - _equals(a, b) { - return a === b; - } - - _includes(a, b) { - return a.indexOf(b) >= 0; - } - - _getValue(obj, item) { - if (obj) { - return obj[item.name]; - } - return null; - } - - _valueChanged(ev) { - let value = ev.detail.value; - if (ev.model.item.type === "integer") { - value = Number(ev.detail.value); - } - this.set(["data", ev.model.item.name], value); - } - - _passwordFieldType(unmaskedPassword) { - return unmaskedPassword ? "text" : "password"; - } - - _passwordFieldIcon(unmaskedPassword) { - return unmaskedPassword ? "hass:eye-off" : "hass:eye"; - } - - _optionValue(item) { - return Array.isArray(item) ? item[0] : item; - } - - _optionLabel(item) { - return Array.isArray(item) ? item[1] : item; - } -} - -customElements.define("ha-form", HaForm); diff --git a/src/components/ha-form/ha-form-boolean.ts b/src/components/ha-form/ha-form-boolean.ts new file mode 100644 index 0000000000..5921cd3e43 --- /dev/null +++ b/src/components/ha-form/ha-form-boolean.ts @@ -0,0 +1,70 @@ +import { + customElement, + LitElement, + html, + property, + TemplateResult, + CSSResult, + css, + query, +} from "lit-element"; +import { + HaFormElement, + HaFormBooleanData, + HaFormBooleanSchema, +} from "./ha-form"; +import { fireEvent } from "../../common/dom/fire_event"; + +import "@polymer/paper-checkbox/paper-checkbox"; +// Not duplicate, is for typing +// tslint:disable-next-line +import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; + +@customElement("ha-form-boolean") +export class HaFormBoolean extends LitElement implements HaFormElement { + @property() public schema!: HaFormBooleanSchema; + @property() public data!: HaFormBooleanData; + @property() public label!: string; + @property() public suffix!: string; + @query("paper-checkbox") private _input?: HTMLElement; + + public focus() { + if (this._input) { + this._input.focus(); + } + } + + protected render(): TemplateResult { + return html` + + ${this.label} + + `; + } + + private _valueChanged(ev: Event) { + fireEvent( + this, + "value-changed", + { + value: (ev.target as PaperCheckboxElement).checked, + }, + { bubbles: false } + ); + } + + static get styles(): CSSResult { + return css` + paper-checkbox { + display: inline-block; + padding: 22px 0; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-boolean": HaFormBoolean; + } +} diff --git a/src/components/ha-form/ha-form-float.ts b/src/components/ha-form/ha-form-float.ts new file mode 100644 index 0000000000..0d2279ea72 --- /dev/null +++ b/src/components/ha-form/ha-form-float.ts @@ -0,0 +1,65 @@ +import { + customElement, + LitElement, + html, + property, + TemplateResult, + query, +} from "lit-element"; +import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./ha-form"; +import { fireEvent } from "../../common/dom/fire_event"; + +import "@polymer/paper-input/paper-input"; +// Not duplicate, is for typing +// tslint:disable-next-line +import { PaperInputElement } from "@polymer/paper-input/paper-input"; + +@customElement("ha-form-float") +export class HaFormFloat extends LitElement implements HaFormElement { + @property() public schema!: HaFormFloatSchema; + @property() public data!: HaFormFloatData; + @property() public label!: string; + @property() public suffix!: string; + @query("paper-input") private _input?: HTMLElement; + + public focus() { + if (this._input) { + this._input.focus(); + } + } + + protected render(): TemplateResult { + return html` + + ${this.suffix} + + `; + } + + private _valueChanged(ev: Event) { + const value = Number((ev.target as PaperInputElement).value); + if (this.data === value) { + return; + } + fireEvent( + this, + "value-changed", + { + value, + }, + { bubbles: false } + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-float": HaFormFloat; + } +} diff --git a/src/components/ha-form/ha-form-integer.ts b/src/components/ha-form/ha-form-integer.ts new file mode 100644 index 0000000000..4b38678e52 --- /dev/null +++ b/src/components/ha-form/ha-form-integer.ts @@ -0,0 +1,85 @@ +import { + customElement, + LitElement, + html, + property, + TemplateResult, + query, +} from "lit-element"; +import { + HaFormElement, + HaFormIntegerData, + HaFormIntegerSchema, +} from "./ha-form"; +import { fireEvent } from "../../common/dom/fire_event"; + +import "../ha-paper-slider"; +import "@polymer/paper-input/paper-input"; +// Not duplicate, is for typing +// tslint:disable-next-line +import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import { PaperSliderElement } from "@polymer/paper-slider/paper-slider"; + +@customElement("ha-form-integer") +export class HaFormInteger extends LitElement implements HaFormElement { + @property() public schema!: HaFormIntegerSchema; + @property() public data!: HaFormIntegerData; + @property() public label!: string; + @property() public suffix!: string; + @query("paper-input ha-paper-slider") private _input?: HTMLElement; + + public focus() { + if (this._input) { + this._input.focus(); + } + } + + protected render(): TemplateResult { + return "valueMin" in this.schema && "valueMax" in this.schema + ? html` +
+ ${this.label} + +
+ ` + : html` + + `; + } + + private _valueChanged(ev: Event) { + const value = Number( + (ev.target as PaperInputElement | PaperSliderElement).value + ); + if (this.data === value) { + return; + } + fireEvent( + this, + "value-changed", + { + value, + }, + { bubbles: false } + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-integer": HaFormInteger; + } +} diff --git a/src/components/ha-form/ha-form-select.ts b/src/components/ha-form/ha-form-select.ts new file mode 100644 index 0000000000..25a13abd23 --- /dev/null +++ b/src/components/ha-form/ha-form-select.ts @@ -0,0 +1,78 @@ +import { + customElement, + LitElement, + html, + property, + TemplateResult, + query, +} from "lit-element"; +import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form"; +import { fireEvent } from "../../common/dom/fire_event"; + +import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; +import "@polymer/paper-listbox/paper-listbox"; +import "@polymer/paper-item/paper-item"; + +@customElement("ha-form-select") +export class HaFormSelect extends LitElement implements HaFormElement { + @property() public schema!: HaFormSelectSchema; + @property() public data!: HaFormSelectData; + @property() public label!: string; + @property() public suffix!: string; + @query("paper-dropdown-menu") private _input?: HTMLElement; + + public focus() { + if (this._input) { + this._input.focus(); + } + } + + protected render(): TemplateResult { + return html` + + + ${this.schema.options!.map( + (item) => html` + + ${this._optionLabel(item)} + + ` + )} + + + `; + } + + private _optionValue(item) { + return Array.isArray(item) ? item[0] : item; + } + + private _optionLabel(item) { + return Array.isArray(item) ? item[1] : item; + } + + private _valueChanged(ev: CustomEvent) { + if (!ev.detail.value) { + return; + } + fireEvent( + this, + "value-changed", + { + value: ev.detail.value.itemValue, + }, + { bubbles: false } + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-select": HaFormSelect; + } +} diff --git a/src/components/ha-form/ha-form-string.ts b/src/components/ha-form/ha-form-string.ts new file mode 100644 index 0000000000..82a3db8131 --- /dev/null +++ b/src/components/ha-form/ha-form-string.ts @@ -0,0 +1,93 @@ +import { + customElement, + LitElement, + html, + property, + TemplateResult, + query, +} from "lit-element"; + +import { HaFormElement, HaFormStringData, HaFormStringSchema } from "./ha-form"; +import { fireEvent } from "../../common/dom/fire_event"; + +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-icon-button/paper-icon-button"; +// Not duplicate, is for typing +// tslint:disable-next-line +import { PaperInputElement } from "@polymer/paper-input/paper-input"; + +@customElement("ha-form-string") +export class HaFormString extends LitElement implements HaFormElement { + @property() public schema!: HaFormStringSchema; + @property() public data!: HaFormStringData; + @property() public label!: string; + @property() public suffix!: string; + @property() private _unmaskedPassword = false; + @query("paper-input") private _input?: HTMLElement; + + public focus() { + if (this._input) { + this._input.focus(); + } + } + + protected render(): TemplateResult { + return this.schema.name.includes("password") + ? html` + + + + + ` + : html` + + `; + } + + private _toggleUnmaskedPassword(ev: Event) { + this._unmaskedPassword = (ev.target as any).active; + } + + private _valueChanged(ev: Event) { + const value = (ev.target as PaperInputElement).value; + if (this.data === value) { + return; + } + fireEvent( + this, + "value-changed", + { + value, + }, + { bubbles: false } + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-string": HaFormString; + } +} diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts new file mode 100644 index 0000000000..a8fe7d813d --- /dev/null +++ b/src/components/ha-form/ha-form.ts @@ -0,0 +1,225 @@ +import { + customElement, + LitElement, + html, + property, + query, + CSSResult, + css, + PropertyValues, +} from "lit-element"; + +import "./ha-form-string"; +import "./ha-form-integer"; +import "./ha-form-float"; +import "./ha-form-boolean"; +import "./ha-form-select"; +import { fireEvent } from "../../common/dom/fire_event"; + +export type HaFormSchema = + | HaFormStringSchema + | HaFormIntegerSchema + | HaFormFloatSchema + | HaFormBooleanSchema + | HaFormSelectSchema; + +export interface HaFormBaseSchema { + name: string; + default?: HaFormData; + required?: boolean; + optional?: boolean; + description?: { suffix?: string }; +} + +export interface HaFormIntegerSchema extends HaFormBaseSchema { + type: "integer"; + default?: HaFormIntegerData; + valueMin?: number; + valueMax?: number; +} + +export interface HaFormSelectSchema extends HaFormBaseSchema { + type: "select"; + options?: string[]; +} + +export interface HaFormFloatSchema extends HaFormBaseSchema { + type: "float"; +} + +export interface HaFormStringSchema extends HaFormBaseSchema { + type: "string"; +} + +export interface HaFormBooleanSchema extends HaFormBaseSchema { + type: "boolean"; +} + +export interface HaFormDataContainer { + [key: string]: HaFormData; +} + +export type HaFormData = + | HaFormStringData + | HaFormIntegerData + | HaFormFloatData + | HaFormBooleanData + | HaFormSelectData; + +export type HaFormStringData = string; +export type HaFormIntegerData = number; +export type HaFormFloatData = number; +export type HaFormBooleanData = boolean; +export type HaFormSelectData = string; + +export interface HaFormElement extends LitElement { + schema: HaFormSchema; + data: HaFormDataContainer | HaFormData; + label?: string; + suffix?: string; +} + +@customElement("ha-form") +export class HaForm extends LitElement implements HaFormElement { + @property() public data!: HaFormDataContainer | HaFormData; + @property() public schema!: HaFormSchema; + @property() public error; + @property() public computeError?: (schema: HaFormSchema, error) => string; + @property() public computeLabel?: (schema: HaFormSchema) => string; + @property() public computeSuffix?: (schema: HaFormSchema) => string; + @query("ha-form") private _childForm?: HaForm; + @query("#element") private _elementContainer?: HTMLDivElement; + + public focus() { + const input = this._childForm + ? this._childForm + : this._elementContainer + ? this._elementContainer.lastChild + : undefined; + + if (!input) { + return; + } + + (input as HTMLElement).focus(); + } + + protected render() { + if (Array.isArray(this.schema)) { + return html` + ${this.error && this.error.base + ? html` +
+ ${this._computeError(this.error.base, this.schema)} +
+ ` + : ""} + ${this.schema.map( + (item) => html` + + ` + )} + `; + } + + return html` + ${this.error + ? html` +
+ ${this._computeError(this.error, this.schema)} +
+ ` + : ""} +
+ `; + } + + protected updated(changedProperties: PropertyValues) { + const schemaChanged = changedProperties.has("schema"); + const oldSchema = schemaChanged + ? changedProperties.get("schema") + : undefined; + if ( + !Array.isArray(this.schema) && + schemaChanged && + (!oldSchema || (oldSchema as HaFormSchema).type !== this.schema.type) + ) { + const element = document.createElement( + `ha-form-${this.schema.type}` + ) as HaFormElement; + element.schema = this.schema; + element.data = this.data; + element.label = this._computeLabel(this.schema); + element.suffix = this._computeSuffix(this.schema); + if (this._elementContainer!.lastChild) { + this._elementContainer!.removeChild(this._elementContainer!.lastChild); + } + this._elementContainer!.append(element); + } else if (this._elementContainer && this._elementContainer.lastChild) { + const element = this._elementContainer!.lastChild as HaFormElement; + element.schema = this.schema; + element.data = this.data; + element.label = this._computeLabel(this.schema); + element.suffix = this._computeSuffix(this.schema); + } + } + + private _computeLabel(schema: HaFormSchema) { + return this.computeLabel + ? this.computeLabel(schema) + : schema + ? schema.name + : ""; + } + + private _computeSuffix(schema: HaFormSchema) { + return this.computeSuffix + ? this.computeSuffix(schema) + : schema && schema.description + ? schema.description.suffix + : ""; + } + + private _computeError(error, schema: HaFormSchema) { + return this.computeError ? this.computeError(error, schema) : error; + } + + private _getValue(obj, item) { + if (obj) { + return obj[item.name]; + } + return null; + } + + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const schema = (ev.target as HaFormElement).schema; + const data = this.data as HaFormDataContainer; + data[schema.name] = ev.detail.value; + fireEvent(this, "value-changed", { + value: { ...data }, + }); + } + + static get styles(): CSSResult { + return css` + .error { + color: var(--error-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form": HaForm; + } +} diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index c10900e340..a85f3b11bd 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -14,7 +14,7 @@ import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-spinner/paper-spinner"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import "../../components/ha-form"; +import "../../components/ha-form/ha-form"; import "../../components/ha-markdown"; import "../../resources/ha-style"; import "../../components/dialog/ha-paper-dialog"; diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 4eb73f5c02..3b3d2fbad3 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -12,10 +12,9 @@ import "@material/mwc-button"; import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-spinner/paper-spinner"; -import "../../components/ha-form"; +import "../../components/ha-form/ha-form"; import "../../components/ha-markdown"; import "../../resources/ha-style"; -import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import { fireEvent } from "../../common/dom/fire_event"; import { configFlowContentStyles } from "./styles"; @@ -69,7 +68,7 @@ class StepFlowForm extends LitElement { ${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} ): void { - this._stepData = applyPolymerEvent(ev, this._stepData); + private _stepDataChanged(ev: CustomEvent): void { + this._stepData = ev.detail.value; } private _labelCallback = (field: FieldSchema): string => diff --git a/src/panels/config/js/condition/device.tsx b/src/panels/config/js/condition/device.tsx index 1fd9a8385e..17032c479d 100644 --- a/src/panels/config/js/condition/device.tsx +++ b/src/panels/config/js/condition/device.tsx @@ -2,7 +2,7 @@ import { h, Component } from "preact"; import "../../../../components/device/ha-device-picker"; import "../../../../components/device/ha-device-condition-picker"; -import "../../../../components/ha-form"; +import "../../../../components/ha-form/ha-form"; import { fetchDeviceConditionCapabilities, @@ -64,9 +64,9 @@ export default class DeviceCondition extends Component { {extraFieldsData && ( )} @@ -98,15 +98,9 @@ export default class DeviceCondition extends Component { } private _extraFieldsChanged(ev) { - if (!ev.detail.path) { - return; - } - const item = ev.detail.path.replace("data.", ""); - const value = ev.detail.value || undefined; - this.props.onChange(this.props.index, { ...this.props.condition, - [item]: value, + ...ev.detail.value, }); } diff --git a/src/panels/config/js/script/device.tsx b/src/panels/config/js/script/device.tsx index 344ff96253..3ae41ba5a8 100644 --- a/src/panels/config/js/script/device.tsx +++ b/src/panels/config/js/script/device.tsx @@ -2,7 +2,7 @@ import { h, Component } from "preact"; import "../../../../components/device/ha-device-picker"; import "../../../../components/device/ha-device-action-picker"; -import "../../../../components/ha-form"; +import "../../../../components/ha-form/ha-form"; import { fetchDeviceActionCapabilities, @@ -117,15 +117,9 @@ export default class DeviceActionEditor extends Component< } private _extraFieldsChanged(ev) { - if (!ev.detail.path) { - return; - } - const item = ev.detail.path.replace("data.", ""); - const value = ev.detail.value || undefined; - this.props.onChange(this.props.index, { ...this.props.action, - [item]: value, + ...ev.detail.value, }); } diff --git a/src/panels/config/js/trigger/device.tsx b/src/panels/config/js/trigger/device.tsx index a9dd929ac1..91d6a8259f 100644 --- a/src/panels/config/js/trigger/device.tsx +++ b/src/panels/config/js/trigger/device.tsx @@ -2,7 +2,7 @@ import { h, Component } from "preact"; import "../../../../components/device/ha-device-picker"; import "../../../../components/device/ha-device-trigger-picker"; -import "../../../../components/ha-form"; +import "../../../../components/ha-form/ha-form"; import { fetchDeviceTriggerCapabilities, @@ -65,9 +65,9 @@ export default class DeviceTrigger extends Component { {extraFieldsData && ( )} @@ -99,15 +99,9 @@ export default class DeviceTrigger extends Component { } private _extraFieldsChanged(ev) { - if (!ev.detail.path) { - return; - } - const item = ev.detail.path.replace("data.", ""); - const value = ev.detail.value || undefined; - this.props.onChange(this.props.index, { ...this.props.trigger, - [item]: value, + ...ev.detail.value, }); } diff --git a/src/panels/profile/ha-mfa-module-setup-flow.js b/src/panels/profile/ha-mfa-module-setup-flow.js index df25be0133..79394d8196 100644 --- a/src/panels/profile/ha-mfa-module-setup-flow.js +++ b/src/panels/profile/ha-mfa-module-setup-flow.js @@ -5,7 +5,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../components/dialog/ha-paper-dialog"; -import "../../components/ha-form"; +import "../../components/ha-form/ha-form"; import "../../components/ha-markdown"; import "../../resources/ha-style"; diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index c35ab84f64..88b9552598 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -33,7 +33,9 @@ documentContainer.innerHTML = ` --scrollbar-thumb-color: rgb(194, 194, 194); - --error-state-color: #db4437; + + --error-color: #db4437; + --error-state-color: var(--error-color); /* states and badges */ --state-icon-color: #44739e;