@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
343 lines (314 loc) • 10.5 kB
JavaScript
import { Fore } from './fore.js';
import { evaluateXPathToFirstNode } from './xpath-evaluation.js';
async function handleResponse(fxInstance, response) {
const { status } = response;
if (status >= 400) {
// console.log('response status', status);
alert(`response status: ${status} - failed to load data for '${fxInstance.src}' - stopping.`);
throw new Error(`failed to load data - status: ${status}`);
}
const responseContentType = response.headers.get('content-type').toLowerCase();
// console.log('********** responseContentType *********', responseContentType);
if (responseContentType.startsWith('text/html')) {
// const htmlResponse = response.text();
// return new DOMParser().parseFromString(htmlResponse, 'text/html');
// return response.text();
return response.text().then(result =>
// console.log('xml ********', result);
new DOMParser().parseFromString(result, 'text/html'),
);
}
if (responseContentType.startsWith('text/')) {
// console.log("********** inside res plain *********");
return response.text();
}
if (responseContentType.startsWith('application/json')) {
// console.log("********** inside res json *********");
return response.json();
}
if (
responseContentType.startsWith('application/xml') ||
responseContentType.startsWith('text/xml')
) {
// See https://www.rfc-editor.org/rfc/rfc7303
const text = await response.text();
// console.log('xml ********', result);
return new DOMParser().parseFromString(text, 'application/xml');
}
throw new Error(`unable to handle response content type: ${responseContentType}`);
}
/**
* Container for data instances.
*
* Offers several ways of loading data from either inline content or via 'src' attribute which will use the fetch
* API to resolve data.
*/
export class FxInstance extends HTMLElement {
constructor() {
super();
this.model = this.parentNode;
this.attachShadow({ mode: 'open' });
this.originalInstance = null;
this.partialInstance = null;
this.credentials = '';
}
connectedCallback() {
// console.log('connectedCallback ', this);
if (this.hasAttribute('src')) {
this.src = this.getAttribute('src');
}
if (this.hasAttribute('id')) {
this.id = this.getAttribute('id');
} else {
this.id = 'default';
}
this.credentials = this.hasAttribute('credentials')
? this.getAttribute('credentials')
: 'same-origin';
if (!['same-origin', 'include', 'omit'].includes(this.credentials)) {
console.error(
`fx-submission: the value of credentials is not valid. Expected 'same-origin', 'include' or 'omit' but got '${this.credentials}'`,
this,
);
}
if (this.hasAttribute('type')) {
this.type = this.getAttribute('type');
} else {
this.type = 'xml';
this.setAttribute('type', this.type);
}
const style = `
:host {
display: none;
}
:host * {
display:none;
}
::slotted(*){
display:none;
}
`;
const html = `
`;
this.shadowRoot.innerHTML = `
<style>
${style}
</style>
${html}
`;
this.partialInstance = {};
}
/**
* Is called by fx-model during initialization phase (model-construct)
* @returns {Promise<void>}
*/
async init() {
// console.log('fx-instance init');
await this._initInstance();
// console.log(`### <<<<< instance ${this.id} loaded >>>>> `);
this.dispatchEvent(
new CustomEvent('instance-loaded', {
composed: true,
bubbles: true,
detail: { instance: this },
}),
);
return this;
}
reset() {
// this._useInlineData();
this.instanceData = this.originalInstance.cloneNode(true);
}
evalXPath(xpath) {
const formElement = this.parentElement.parentElement;
const result = evaluateXPathToFirstNode(xpath, this.getDefaultContext(), formElement);
return result;
}
/**
* returns the current instance data
*
* @returns {Document | T | any}
*/
getInstanceData() {
if (!this.instanceData) {
this.createInstanceData();
}
return this.instanceData;
}
setInstanceData(data) {
if (!data) {
this.createInstanceData();
return;
}
this._setInitialData(data);
// this.instanceData = data;
}
/**
* return the default context (root node of respective instance) for XPath evalution.
*
* @returns {Document|T|any|Element}
*/
getDefaultContext() {
// Note: use the getter here: it might provide us with stubbed data if anything async is racing,
// such as an @src attribute
const instanceData = this.getInstanceData();
if (this.type === 'xml') {
return instanceData.firstElementChild;
}
return instanceData;
}
/**
* does the actual loading of data. Handles inline data, data loaded via fetch() or data constructed from
* querystring.
*
* @returns {Promise<void>}
* @private
*/
async _initInstance() {
if (this.src === '#querystring') {
/*
* generate XML data from URL querystring
* todo: there's no variant to generate JSON yet
*/
// eslint-disable-next-line no-restricted-globals
const query = new URLSearchParams(location.search);
const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
const root = doc.firstElementChild;
for (const p of query) {
const newNode = doc.createElement(p[0]);
newNode.appendChild(doc.createTextNode(p[1]));
root.appendChild(newNode);
}
this._setInitialData(doc);
} else if (this.src) {
await this._loadData();
} else if (this.childNodes.length !== 0) {
this._useInlineData();
}
}
createInstanceData() {
if (this.type === 'xml') {
// const doc = new DOMParser().parseFromString('<data data-id="default"></data>', 'application/xml');
const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
this.instanceData = doc;
this.originalInstance = this.instanceData.cloneNode(true);
}
if (this.type === 'json') {
this.instanceData = {};
this.originalInstance = [...this.instanceData];
}
if (this.type === 'text') {
this.instanceData = this.innerText;
this.originalInstance = this.innerText;
console.log('text data', this.instanceData);
}
}
async _loadData() {
const url = `${this.src}`;
if (url.startsWith('localStore')) {
const key = url.substring(url.indexOf(':') + 1);
const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
this.instanceData = doc;
// ### does it make sense to store originalData here?
if (!key) {
console.warn('no key specified for localStore');
return;
}
const serialized = localStorage.getItem(key);
if (!serialized) {
console.warn(`Data for key ${key} cannot be found`);
this._useInlineData();
return;
}
const data = new DOMParser().parseFromString(serialized, 'application/xml');
// let data = this._parse(serialized, instance);
doc.firstElementChild.replaceWith(data.firstElementChild);
return;
}
const contentType = Fore.getContentType(this, 'get');
try {
const response = await fetch(url, {
method: 'GET',
credentials: this.credentials,
mode: 'cors',
headers: {
'Content-Type': contentType,
},
});
const data = await handleResponse(this, response);
this._setInitialData(data);
/*
if (data.nodeType) {
this._setInitialData(data);
this.instanceData = data;
this.originalInstance = this.instanceData.cloneNode(true);
console.log('instanceData loaded: ', this.id, this.instanceData);
return;
}
this.instanceData = data;
this.originalInstance = [...data];
*/
} catch (error) {
throw new Error(`failed loading data ${error}`);
}
}
_setInitialData(data) {
this.instanceData = data;
if (data.nodeType) {
this.originalInstance = this.instanceData.cloneNode(true);
} else {
this.originalInstance = { ...this.instanceData };
}
}
_getContentType() {
if (this.type === 'xml') {
return 'application/xml';
}
if (this.type === 'json') {
return 'application/json';
}
console.warn('content-type unknown ', this.type);
return null;
}
_useInlineData() {
if (this.type === 'xml') {
// console.log('innerHTML ', this.innerHTML);
const instanceData = new DOMParser().parseFromString(this.innerHTML, 'application/xml');
// console.log('fx-instance init id:', this.id);
// this.instanceData = instanceData;
this._setInitialData(instanceData);
// console.log('instanceData ', this.instanceData);
// console.log('instanceData ', this.instanceData.firstElementChild);
// console.log('fx-instance data: ', this.instanceData);
// this.instanceData.firstElementChild.setAttribute('id', this.id);
// todo: move innerHTML out to shadowDOM (for later reset)
} else if (this.type === 'json') {
// this.instanceData = JSON.parse(this.textContent);
this._setInitialData(JSON.parse(this.textContent));
} else if (this.type === 'html') {
// this.instanceData = this.firstElementChild.children;
this._setInitialData(this.firstElementChild.children);
} else if (this.type === 'text') {
// this.instanceData = this.textContent;
this._setInitialData(this.textContent);
} else {
console.warn('unknow type for data ', this.type);
}
}
// _handleResponse() {
// console.log('_handleResponse ');
// const ajax = this.shadowRoot.getElementById('loader');
// const instanceData = new DOMParser().parseFromString(ajax.lastResponse, 'application/xml');
// this.instanceData = instanceData;
// console.log('data: ', this.instanceData);
// }
/*
_handleError() {
const loader = this.shadowRoot.getElementById('loader');
console.log('_handleResponse ', loader.lastError);
}
*/
}
if (!customElements.get('fx-instance')) {
customElements.define('fx-instance', FxInstance);
}