mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 19:56:42 +00:00
Migrate ha-form to lit (#4000)
* Migrate ha-form to lit * Fix import path * Update * add default, change suffix, fix import * Fix select
This commit is contained in:
parent
035057b185
commit
00f2d36cb5
@ -7,7 +7,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import "../components/ha-form";
|
import "../components/ha-form/ha-form";
|
||||||
import "../components/ha-markdown";
|
import "../components/ha-markdown";
|
||||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||||
import { AuthProvider } from "../data/auth";
|
import { AuthProvider } from "../data/auth";
|
||||||
|
@ -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`
|
|
||||||
<style>
|
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
paper-checkbox {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 22px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<template is="dom-if" if="[[_isArray(schema)]]" restamp="">
|
|
||||||
<template is="dom-if" if="[[error.base]]">
|
|
||||||
<div class="error">[[computeError(error.base, schema)]]</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-repeat" items="[[schema]]">
|
|
||||||
<ha-form
|
|
||||||
data="[[_getValue(data, item)]]"
|
|
||||||
schema="[[item]]"
|
|
||||||
error="[[_getValue(error, item)]]"
|
|
||||||
on-data-changed="_valueChanged"
|
|
||||||
compute-error="[[computeError]]"
|
|
||||||
compute-label="[[computeLabel]]"
|
|
||||||
compute-suffix="[[computeSuffix]]"
|
|
||||||
></ha-form>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!_isArray(schema)]]" restamp="">
|
|
||||||
<template is="dom-if" if="[[error]]">
|
|
||||||
<div class="error">[[computeError(error, schema)]]</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if='[[_equals(schema.type, "string")]]'
|
|
||||||
restamp=""
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if='[[_includes(schema.name, "password")]]'
|
|
||||||
restamp=""
|
|
||||||
>
|
|
||||||
<paper-input
|
|
||||||
type="[[_passwordFieldType(unmaskedPassword)]]"
|
|
||||||
label="[[computeLabel(schema)]]"
|
|
||||||
value="{{data}}"
|
|
||||||
required="[[schema.required]]"
|
|
||||||
auto-validate="[[schema.required]]"
|
|
||||||
error-message="Required"
|
|
||||||
>
|
|
||||||
<paper-icon-button
|
|
||||||
toggles
|
|
||||||
active="{{unmaskedPassword}}"
|
|
||||||
slot="suffix"
|
|
||||||
icon="[[_passwordFieldIcon(unmaskedPassword)]]"
|
|
||||||
id="iconButton"
|
|
||||||
title="Click to toggle between masked and clear password"
|
|
||||||
>
|
|
||||||
</paper-icon-button>
|
|
||||||
</paper-input>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if='[[!_includes(schema.name, "password")]]'
|
|
||||||
restamp=""
|
|
||||||
>
|
|
||||||
<paper-input
|
|
||||||
label="[[computeLabel(schema)]]"
|
|
||||||
value="{{data}}"
|
|
||||||
required="[[schema.required]]"
|
|
||||||
auto-validate="[[schema.required]]"
|
|
||||||
error-message="Required"
|
|
||||||
></paper-input>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if='[[_equals(schema.type, "integer")]]'
|
|
||||||
restamp=""
|
|
||||||
>
|
|
||||||
<template is="dom-if" if="[[_isRange(schema)]]" restamp="">
|
|
||||||
<div>
|
|
||||||
[[computeLabel(schema)]]
|
|
||||||
<ha-paper-slider
|
|
||||||
pin=""
|
|
||||||
value="{{data}}"
|
|
||||||
min="[[schema.valueMin]]"
|
|
||||||
max="[[schema.valueMax]]"
|
|
||||||
></ha-paper-slider>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!_isRange(schema)]]" restamp="">
|
|
||||||
<paper-input
|
|
||||||
label="[[computeLabel(schema)]]"
|
|
||||||
value="{{data}}"
|
|
||||||
type="number"
|
|
||||||
required="[[schema.required]]"
|
|
||||||
auto-validate="[[schema.required]]"
|
|
||||||
error-message="Required"
|
|
||||||
></paper-input>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if='[[_equals(schema.type, "float")]]' restamp="">
|
|
||||||
<!-- TODO -->
|
|
||||||
<paper-input
|
|
||||||
label="[[computeLabel(schema)]]"
|
|
||||||
value="{{data}}"
|
|
||||||
required="[[schema.required]]"
|
|
||||||
auto-validate="[[schema.required]]"
|
|
||||||
error-message="Required"
|
|
||||||
>
|
|
||||||
<span suffix="" slot="suffix">[[computeSuffix(schema)]]</span>
|
|
||||||
</paper-input>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if='[[_equals(schema.type, "boolean")]]'
|
|
||||||
restamp=""
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<paper-checkbox checked="{{data}}"
|
|
||||||
>[[computeLabel(schema)]]</paper-checkbox
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if='[[_equals(schema.type, "select")]]'
|
|
||||||
restamp=""
|
|
||||||
>
|
|
||||||
<paper-dropdown-menu label="[[computeLabel(schema)]]">
|
|
||||||
<paper-listbox
|
|
||||||
slot="dropdown-content"
|
|
||||||
attr-for-selected="item-name"
|
|
||||||
selected="{{data}}"
|
|
||||||
>
|
|
||||||
<template is="dom-repeat" items="[[schema.options]]">
|
|
||||||
<paper-item item-name$="[[_optionValue(item)]]"
|
|
||||||
>[[_optionLabel(item)]]</paper-item
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</paper-listbox>
|
|
||||||
</paper-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
70
src/components/ha-form/ha-form-boolean.ts
Normal file
70
src/components/ha-form/ha-form-boolean.ts
Normal file
@ -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`
|
||||||
|
<paper-checkbox .checked=${this.data} @change=${this._valueChanged}>
|
||||||
|
${this.label}
|
||||||
|
</paper-checkbox>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
65
src/components/ha-form/ha-form-float.ts
Normal file
65
src/components/ha-form/ha-form-float.ts
Normal file
@ -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`
|
||||||
|
<paper-input
|
||||||
|
.label=${this.label}
|
||||||
|
.value=${this.data}
|
||||||
|
.required=${this.schema.required}
|
||||||
|
.autoValidate=${this.schema.required}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
<span suffix="" slot="suffix">${this.suffix}</span>
|
||||||
|
</paper-input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
85
src/components/ha-form/ha-form-integer.ts
Normal file
85
src/components/ha-form/ha-form-integer.ts
Normal file
@ -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`
|
||||||
|
<div>
|
||||||
|
${this.label}
|
||||||
|
<ha-paper-slider
|
||||||
|
pin=""
|
||||||
|
.value=${this.data}
|
||||||
|
.min=${this.schema.valueMin}
|
||||||
|
.max=${this.schema.valueMax}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-paper-slider>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<paper-input
|
||||||
|
type="number"
|
||||||
|
.label=${this.label}
|
||||||
|
.value=${this.data}
|
||||||
|
.required=${this.schema.required}
|
||||||
|
.autoValidate=${this.schema.required}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></paper-input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
78
src/components/ha-form/ha-form-select.ts
Normal file
78
src/components/ha-form/ha-form-select.ts
Normal file
@ -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`
|
||||||
|
<paper-dropdown-menu .label=${this.label}>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
attr-for-selected="item-value"
|
||||||
|
.selected=${this.data}
|
||||||
|
@selected-item-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
${this.schema.options!.map(
|
||||||
|
(item) => html`
|
||||||
|
<paper-item .itemValue=${this._optionValue(item)}>
|
||||||
|
${this._optionLabel(item)}
|
||||||
|
</paper-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
93
src/components/ha-form/ha-form-string.ts
Normal file
93
src/components/ha-form/ha-form-string.ts
Normal file
@ -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`
|
||||||
|
<paper-input
|
||||||
|
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||||
|
.label=${this.label}
|
||||||
|
.value=${this.data}
|
||||||
|
.required=${this.schema.required}
|
||||||
|
.autoValidate=${this.schema.required}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
<paper-icon-button
|
||||||
|
toggles
|
||||||
|
.active=${this._unmaskedPassword}
|
||||||
|
slot="suffix"
|
||||||
|
.icon=${this._unmaskedPassword ? "hass:eye-off" : "hass:eye"}
|
||||||
|
id="iconButton"
|
||||||
|
title="Click to toggle between masked and clear password"
|
||||||
|
@click=${this._toggleUnmaskedPassword}
|
||||||
|
>
|
||||||
|
</paper-icon-button>
|
||||||
|
</paper-input>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<paper-input
|
||||||
|
.label=${this.label}
|
||||||
|
.value=${this.data}
|
||||||
|
.required=${this.schema.required}
|
||||||
|
.autoValidate=${this.schema.required}
|
||||||
|
error-message="Required"
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></paper-input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
225
src/components/ha-form/ha-form.ts
Normal file
225
src/components/ha-form/ha-form.ts
Normal file
@ -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`
|
||||||
|
<div class="error">
|
||||||
|
${this._computeError(this.error.base, this.schema)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this.schema.map(
|
||||||
|
(item) => html`
|
||||||
|
<ha-form
|
||||||
|
.data=${this._getValue(this.data, item)}
|
||||||
|
.schema=${item}
|
||||||
|
.error=${this._getValue(this.error, item)}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
.computeError=${this.computeError}
|
||||||
|
.computeLabel=${this.computeLabel}
|
||||||
|
.computeSuffix=${this.computeSuffix}
|
||||||
|
></ha-form>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this.error
|
||||||
|
? html`
|
||||||
|
<div class="error">
|
||||||
|
${this._computeError(this.error, this.schema)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div id="element" @value-changed=${this._valueChanged}></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
|
|||||||
import "@polymer/paper-spinner/paper-spinner";
|
import "@polymer/paper-spinner/paper-spinner";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
|
||||||
import "../../components/ha-form";
|
import "../../components/ha-form/ha-form";
|
||||||
import "../../components/ha-markdown";
|
import "../../components/ha-markdown";
|
||||||
import "../../resources/ha-style";
|
import "../../resources/ha-style";
|
||||||
import "../../components/dialog/ha-paper-dialog";
|
import "../../components/dialog/ha-paper-dialog";
|
||||||
|
@ -12,10 +12,9 @@ import "@material/mwc-button";
|
|||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
import "@polymer/paper-spinner/paper-spinner";
|
import "@polymer/paper-spinner/paper-spinner";
|
||||||
|
|
||||||
import "../../components/ha-form";
|
import "../../components/ha-form/ha-form";
|
||||||
import "../../components/ha-markdown";
|
import "../../components/ha-markdown";
|
||||||
import "../../resources/ha-style";
|
import "../../resources/ha-style";
|
||||||
import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types";
|
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { configFlowContentStyles } from "./styles";
|
import { configFlowContentStyles } from "./styles";
|
||||||
@ -69,7 +68,7 @@ class StepFlowForm extends LitElement {
|
|||||||
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
|
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
|
||||||
<ha-form
|
<ha-form
|
||||||
.data=${stepData}
|
.data=${stepData}
|
||||||
@data-changed=${this._stepDataChanged}
|
@value-changed=${this._stepDataChanged}
|
||||||
.schema=${step.data_schema}
|
.schema=${step.data_schema}
|
||||||
.error=${step.errors}
|
.error=${step.errors}
|
||||||
.computeLabel=${this._labelCallback}
|
.computeLabel=${this._labelCallback}
|
||||||
@ -169,8 +168,8 @@ class StepFlowForm extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _stepDataChanged(ev: PolymerChangedEvent<any>): void {
|
private _stepDataChanged(ev: CustomEvent): void {
|
||||||
this._stepData = applyPolymerEvent(ev, this._stepData);
|
this._stepData = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _labelCallback = (field: FieldSchema): string =>
|
private _labelCallback = (field: FieldSchema): string =>
|
||||||
|
@ -2,7 +2,7 @@ import { h, Component } from "preact";
|
|||||||
|
|
||||||
import "../../../../components/device/ha-device-picker";
|
import "../../../../components/device/ha-device-picker";
|
||||||
import "../../../../components/device/ha-device-condition-picker";
|
import "../../../../components/device/ha-device-condition-picker";
|
||||||
import "../../../../components/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchDeviceConditionCapabilities,
|
fetchDeviceConditionCapabilities,
|
||||||
@ -64,9 +64,9 @@ export default class DeviceCondition extends Component<any, any> {
|
|||||||
{extraFieldsData && (
|
{extraFieldsData && (
|
||||||
<ha-form
|
<ha-form
|
||||||
data={Object.assign({}, ...extraFieldsData)}
|
data={Object.assign({}, ...extraFieldsData)}
|
||||||
onData-changed={this._extraFieldsChanged}
|
|
||||||
schema={this.state.capabilities.extra_fields}
|
schema={this.state.capabilities.extra_fields}
|
||||||
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
|
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
|
||||||
|
onvalue-changed={this._extraFieldsChanged}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -98,15 +98,9 @@ export default class DeviceCondition extends Component<any, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _extraFieldsChanged(ev) {
|
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.onChange(this.props.index, {
|
||||||
...this.props.condition,
|
...this.props.condition,
|
||||||
[item]: value,
|
...ev.detail.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { h, Component } from "preact";
|
|||||||
|
|
||||||
import "../../../../components/device/ha-device-picker";
|
import "../../../../components/device/ha-device-picker";
|
||||||
import "../../../../components/device/ha-device-action-picker";
|
import "../../../../components/device/ha-device-action-picker";
|
||||||
import "../../../../components/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchDeviceActionCapabilities,
|
fetchDeviceActionCapabilities,
|
||||||
@ -117,15 +117,9 @@ export default class DeviceActionEditor extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _extraFieldsChanged(ev) {
|
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.onChange(this.props.index, {
|
||||||
...this.props.action,
|
...this.props.action,
|
||||||
[item]: value,
|
...ev.detail.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { h, Component } from "preact";
|
|||||||
|
|
||||||
import "../../../../components/device/ha-device-picker";
|
import "../../../../components/device/ha-device-picker";
|
||||||
import "../../../../components/device/ha-device-trigger-picker";
|
import "../../../../components/device/ha-device-trigger-picker";
|
||||||
import "../../../../components/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchDeviceTriggerCapabilities,
|
fetchDeviceTriggerCapabilities,
|
||||||
@ -65,9 +65,9 @@ export default class DeviceTrigger extends Component<any, any> {
|
|||||||
{extraFieldsData && (
|
{extraFieldsData && (
|
||||||
<ha-form
|
<ha-form
|
||||||
data={Object.assign({}, ...extraFieldsData)}
|
data={Object.assign({}, ...extraFieldsData)}
|
||||||
onData-changed={this._extraFieldsChanged}
|
|
||||||
schema={this.state.capabilities.extra_fields}
|
schema={this.state.capabilities.extra_fields}
|
||||||
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
|
computeLabel={this._extraFieldsComputeLabelCallback(hass.localize)}
|
||||||
|
onvalue-changed={this._extraFieldsChanged}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -99,15 +99,9 @@ export default class DeviceTrigger extends Component<any, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _extraFieldsChanged(ev) {
|
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.onChange(this.props.index, {
|
||||||
...this.props.trigger,
|
...this.props.trigger,
|
||||||
[item]: value,
|
...ev.detail.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../../components/dialog/ha-paper-dialog";
|
import "../../components/dialog/ha-paper-dialog";
|
||||||
import "../../components/ha-form";
|
import "../../components/ha-form/ha-form";
|
||||||
import "../../components/ha-markdown";
|
import "../../components/ha-markdown";
|
||||||
import "../../resources/ha-style";
|
import "../../resources/ha-style";
|
||||||
|
|
||||||
|
@ -33,7 +33,9 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
|
|
||||||
--scrollbar-thumb-color: rgb(194, 194, 194);
|
--scrollbar-thumb-color: rgb(194, 194, 194);
|
||||||
|
|
||||||
--error-state-color: #db4437;
|
|
||||||
|
--error-color: #db4437;
|
||||||
|
--error-state-color: var(--error-color);
|
||||||
|
|
||||||
/* states and badges */
|
/* states and badges */
|
||||||
--state-icon-color: #44739e;
|
--state-icon-color: #44739e;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user