UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

340 lines (303 loc) 9.12 kB
/** * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact schukai GmbH. */ import { instanceSymbol } from "../../constants.mjs"; import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs"; import { CustomElement } from "../../dom/customelement.mjs"; import { assembleMethodSymbol, registerCustomElement, } from "../../dom/customelement.mjs"; import { isString } from "../../types/is.mjs"; import { FetchBoxStyleSheet } from "./stylesheet/fetch-box.mjs"; import { addErrorAttribute } from "../../dom/error.mjs"; import { Formatter } from "../../text/formatter.mjs"; export { FetchBox }; /** * @private * @type {symbol} */ export const fetchBoxElementSymbol = Symbol("fetchBoxElement"); /** * A FetchBox * * @fragments /fragments/components/content/fetch-box/ * * @example /examples/components/content/fetch-box-simple * * @since 3.115.0 * @copyright schukai GmbH * @summary A beautiful FetchBox that can make your life easier and also looks good. Its like a box, but it fetches data from a URL. */ class FetchBox extends CustomElement { /** * This method is called by the `instanceof` operator. * @returns {symbol} */ static get [instanceSymbol]() { return Symbol.for( "@schukai/monster/components/content/fetch-box@@instance", ); } /** * * @return {Components.Content.FetchBox */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); return this; } /** * Handles the component's connection to the DOM. * Determines the styling of the component based on its parent's tag name. * Fetches necessary data for the component. * * @return {void} This method does not return a value. */ connectedCallback() { super.connectedCallback(); const parent = this.parentElement; if (parent) { const blockLevelElements = [ "DIV", "SECTION", "ARTICLE", "HEADER", "FOOTER", "MAIN", "NAV", "ASIDE", ]; const isBlockLevel = blockLevelElements.includes(parent.tagName); if (isBlockLevel) { this.style.display = "block"; this.style.width = "100%"; this.style.height = "100%"; } else { this.style.display = "inline-flex"; this.style.height = "100%"; } } this.fetch(); } /** * To set the options via the HTML Tag, the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @property {Object} templates Template definitions * @property {string} templates.main Main template * @property {Object} classes CSS classes * @property {string} classes.default CSS class for the main template * @property {string} classes.loading CSS class for the loading template * @property {string} classes.error CSS class for the error template * @property {string} classes.empty CSS class for the empty template * @property {Object} fetch fetch options for the request * @property {string} fetch.redirect error, follow, manual * @property {string} fetch.method GET, POST, PUT, DELETE * @property {string} fetch.mode same-origin, cors, no-cors, navigate * @property {string} fetch.credentials omit, same-origin, include * @property {Object} fetch.headers * @property {string} fetch.headers.accept text/html, application/json * @property {string} fetch.headers.content-type application/json * @property {string} parameter value for the data url * @property {Object} formatter * @property {Object} formatter.marker * @property {string} formatter.marker.open marker for the url * @property {string} formatter.marker.close marker for the url * @property {string} url url to fetch */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, classes: { default: "monster-fetch-box-default", loading: "loading", error: "error", empty: "empty", }, data: {}, url: null, fetch: { redirect: "error", method: "GET", mode: "same-origin", credentials: "same-origin", headers: { accept: "text/html", }, }, content: { // <slot name="loading"></slot> loading: `<div class="monster-skeleton-animated monster-skeleton-col-100"></div>`, error: `<slot name="error"></slot>`, empty: `<slot name="empty"></slot>`, }, visible: "loading", parameter: null, formatter: { marker: { open: null, close: null, }, }, }); } /** * @return {string} */ static getTag() { return "monster-fetch-box"; } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [FetchBoxStyleSheet]; } /** * load content from url */ fetch() { try { return loadContent .call(this) .then((obj) => { if (obj.type !== "application/json") { this.setOption("visible", "error"); throw new Error("not a json response"); } try { const content = obj.content; if (!isString(content) || content === "") { this.setOption("visible", "empty"); return; } const jsonContent = JSON.parse(content); if (jsonContent === null) { this.setOption("visible", "empty"); return; } this.setOption("data", jsonContent); this.setOption("visible", "default"); } catch (e) { this.setOption("visible", "error"); throw e; } }) .catch((e) => { this.setOption("visible", "error"); addErrorAttribute(this, e); }); } catch (e) { addErrorAttribute(this, e); this.setOption("visible", "error"); return Promise.reject(e); } } } /** * @private * @throws {Error} missing url * @throws {Error} we won't be able to read the data * @throws {Error} request failed * @return {Promise} */ function loadContent() { let url = this.getOption("url"); if (url instanceof URL) { url = url.toString(); } if (!isString(url) || url === "") { throw new Error("missing url"); } let p = this.getOption("parameter", null); if (p === null) { p = ""; } const data = { parameter: p, }; const formatter = new Formatter(data); if (this.getOption("formatter.marker.open")) { const open = this.getOption("formatter.marker.open"); if (!isString(open)) { throw new TypeError("open is not a string"); } const close = this.getOption("formatter.marker.close"); if (close !== undefined && !isString(close)) { throw new TypeError("close is not a string"); } formatter.setMarker(open, close); } const formattedUrl = formatter.format(url); const options = this.getOption("fetch", {}); return fetch(formattedUrl, options).then((response) => { if (!response.ok) { if (["error", "opaque", "opaqueredirect"].includes(response.type)) { throw new Error(`we won't be able to read the data (${response.type})`); } const statusClass = String(response.status).charAt(0); if (statusClass === "4") { throw new Error(`client error ${response.statusText}`); } throw new Error( `undefined status (${response.status} / ${response.statusText}) or type (${response.type})`, ); } return response.text().then((content) => ({ content, type: response.headers.get("Content-Type"), })); }); } /** * @private * @return {void} */ function initControlReferences() { this[fetchBoxElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="control"]`, ); } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control"> <div data-monster-attributes="class path:classes.default, data-monster-visible path:visible | equals:default | ?:true:false "> <slot></slot> </div> <div data-monster-attributes="class path:classes.loading, data-monster-visible path:visible | equals:loading | ?:true:false" data-monster-replace="path:content.loading"></div> <div data-monster-attributes="class path:classes.error, data-monster-visible path:visible | equals:error | ?:true:false" data-monster-replace="path:content.error"></div> <div data-monster-attributes="class path:classes.empty, data-monster-visible path:visible | equals:empty | ?:true:false" data-monster-replace="path:content.empty"></div> </div>`; } registerCustomElement(FetchBox);