diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 35657af5f8..0587fde184 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -1,5 +1,8 @@ -import { HomeAssistant } from "../../src/layouts/app/home-assistant"; -import { provideHass } from "../../src/fake_data/provide_hass"; +import { HomeAssistantAppEl } from "../../src/layouts/app/home-assistant"; +import { + provideHass, + MockHomeAssistant, +} from "../../src/fake_data/provide_hass"; import { navigate } from "../../src/common/navigate"; import { mockLovelace } from "./stubs/lovelace"; import { mockAuth } from "./stubs/auth"; @@ -11,14 +14,14 @@ import { mockSystemLog } from "./stubs/system_log"; import { mockTemplate } from "./stubs/template"; import { mockEvents } from "./stubs/events"; import { mockMediaPlayer } from "./stubs/media_player"; +import { HomeAssistant } from "../../src/types"; -class HaDemo extends HomeAssistant { +class HaDemo extends HomeAssistantAppEl { protected async _handleConnProm() { - const initial: Partial = { + const initial: Partial = { panelUrl: (this as any).panelUrl, // Override updateHass so that the correct hass lifecycle methods are called - updateHass: (hassUpdate) => - // @ts-ignore + updateHass: (hassUpdate: Partial) => this._updateHass(hassUpdate), }; diff --git a/src/common/dom/fire_event.ts b/src/common/dom/fire_event.ts index 57ecbd02c3..2e3a136225 100644 --- a/src/common/dom/fire_event.ts +++ b/src/common/dom/fire_event.ts @@ -55,7 +55,7 @@ export interface HASSDomEvent extends Event { * @return {Event} The new event that was fired. */ export const fireEvent = ( - node: HTMLElement, + node: HTMLElement | Window, type: HassEvent, detail?: HASSDomEvents[HassEvent], options?: { diff --git a/src/common/navigate.ts b/src/common/navigate.ts index 2b6a9c8d56..33ee8d46d9 100644 --- a/src/common/navigate.ts +++ b/src/common/navigate.ts @@ -1,7 +1,7 @@ import { fireEvent } from "./dom/fire_event"; export const navigate = ( - node: HTMLElement, + _node: any, path: string, replace: boolean = false ) => { @@ -18,5 +18,5 @@ export const navigate = ( history.pushState(null, "", path); } } - fireEvent(node, "location-changed"); + fireEvent(window, "location-changed"); }; diff --git a/src/components/ha-toast.js b/src/components/ha-toast.ts similarity index 71% rename from src/components/ha-toast.js rename to src/components/ha-toast.ts index ba14c6a392..488a152967 100644 --- a/src/components/ha-toast.js +++ b/src/components/ha-toast.ts @@ -1,9 +1,10 @@ import "@polymer/paper-toast/paper-toast"; +// tslint:disable-next-line const PaperToast = customElements.get("paper-toast"); -class HaToast extends PaperToast { - connectedCallback() { +export class HaToast extends PaperToast { + public connectedCallback() { super.connectedCallback(); if (!this._resizeListener) { @@ -15,10 +16,16 @@ class HaToast extends PaperToast { this._resizeListener(this._mediaq); } - disconnectedCallback() { + public disconnectedCallback() { super.disconnectedCallback(); this._mediaq.removeListener(this._resizeListener); } } +declare global { + interface HTMLElementTagNameMap { + "ha-toast": HaToast; + } +} + customElements.define("ha-toast", HaToast); diff --git a/src/layouts/app/auth-mixin.js b/src/layouts/app/auth-mixin.js index b913bac7ea..22b5e8273f 100644 --- a/src/layouts/app/auth-mixin.js +++ b/src/layouts/app/auth-mixin.js @@ -6,8 +6,8 @@ import { subscribeUser } from "../../data/ws-user"; export default (superClass) => class extends superClass { - ready() { - super.ready(); + firstUpdated(changedProps) { + super.firstUpdated(changedProps); this.addEventListener("hass-logout", () => this._handleLogout()); // HACK :( We don't have a way yet to trigger an update of `subscribeUser` this.addEventListener("hass-refresh-current-user", () => diff --git a/src/layouts/app/connection-mixin.js b/src/layouts/app/connection-mixin.js index a7b50fa897..bef29cbbbe 100644 --- a/src/layouts/app/connection-mixin.js +++ b/src/layouts/app/connection-mixin.js @@ -20,8 +20,8 @@ import { subscribePanels } from "../../data/ws-panels"; export default (superClass) => class extends EventsMixin(LocalizeMixin(superClass)) { - ready() { - super.ready(); + firstUpdated(changedProps) { + super.firstUpdated(changedProps); this._handleConnProm(); } @@ -48,7 +48,7 @@ export default (superClass) => panels: null, services: null, user: null, - panelUrl: this.panelUrl, + panelUrl: this._panelUrl, language: getActiveTranslation(), // If resources are already loaded, don't discard them diff --git a/src/layouts/app/dialog-manager-mixin.ts b/src/layouts/app/dialog-manager-mixin.ts index 3bd480f3a1..812754d162 100644 --- a/src/layouts/app/dialog-manager-mixin.ts +++ b/src/layouts/app/dialog-manager-mixin.ts @@ -1,5 +1,4 @@ -import { PolymerElement } from "@polymer/polymer"; -import { Constructor } from "lit-element"; +import { Constructor, LitElement } from "lit-element"; import { HASSDomEvent, ValidHassDomEvent } from "../../common/dom/fire_event"; interface RegisterDialogParams { @@ -23,10 +22,10 @@ declare global { } } -export const dialogManagerMixin = (superClass: Constructor) => +export const dialogManagerMixin = (superClass: Constructor) => class extends superClass { - public ready() { - super.ready(); + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); this.addEventListener("register-dialog", (e) => this.registerDialog(e.detail) ); diff --git a/src/layouts/app/disconnect-toast-mixin.js b/src/layouts/app/disconnect-toast-mixin.js deleted file mode 100644 index 42f160592e..0000000000 --- a/src/layouts/app/disconnect-toast-mixin.js +++ /dev/null @@ -1,27 +0,0 @@ -import LocalizeMixin from "../../mixins/localize-mixin"; - -export default (superClass) => - class extends LocalizeMixin(superClass) { - hassConnected() { - super.hassConnected(); - // Need to load in advance because when disconnected, can't dynamically load code. - import(/* webpackChunkName: "ha-toast" */ "../../components/ha-toast"); - } - - hassReconnected() { - super.hassReconnected(); - this.__discToast.opened = false; - } - - hassDisconnected() { - super.hassDisconnected(); - if (!this.__discToast) { - const el = document.createElement("ha-toast"); - el.duration = 0; - el.text = this.localize("ui.notification_toast.connection_lost"); - this.__discToast = el; - this.shadowRoot.appendChild(el); - } - this.__discToast.opened = true; - } - }; diff --git a/src/layouts/app/disconnect-toast-mixin.ts b/src/layouts/app/disconnect-toast-mixin.ts new file mode 100644 index 0000000000..5cbfe6b60d --- /dev/null +++ b/src/layouts/app/disconnect-toast-mixin.ts @@ -0,0 +1,40 @@ +import { Constructor, LitElement } from "lit-element"; +import { HassBaseEl } from "./hass-base-mixin"; +import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin"; +import { HaToast } from "../../components/ha-toast"; + +export default (superClass: Constructor) => + class extends hassLocalizeLitMixin(superClass) { + private _discToast?: HaToast; + + protected hassConnected() { + super.hassConnected(); + // Need to load in advance because when disconnected, can't dynamically load code. + import(/* webpackChunkName: "ha-toast" */ "../../components/ha-toast"); + } + + protected hassReconnected() { + super.hassReconnected(); + if (this._discToast) { + this._discToast.opened = false; + } + } + + protected hassDisconnected() { + super.hassDisconnected(); + if (!this._discToast) { + const el = document.createElement("ha-toast"); + el.duration = 0; + // Temp. Somehow the localize func is not getting recalculated for + // this class. Manually generating one. Will be fixed when we move + // the localize function to the hass object. + const { language, resources } = this.hass!; + el.text = (this as any).__computeLocalize(language, resources)( + "ui.notification_toast.connection_lost" + ); + this._discToast = el; + this.shadowRoot!.appendChild(el as any); + } + this._discToast.opened = true; + } + }; diff --git a/src/layouts/app/hass-base-mixin.js b/src/layouts/app/hass-base-mixin.js deleted file mode 100644 index 13a6760f59..0000000000 --- a/src/layouts/app/hass-base-mixin.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable no-unused-vars */ -export default (superClass) => - class extends superClass { - constructor() { - super(); - this.__pendingHass = false; - this.__provideHass = []; - } - - // Exists so all methods can safely call super method - hassConnected() {} - - hassReconnected() {} - - hassDisconnected() {} - - panelUrlChanged(newPanelUrl) {} - - hassChanged(hass, oldHass) { - this.__provideHass.forEach((el) => { - el.hass = hass; - }); - } - - provideHass(el) { - this.__provideHass.push(el); - el.hass = this.hass; - } - - async _updateHass(obj) { - const oldHass = this.hass; - this.hass = Object.assign({}, this.hass, obj); - this.__pendingHass = true; - - await 0; - - if (!this.__pendingHass) return; - - this.__pendingHass = false; - this.hassChanged(this.hass, oldHass); - } - }; diff --git a/src/layouts/app/hass-base-mixin.ts b/src/layouts/app/hass-base-mixin.ts new file mode 100644 index 0000000000..f748aca450 --- /dev/null +++ b/src/layouts/app/hass-base-mixin.ts @@ -0,0 +1,60 @@ +import { Constructor } from "lit-element"; +import { HomeAssistant } from "../../types"; + +/* tslint:disable */ + +export class HassBaseEl { + protected hass?: HomeAssistant; + protected hassConnected() {} + protected hassReconnected() {} + protected hassDisconnected() {} + protected hassChanged(_hass: HomeAssistant, _oldHass?: HomeAssistant) {} + protected panelUrlChanged(_newPanelUrl: string) {} + protected provideHass(_el: HTMLElement) {} + protected _updateHass(_obj: Partial) {} +} + +export default (superClass: Constructor): Constructor => + // @ts-ignore + class extends superClass { + private __provideHass: HTMLElement[]; + // @ts-ignore + protected hass: HomeAssistant; + + constructor() { + super(); + this.__provideHass = []; + } + + // Exists so all methods can safely call super method + protected hassConnected() { + // tslint:disable-next-line + } + + protected hassReconnected() { + // tslint:disable-next-line + } + + protected hassDisconnected() { + // tslint:disable-next-line + } + + protected panelUrlChanged(_newPanelUrl) { + // tslint:disable-next-line + } + + protected hassChanged(hass, _oldHass) { + this.__provideHass.forEach((el) => { + (el as any).hass = hass; + }); + } + + protected provideHass(el) { + this.__provideHass.push(el); + el.hass = this.hass; + } + + protected async _updateHass(obj) { + this.hass = { ...this.hass, ...obj }; + } + }; diff --git a/src/layouts/app/home-assistant.js b/src/layouts/app/home-assistant.js deleted file mode 100644 index 4201d70656..0000000000 --- a/src/layouts/app/home-assistant.js +++ /dev/null @@ -1,120 +0,0 @@ -import "@polymer/app-route/app-location"; -import "@polymer/app-route/app-route"; -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { afterNextRender } from "@polymer/polymer/lib/utils/render-status"; -import { html as litHtml, LitElement } from "lit-element"; - -import "../home-assistant-main"; -import "../ha-init-page"; -import "../../resources/ha-style"; -import registerServiceWorker from "../../util/register-service-worker"; -import { DEFAULT_PANEL } from "../../common/const"; - -import HassBaseMixin from "./hass-base-mixin"; -import AuthMixin from "./auth-mixin"; -import TranslationsMixin from "./translations-mixin"; -import ThemesMixin from "./themes-mixin"; -import MoreInfoMixin from "./more-info-mixin"; -import SidebarMixin from "./sidebar-mixin"; -import { dialogManagerMixin } from "./dialog-manager-mixin"; -import ConnectionMixin from "./connection-mixin"; -import NotificationMixin from "./notification-mixin"; -import DisconnectToastMixin from "./disconnect-toast-mixin"; - -LitElement.prototype.html = litHtml; - -const ext = (baseClass, mixins) => - mixins.reduceRight((base, mixin) => mixin(base), baseClass); - -export class HomeAssistant extends ext(PolymerElement, [ - AuthMixin, - ThemesMixin, - TranslationsMixin, - MoreInfoMixin, - SidebarMixin, - DisconnectToastMixin, - ConnectionMixin, - NotificationMixin, - dialogManagerMixin, - HassBaseMixin, -]) { - static get template() { - return html` - - - - - - `; - } - - static get properties() { - return { - hass: { - type: Object, - value: null, - }, - showMain: { - type: Boolean, - computed: "computeShowMain(hass)", - }, - route: Object, - routeData: Object, - panelUrl: { - type: String, - computed: "computePanelUrl(routeData)", - observer: "panelUrlChanged", - }, - _error: { - type: Boolean, - value: false, - }, - }; - } - - ready() { - super.ready(); - afterNextRender(null, registerServiceWorker); - } - - computeShowMain(hass) { - return hass && hass.states && hass.config && hass.panels && hass.services; - } - - computePanelUrl(routeData) { - return ( - (routeData && routeData.panel) || - localStorage.defaultPage || - DEFAULT_PANEL - ); - } - - get _useHashAsPath() { - return __DEMO__; - } - - panelUrlChanged(newPanelUrl) { - super.panelUrlChanged(newPanelUrl); - this._updateHass({ panelUrl: newPanelUrl }); - } -} - -customElements.define("home-assistant", HomeAssistant); diff --git a/src/layouts/app/home-assistant.ts b/src/layouts/app/home-assistant.ts new file mode 100644 index 0000000000..3724fac607 --- /dev/null +++ b/src/layouts/app/home-assistant.ts @@ -0,0 +1,119 @@ +import "@polymer/app-route/app-location"; +import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import { + html, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "lit-element"; + +import "../home-assistant-main"; +import "../ha-init-page"; +import "../../resources/ha-style"; +import { registerServiceWorker } from "../../util/register-service-worker"; +import { DEFAULT_PANEL } from "../../common/const"; + +import HassBaseMixin from "./hass-base-mixin"; +import AuthMixin from "./auth-mixin"; +import TranslationsMixin from "./translations-mixin"; +import ThemesMixin from "./themes-mixin"; +import MoreInfoMixin from "./more-info-mixin"; +import SidebarMixin from "./sidebar-mixin"; +import { dialogManagerMixin } from "./dialog-manager-mixin"; +import ConnectionMixin from "./connection-mixin"; +import NotificationMixin from "./notification-mixin"; +import DisconnectToastMixin from "./disconnect-toast-mixin"; +import { Route, HomeAssistant } from "../../types"; +import { navigate } from "../../common/navigate"; + +(LitElement.prototype as any).html = html; + +const ext = (baseClass: T, mixins): T => + mixins.reduceRight((base, mixin) => mixin(base), baseClass); + +export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [ + AuthMixin, + ThemesMixin, + TranslationsMixin, + MoreInfoMixin, + SidebarMixin, + DisconnectToastMixin, + ConnectionMixin, + NotificationMixin, + dialogManagerMixin, +]) { + private _route?: Route; + private _error?: boolean; + private _panelUrl?: string; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + _route: {}, + _routeData: {}, + _panelUrl: {}, + _error: {}, + }; + } + + protected render() { + const hass = this.hass; + + return html` + + ${this._panelUrl === undefined || this._route === undefined + ? "" + : hass && hass.states && hass.config && hass.panels && hass.services + ? html` + + ` + : html` + + `} + `; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + setTimeout(registerServiceWorker, 1000); + } + + protected updated(changedProps: PropertyValues): void { + if (changedProps.has("_panelUrl")) { + this.panelUrlChanged(this._panelUrl!); + this._updateHass({ panelUrl: this._panelUrl }); + } + if (changedProps.has("hass")) { + this.hassChanged(this.hass!, changedProps.get("hass") as + | HomeAssistant + | undefined); + } + } + + private _routeChanged(ev) { + const route = ev.detail.value as Route; + + // If it's the first route that we process, + // check if we should navigate away from / + if (this._route === undefined && route.path === "/") { + navigate(window, `/${localStorage.defaultPage || DEFAULT_PANEL}`, true); + return; + } + + this._route = route; + + const dividerPos = route.path.indexOf("/", 1); + this._panelUrl = + dividerPos === -1 + ? route.path.substr(1) + : route.path.substr(1, dividerPos - 1); + } +} + +customElements.define("home-assistant", HomeAssistantAppEl); diff --git a/src/layouts/app/more-info-mixin.js b/src/layouts/app/more-info-mixin.js index a15ce2f0e9..276d2f7e64 100644 --- a/src/layouts/app/more-info-mixin.js +++ b/src/layouts/app/more-info-mixin.js @@ -2,8 +2,8 @@ import { afterNextRender } from "@polymer/polymer/lib/utils/render-status"; export default (superClass) => class extends superClass { - ready() { - super.ready(); + firstUpdated(changedProps) { + super.firstUpdated(changedProps); this.addEventListener("hass-more-info", (e) => this._handleMoreInfo(e)); // Load it once we are having the initial rendering done. diff --git a/src/layouts/app/notification-mixin.js b/src/layouts/app/notification-mixin.js index c1fba46902..43cf2461c3 100644 --- a/src/layouts/app/notification-mixin.js +++ b/src/layouts/app/notification-mixin.js @@ -1,7 +1,7 @@ export default (superClass) => class extends superClass { - ready() { - super.ready(); + firstUpdated(changedProps) { + super.firstUpdated(changedProps); this.registerDialog({ dialogShowEvent: "hass-notification", dialogTag: "notification-manager", diff --git a/src/layouts/app/sidebar-mixin.js b/src/layouts/app/sidebar-mixin.js index c0ff17804f..443d81a26d 100644 --- a/src/layouts/app/sidebar-mixin.js +++ b/src/layouts/app/sidebar-mixin.js @@ -2,8 +2,8 @@ import { storeState } from "../../util/ha-pref-storage"; export default (superClass) => class extends superClass { - ready() { - super.ready(); + firstUpdated(changedProps) { + super.firstUpdated(changedProps); this.addEventListener("hass-dock-sidebar", (e) => this._handleDockSidebar(e) ); diff --git a/src/layouts/app/themes-mixin.js b/src/layouts/app/themes-mixin.js index 7698dc0746..3e4cf513c8 100644 --- a/src/layouts/app/themes-mixin.js +++ b/src/layouts/app/themes-mixin.js @@ -4,9 +4,8 @@ import { subscribeThemes } from "../../data/ws-themes"; export default (superClass) => class extends superClass { - ready() { - super.ready(); - + firstUpdated(changedProps) { + super.firstUpdated(changedProps); this.addEventListener("settheme", (ev) => { this._updateHass({ selectedTheme: ev.detail }); this._applyTheme(); diff --git a/src/layouts/app/translations-mixin.js b/src/layouts/app/translations-mixin.js index 81721c0f1d..41ace43e74 100644 --- a/src/layouts/app/translations-mixin.js +++ b/src/layouts/app/translations-mixin.js @@ -8,8 +8,8 @@ import { storeState } from "../../util/ha-pref-storage"; export default (superClass) => class extends superClass { - ready() { - super.ready(); + firstUpdated(changedProps) { + super.firstUpdated(changedProps); this.addEventListener("hass-language-select", (e) => this._selectLanguage(e) ); @@ -81,6 +81,6 @@ export default (superClass) => storeState(this.hass); this._loadResources(); this._loadBackendTranslations(); - this._loadTranslationFragment(this.panelUrl); + this._loadTranslationFragment(this.hass.panelUrl); } }; diff --git a/src/layouts/home-assistant-main.js b/src/layouts/home-assistant-main.js index 6e4379b603..4f99bf5958 100644 --- a/src/layouts/home-assistant-main.js +++ b/src/layouts/home-assistant-main.js @@ -81,9 +81,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) { return { hass: Object, narrow: Boolean, - tail: { - type: Object, - }, route: { type: Object, observer: "_routeChanged", @@ -136,13 +133,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) { } } - connectedCallback() { - super.connectedCallback(); - if (this.tail.prefix === "") { - this.navigate(`/${localStorage.defaultPage || DEFAULT_PANEL}`, true); - } - } - computeForceNarrow(narrow, dockedSidebar) { return narrow || !dockedSidebar; } diff --git a/src/util/register-service-worker.js b/src/util/register-service-worker.js index 2ad485830b..c80eef58b8 100644 --- a/src/util/register-service-worker.js +++ b/src/util/register-service-worker.js @@ -1,7 +1,7 @@ const serviceWorkerUrl = __BUILD__ === "latest" ? "/service_worker.js" : "/service_worker_es5.js"; -export default () => { +export const registerServiceWorker = () => { if (!("serviceWorker" in navigator)) return; navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {