@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
340 lines (303 loc) • 9.12 kB
JavaScript
/**
* 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);