From 6432207bf1cb651d6f7a780a1ff02094a0ef650f Mon Sep 17 00:00:00 2001 From: Ian Richardson Date: Tue, 6 Nov 2018 03:47:24 -0600 Subject: [PATCH] New Card: Shopping List (#1970) * New Card: Shopping List Following features: - Add item - Edit item - Complete item - Clear items * Address Travis complaint * Addressed review comments * Update translation variable name * Line up input row text * Taking MVP to heart Addressed review comments and scaled this back to just get a simple shopping list card out there and we can discuss/debate how best to add the additional pieces with smaller PRs * Remove calling connected in set hass --- gallery/src/data/provide_hass.js | 6 + .../src/demos/demo-hui-shopping-list-card.js | 55 ++++++ src/data/shopping-list.ts | 28 +++ .../lovelace/cards/hui-shopping-list-card.ts | 165 ++++++++++++++++++ .../lovelace/common/create-card-element.js | 2 + 5 files changed, 256 insertions(+) create mode 100644 gallery/src/demos/demo-hui-shopping-list-card.js create mode 100644 src/data/shopping-list.ts create mode 100644 src/panels/lovelace/cards/hui-shopping-list-card.ts diff --git a/gallery/src/data/provide_hass.js b/gallery/src/data/provide_hass.js index 79db10cbda..8d260a2213 100644 --- a/gallery/src/data/provide_hass.js +++ b/gallery/src/data/provide_hass.js @@ -29,6 +29,12 @@ export default (elements, { initialStates = {} } = {}) => { resources: demoResources, states: initialStates, themes: {}, + connection: { + subscribeEvents: async (callback, event) => { + console.log("subscribeEvents", event); + return () => console.log("unsubscribeEvents", event); + }, + }, // Mock properties mockEntities: entities, diff --git a/gallery/src/demos/demo-hui-shopping-list-card.js b/gallery/src/demos/demo-hui-shopping-list-card.js new file mode 100644 index 0000000000..2f485dfcc0 --- /dev/null +++ b/gallery/src/demos/demo-hui-shopping-list-card.js @@ -0,0 +1,55 @@ +import { html } from "@polymer/polymer/lib/utils/html-tag"; +import { PolymerElement } from "@polymer/polymer/polymer-element"; + +import provideHass from "../data/provide_hass"; +import "../components/demo-cards"; + +const CONFIGS = [ + { + heading: "List example", + config: ` +- type: shopping-list + `, + }, + { + heading: "List with title example", + config: ` +- type: shopping-list + title: Shopping List + `, + }, +]; + +class DemoShoppingListEntity extends PolymerElement { + static get template() { + return html` + + `; + } + + static get properties() { + return { + _configs: { + type: Object, + value: CONFIGS, + }, + }; + } + + ready() { + super.ready(); + const hass = provideHass(this.$.demos); + + hass.mockAPI("shopping_list", () => [ + { name: "list", id: 1, complete: false }, + { name: "all", id: 2, complete: false }, + { name: "the", id: 3, complete: false }, + { name: "things", id: 4, complete: true }, + ]); + } +} + +customElements.define("demo-hui-shopping-list-card", DemoShoppingListEntity); diff --git a/src/data/shopping-list.ts b/src/data/shopping-list.ts new file mode 100644 index 0000000000..9766354e21 --- /dev/null +++ b/src/data/shopping-list.ts @@ -0,0 +1,28 @@ +import { HomeAssistant } from "../types"; + +export interface ShoppingListItem { + id: number; + name: string; + complete: boolean; +} + +export const fetchItems = (hass: HomeAssistant): Promise => + hass.callApi("GET", "shopping_list"); + +export const saveEdit = ( + hass: HomeAssistant, + itemId: number, + name: string +): Promise => + hass.callApi("POST", "shopping_list/item/" + itemId, { + name, + }); + +export const completeItem = ( + hass: HomeAssistant, + itemId: number, + complete: boolean +): Promise => + hass.callApi("POST", "shopping_list/item/" + itemId, { + complete, + }); diff --git a/src/panels/lovelace/cards/hui-shopping-list-card.ts b/src/panels/lovelace/cards/hui-shopping-list-card.ts new file mode 100644 index 0000000000..ac918c856d --- /dev/null +++ b/src/panels/lovelace/cards/hui-shopping-list-card.ts @@ -0,0 +1,165 @@ +import { html, LitElement } from "@polymer/lit-element"; +import { repeat } from "lit-html/directives/repeat"; +import { TemplateResult } from "lit-html"; +import "@polymer/paper-checkbox/paper-checkbox"; +import "@polymer/paper-input/paper-input"; + +import "../../../components/ha-card"; + +import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; +import { HomeAssistant } from "../../../types"; +import { LovelaceCard, LovelaceConfig } from "../types"; +import { + fetchItems, + completeItem, + saveEdit, + ShoppingListItem, +} from "../../../data/shopping-list"; + +interface Config extends LovelaceConfig { + title?: string; +} + +class HuiShoppingListCard extends hassLocalizeLitMixin(LitElement) + implements LovelaceCard { + private _hass?: HomeAssistant; + private _config?: Config; + private _items?: ShoppingListItem[]; + private _unsubEvents?: Promise<() => Promise>; + + static get properties() { + return { + _config: {}, + _items: {}, + }; + } + + set hass(hass: HomeAssistant) { + this._hass = hass; + } + + public getCardSize(): number { + return ( + (this._config ? (this._config.title ? 1 : 0) : 0) + + (this._items ? this._items.length : 3) + ); + } + + public setConfig(config: Config): void { + this._config = config; + this._items = []; + this._fetchData(); + } + + public connectedCallback(): void { + super.connectedCallback(); + + if (this._hass) { + this._unsubEvents = this._hass.connection.subscribeEvents( + () => this._fetchData(), + "shopping_list_updated" + ); + this._fetchData(); + } + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + + if (this._unsubEvents) { + this._unsubEvents.then((unsub) => unsub()); + } + } + + protected render(): TemplateResult { + if (!this._config || !this._hass) { + return html``; + } + + return html` + ${this.renderStyle()} + + ${repeat( + this._items!, + (item) => item.id, + (item, index) => + html` +
+ + + + +
+ ` + )} +
+ `; + } + + private renderStyle(): TemplateResult { + return html` + + `; + } + + private async _fetchData(): Promise { + if (this._hass) { + this._items = await fetchItems(this._hass); + } + } + + private _completeItem(ev): void { + completeItem(this._hass!, ev.target.itemId, ev.target.checked).catch(() => + this._fetchData() + ); + } + + private _saveEdit(ev): void { + saveEdit(this._hass!, ev.target.itemId, ev.target.value).catch(() => + this._fetchData() + ); + + ev.target.blur(); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-shopping-list-card": HuiShoppingListCard; + } +} + +customElements.define("hui-shopping-list-card", HuiShoppingListCard); diff --git a/src/panels/lovelace/common/create-card-element.js b/src/panels/lovelace/common/create-card-element.js index f7cc0db4a3..c4cbc406a2 100644 --- a/src/panels/lovelace/common/create-card-element.js +++ b/src/panels/lovelace/common/create-card-element.js @@ -21,6 +21,7 @@ import "../cards/hui-picture-glance-card"; import "../cards/hui-plant-status-card"; import "../cards/hui-sensor-card"; import "../cards/hui-vertical-stack-card.ts"; +import "../cards/hui-shopping-list-card"; import "../cards/hui-thermostat-card.ts"; import "../cards/hui-weather-forecast-card"; import "../cards/hui-gauge-card"; @@ -49,6 +50,7 @@ const CARD_TYPES = new Set([ "picture-glance", "plant-status", "sensor", + "shopping-list", "thermostat", "vertical-stack", "weather-forecast",