From 4835218d1a5b300990542a187ade18dd3afe142c Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Mon, 22 Jan 2024 15:10:56 +0100
Subject: [PATCH] fix: double fetch #134

---
 playground/datatable/index.html               |   5 +-
 playground/vite.config.js                     | 106 +--
 .../components/datatable/datasource/rest.mjs  | 672 ++++++++++--------
 source/components/datatable/filter.mjs        |  10 +-
 source/data/datasource/server/restapi.mjs     |   1 +
 source/dom/customelement.mjs                  |   2 +-
 6 files changed, 402 insertions(+), 394 deletions(-)

diff --git a/playground/datatable/index.html b/playground/datatable/index.html
index 6aaefbf65..e0d092010 100644
--- a/playground/datatable/index.html
+++ b/playground/datatable/index.html
@@ -22,11 +22,10 @@
         <monster-config-manager></monster-config-manager>
 
         <h1>Datatable with Pagination </h1>
-        <!-- "url": "https://localhost.schukai.net:8443/api/commerce/orders/search?q=${path:query | default:order.customerUID>0}&page=${page}&orderby=${path:order | default:oid}", -->
         <script id="id-for-this-config" type="application/json">
             {
                 "read": {
-                    "url": "./data.json",
+                    "url": "http://localhost:8070/api/commerce/orders/search?q=${path:query | default:order.customerUID>0}&page=${page}&orderby=${path:order | default:oid}",
                     "init": {
                         "method": "GET",
                         "headers": {
@@ -181,4 +180,4 @@
 </main>
 
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/playground/vite.config.js b/playground/vite.config.js
index 9d04bb106..12537104a 100644
--- a/playground/vite.config.js
+++ b/playground/vite.config.js
@@ -1,5 +1,5 @@
 // vite.config.js
-import {basename, resolve,join} from 'path'
+import {basename, resolve, join} from 'path'
 import {defineConfig} from 'vite'
 import {existsSync} from 'fs'
 import banner from 'vite-plugin-banner'
@@ -29,9 +29,9 @@ import {ViteMinifyPlugin} from 'vite-plugin-minify'
 // const dist = resolve(__dirname, '')
 
 
-function getAppRootDir () {
+function getAppRootDir() {
     let currentDir = __dirname
-    while(!existsSync(join(currentDir, 'package.json'))) {
+    while (!existsSync(join(currentDir, 'package.json'))) {
         currentDir = join(currentDir, '..')
     }
     return currentDir
@@ -40,49 +40,6 @@ function getAppRootDir () {
 const rootDir = getAppRootDir()
 const playgroundDir = join(rootDir, 'playground')
 
-//
-// globSync(source + '/**/*.html', {
-//     ignore: [
-//         '**/resource/**'
-//     ]
-// }).forEach((file) => {
-//
-//     let lang = file.split('/').slice(-2)[0]
-//     let key = lang + "/" + basename(file).replace('.html', '')
-//
-//     files[key] = file;
-//
-//    
-// })
-//
-// console.log(files)
-
-
-// const rootDirectory = resolve(__dirname, '/');
-
-
-
-// function createStyleSheetPlugin() {
-//     return {
-//         name: 'buildStart',
-//         buildStart() {
-//             // console.log('buildStart')
-//             //
-//             // exec('pnpm run -C ' + scriptDir + " build-style", (err, stdout, stderr) => {
-//             //     if (err) {
-//             //         //some err occurred
-//             //         console.error(err)
-//             //     }
-//             // });
-//
-//         },
-//         async transform(raw, id, options) {
-//             //console.log(id, options,raw)
-//         }
-//
-//
-//     }
-// }
 console.log(rootDir)
 
 export default defineConfig({
@@ -92,23 +49,6 @@ export default defineConfig({
     root: playgroundDir,
     mode: 'development',
     logLevel: 'info',
-  //  base: "/",
-  //   build: {
-  //       outDir: dist,
-  //       emptyOutDir: true,
-  //       assetsDir: "asset",
-  //       rollupOptions: {
-  //           input: files,
-  //
-  //           output: {
-  //               globals: {
-  //               },
-  //           },            
-  //          
-  //       },
-  //       comments: false,
-  //       minify: 'esbuild',
-  //   },
     esbuild: {
         minifyIdentifiers: true,
         minifySyntax: true,
@@ -116,23 +56,16 @@ export default defineConfig({
         target: 'es2015',
         legalComments: 'none'
     },
-    server: {
-        port: 8070,
-        host: "localhost",
-        https: false,
-        debug: true
-    },
     plugins: [
         banner(
             `/**\n * name: ${pkg.name}\n * version: v${pkg.version}\n * description: ${pkg.description}\n * author: ${pkg.author}\n * homepage: ${pkg.homepage}\n */`
         ),
-  //      mkcert({}),
+
         ViteMinifyPlugin(),
         directoryPlugin({
             baseDir: __dirname
         }),
-      //  createStyleSheetPlugin()
-        
+
     ],
     css: {
         postcss: {
@@ -153,7 +86,34 @@ export default defineConfig({
                 postcssResponsiveType
             ]
         }
-    }
+    },
+
+    server: {
+
+        port: 8070,
+        host: "localhost",
+        https: false,
+        debug: true,
+        ssl: {
+            key: resolve(__dirname, 'ssl/localhost.key'),
+            cert: resolve(__dirname, 'ssl/localhost.crt'),
+        },
+        
+        proxy: {
+            '^/api/commerce/orders/search': {
+                target: 'http://localhost:8090',
+                changeOrigin: false,
+                
+                configure: (proxy, options) => {
+                 
+                    proxy.secure = false
+                    proxy.agent = null
+                    
+                },
+            },
+
+        },
+    },
 
 
 })
diff --git a/source/components/datatable/datasource/rest.mjs b/source/components/datatable/datasource/rest.mjs
index 9a5b8b40c..a0470a2e9 100644
--- a/source/components/datatable/datasource/rest.mjs
+++ b/source/components/datatable/datasource/rest.mjs
@@ -3,25 +3,25 @@
  * SPDX-License-Identifier: AGPL-3.0
  */
 
-import { addAttributeToken } from "../../../dom/attributes.mjs";
-import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
-import { Datasource, dataSourceSymbol } from "../datasource.mjs";
-import { DatasourceStyleSheet } from "../stylesheet/datasource.mjs";
-import { instanceSymbol } from "../../../constants.mjs";
+import {addAttributeToken} from "../../../dom/attributes.mjs";
+import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
+import {Datasource, dataSourceSymbol} from "../datasource.mjs";
+import {DatasourceStyleSheet} from "../stylesheet/datasource.mjs";
+import {instanceSymbol} from "../../../constants.mjs";
 import {
-	assembleMethodSymbol,
-	registerCustomElement,
+    assembleMethodSymbol,
+    registerCustomElement,
 } from "../../../dom/customelement.mjs";
-import { RestAPI } from "../../../data/datasource/server/restapi.mjs";
-import { Formatter } from "../../../text/formatter.mjs";
-import { clone } from "../../../util/clone.mjs";
-import { validateBoolean } from "../../../types/validate.mjs";
-import { findElementWithIdUpwards } from "../../../dom/util.mjs";
-import { Observer } from "../../../types/observer.mjs";
-import { Pathfinder } from "../../../data/pathfinder.mjs";
-import { fireCustomEvent } from "../../../dom/events.mjs";
+import {RestAPI} from "../../../data/datasource/server/restapi.mjs";
+import {Formatter} from "../../../text/formatter.mjs";
+import {clone} from "../../../util/clone.mjs";
+import {validateBoolean} from "../../../types/validate.mjs";
+import {findElementWithIdUpwards} from "../../../dom/util.mjs";
+import {Observer} from "../../../types/observer.mjs";
+import {Pathfinder} from "../../../data/pathfinder.mjs";
+import {fireCustomEvent} from "../../../dom/events.mjs";
 
-export { Rest };
+export {Rest};
 
 /**
  * @private
@@ -29,12 +29,21 @@ export { Rest };
  */
 const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler");
 
+/**
+ * @private
+ * Original at source/components/datatable/datasource/rest.mjs
+ * @type {symbol}
+ */
+const rawDataSymbol = Symbol.for(
+    "@schukai/monster/data/datasource/server/restapi/rawdata",
+);
+
 /**
  * @private
  * @type {symbol}
  */
 const intersectionObserverObserverSymbol = Symbol(
-	"intersectionObserverObserver",
+    "intersectionObserverObserver",
 );
 
 /**
@@ -63,23 +72,23 @@ const filterObserverSymbol = Symbol("filterObserver");
  * @summary A rest api datasource
  */
 class Rest extends Datasource {
-	/**
-	 * the constructor of the class
-	 */
-	constructor() {
-		super();
-		this[dataSourceSymbol] = new RestAPI();
-	}
-
-	/**
-	 * This method is called by the `instanceof` operator.
-	 * @returns {symbol}
-	 */
-	static get [instanceSymbol]() {
-		return Symbol.for("@schukai/monster/components/datasource/rest@@instance");
-	}
-
-	/**
+    /**
+     * the constructor of the class
+     */
+    constructor() {
+        super();
+        this[dataSourceSymbol] = new RestAPI();
+    }
+
+    /**
+     * This method is called by the `instanceof` operator.
+     * @returns {symbol}
+     */
+    static get [instanceSymbol]() {
+        return Symbol.for("@schukai/monster/components/datasource/rest@@instance");
+    }
+
+    /**
      * 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}
      *
@@ -98,7 +107,8 @@ class Rest extends Datasource {
      * @property {Object} datatable Datatable definitions
      * @property {string} datatable.id The id of the datatable control
      * @property {Object} response Response definitions
-     * @property {string} response.errorMessagePath The path to the error message in the response
+     * @property {Object} response.path Path definitions (changed in 3.56.0)
+     * @property {string} response.path.message Path to the message (changed in 3.56.0)
      * @property {Object} read Read configuration
      * @property {string} read.url The url of the rest api
      * @property {string} read.method The method of the rest api
@@ -109,312 +119,346 @@ class Rest extends Datasource {
      * @property {Object} write Write configuration
 
      */
-	get defaults() {
-		const restOptions = new RestAPI().defaults;
-
-		restOptions.read.parameters = {
-			filter: undefined,
-			oderBy: undefined,
-			page: "1",
-		};
-
-		return Object.assign({}, super.defaults, restOptions, {
-			templates: {
-				main: getTemplate(),
-			},
-
-			features: {
-				autoInit: true,
-				filter: false,
-			},
-
-			autoInit: {
-				intersectionObserver: false,
-				oneTime: true,
-			},
-
-			filter: {
-				id: undefined,
-			},
-
-			datatable: {
-				id: undefined,
-			},
-
-			response: {
-				errorMessagePath: "sys.message",
-			},
-		});
-	}
-
-	/**
-	 *
-	 * @param {string} page
-	 * @param {string} query
-	 * @param {string} orderBy
-	 * @returns {Monster.Components.Datatable.Datasource.Rest}
-	 */
-	setParameters({ page, query, orderBy }) {
-		const parameters = this.getOption("read.parameters");
-		if (query !== undefined) {
-			parameters.query = `${query}`;
-			parameters.page = "1";
-		}
-
-		// after a query the page is set to 1, so if the page is not set, it is set to 1
-		if (page !== undefined) parameters.page = `${page}`;
-		if (orderBy !== undefined) parameters.order = `${orderBy}`;
-		this.setOption("read.parameters", parameters);
-		return this;
-	}
-
-	/**
-	 *
-	 * @return {Monster.Components.Form.Form}
-	 */
-	[assembleMethodSymbol]() {
-		super[assembleMethodSymbol]();
-
-		initEventHandler.call(this);
-		initAutoInit.call(this);
-	}
-
-	/**
-	 * @deprecated 2023-06-25
-	 * @returns {Promise<never>|*}
-	 */
-	reload() {
-		return this.fetch();
-	}
-
-	/**
-	 * Fetches the data from the rest api
-	 * @returns {Promise<never>|*}
-	 */
-	fetch() {;
-		const opt = clone(this.getOption("read"));
-		this[dataSourceSymbol].setOption("read", opt);
-
-		let url = this.getOption("read.url");
-		const formatter = new Formatter(this.getOption("read.parameters"));
-
-		if (!url) {
-			return Promise.reject(new Error("No url defined"));
-		}
-
-		url = formatter.format(url);
-
-		this[dataSourceSymbol].setOption("read.url", url);
-
-		return new Promise((resolve, reject) => {
-			fireCustomEvent(this, "monster-datasource-fetch", {
-				datasource: this,
-			});
-
-			setTimeout(() => {
-				this[dataSourceSymbol]
-					.read()
-					.then((response) => {
-						fireCustomEvent(this, "monster-datasource-fetched", {
-							datasource: this,
-						});
-
-						resolve(response);
-					})
-					.catch((error) => {
-						fireCustomEvent(this, "monster-datasource-error", {
-							error: error,
-						});
-
-						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
-						reject(error);
-					});
-			}, 0);
-		});
-	}
-
-	/**
-	 *
-	 * @return {CSSStyleSheet[]}
-	 */
-	static getCSSStyleSheet() {
-		return [DatasourceStyleSheet];
-	}
-
-	/**
-	 * @private
-	 * @return {string}
-	 */
-	static getTag() {
-		return "monster-datasource-rest";
-	}
-
-	/**
-	 * This method activates the intersection observer manually.
-	 * For this purpose, the option `autoInit.intersectionObserver` must be set to `false`.
-	 *
-	 * @returns {Monster.Components.Datatable.Datasource.Rest}
-	 */
-	initIntersectionObserver() {
-		initIntersectionObserver.call(this);
-		return this;
-	}
-
-	/**
-	 * @private
-	 */
-	connectedCallback() {
-		super.connectedCallback();
-
-		setTimeout(() => {
-			if (this.getOption("features.filter", false) === true) {
-				initFilter.call(this);
-			}
-		}, 0);
-	}
-
-	/**
-	 * @private
-	 */
-	disconnectedCallback() {
-		super.disconnectedCallback();
-		removeFilter.call(this);
-	}
+    get defaults() {
+        const restOptions = new RestAPI().defaults;
+
+        restOptions.read.parameters = {
+            filter: undefined,
+            oderBy: undefined,
+            page: "1",
+        };
+
+        return Object.assign({}, super.defaults, restOptions, {
+            templates: {
+                main: getTemplate(),
+            },
+
+            features: {
+                autoInit: false,
+                filter: false,
+            },
+
+            autoInit: {
+                intersectionObserver: false,
+                oneTime: true,
+            },
+
+            filter: {
+                id: undefined,
+            },
+
+            datatable: {
+                id: undefined,
+            },
+
+            response: {
+                path: {
+                    message: "sys.message",
+                    code: "sys.code",
+                }
+            },
+        });
+    }
+
+    /**
+     *
+     * @param {string} page
+     * @param {string} query
+     * @param {string} orderBy
+     * @returns {Monster.Components.Datatable.Datasource.Rest}
+     */
+    setParameters({page, query, orderBy}) {
+        const parameters = this.getOption("read.parameters");
+        if (query !== undefined) {
+            parameters.query = `${query}`;
+            parameters.page = "1";
+        }
+
+        // after a query the page is set to 1, so if the page is not set, it is set to 1
+        if (page !== undefined) parameters.page = `${page}`;
+        if (orderBy !== undefined) parameters.order = `${orderBy}`;
+        this.setOption("read.parameters", parameters);
+        return this;
+    }
+
+    /**
+     *
+     * @return {Monster.Components.Form.Form}
+     */
+    [assembleMethodSymbol]() {
+        super[assembleMethodSymbol]();
+
+        initEventHandler.call(this);
+        initAutoInit.call(this);
+    }
+
+    /**
+     * @deprecated 2023-06-25
+     * @returns {Promise<never>|*}
+     */
+    reload() {
+        return this.fetch();
+    }
+
+    /**
+     * Fetches the data from the rest api
+     * @returns {Promise<never>|*}
+     */
+    fetch() {
+        const opt = clone(this.getOption("read"));
+        this[dataSourceSymbol].setOption("read", opt);
+
+        let url = this.getOption("read.url");
+        const formatter = new Formatter(this.getOption("read.parameters"));
+
+        if (!url) {
+            return Promise.reject(new Error("No url defined"));
+        }
+
+        url = formatter.format(url);
+
+        this[dataSourceSymbol].setOption("read.url", url);
+
+        return new Promise((resolve, reject) => {
+            fireCustomEvent(this, "monster-datasource-fetch", {
+                datasource: this,
+            });
+
+            setTimeout(() => {
+                this[dataSourceSymbol]
+                    .read()
+                    .then((response) => {
+                        fireCustomEvent(this, "monster-datasource-fetched", {
+                            datasource: this,
+                        });
+
+                        resolve(response);
+                    })
+                    .catch((error) => {
+                        fireCustomEvent(this, "monster-datasource-error", {
+                            error: error,
+                        });
+
+                        addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
+                        reject(error);
+                    });
+            }, 0);
+        });
+    }
+
+    /**
+     *
+     * @return {CSSStyleSheet[]}
+     */
+    static getCSSStyleSheet() {
+        return [DatasourceStyleSheet];
+    }
+
+    /**
+     * @private
+     * @return {string}
+     */
+    static getTag() {
+        return "monster-datasource-rest";
+    }
+
+    /**
+     * This method activates the intersection observer manually.
+     * For this purpose, the option `autoInit.intersectionObserver` must be set to `false`.
+     *
+     * @returns {Monster.Components.Datatable.Datasource.Rest}
+     */
+    initIntersectionObserver() {
+        initIntersectionObserver.call(this);
+        return this;
+    }
+
+    /**
+     * @private
+     */
+    connectedCallback() {
+        super.connectedCallback();
+
+        setTimeout(() => {
+            if (this.getOption("features.filter", false) === true) {
+                initFilter.call(this);
+            }
+        }, 0);
+    }
+
+    /**
+     * @private
+     */
+    disconnectedCallback() {
+        super.disconnectedCallback();
+        removeFilter.call(this);
+    }
 }
 
 /**
  * @private
  */
 function removeFilter() {
-	const filterID = this.getOption("filter.id", undefined);
-	if (!filterID) return;
+    const filterID = this.getOption("filter.id", undefined);
+    if (!filterID) return;
 
-	const filterControl = findElementWithIdUpwards(this, filterID);
+    const filterControl = findElementWithIdUpwards(this, filterID);
 
-	if (filterControl && this[filterObserverSymbol]) {
-		filterControl?.detachObserver(this[filterObserverSymbol]);
-	}
+    if (filterControl && this[filterObserverSymbol]) {
+        filterControl?.detachObserver(this[filterObserverSymbol]);
+    }
 }
 
 /**
  * @private
  */
 function initFilter() {
-	const filterID = this.getOption("filter.id", undefined);
-
-	if (!filterID)
-		throw new Error("filter feature is enabled but no filter id is defined");
-
-	const filterControl = findElementWithIdUpwards(this, filterID);
-	if (!filterControl)
-		throw new Error(
-			"filter feature is enabled but no filter control with id " +
-				filterID +
-				" is found",
-		);
-
-	this[filterObserverSymbol] = new Observer(() => {
-		const query = filterControl.getOption("query", undefined);
-		this.setParameters({ query: query });
-		this.fetch()
-			.then((response) => {
-				if (!(response instanceof Response)) {
-					throw new Error("Response is not an instance of Response");
-				}
-
-				if (response?.ok === true) {
-					this.dispatchEvent(new CustomEvent("reload", { bubbles: true }));
-					filterControl?.showSuccess();
-				}
-
-				response
-					.json()
-					.then((json) => {
-						const path = new Pathfinder(json);
-						const error = path.getVia(
-							this.getOption("response.errorMessagePath"),
-						);
-						if (error) {
-							filterControl?.showFailureMessage(error);
-							return;
-						}
-
-						filterControl?.showFailureMessage(e.message);
-					})
-					.catch((e) => {
-						filterControl?.showFailureMessage(e.message);
-					});
-			})
-			.catch((e) => {
-				this.dispatchEvent(
-					new CustomEvent("error", { bubbles: true, detail: e }),
-				);
-
-				if (!(e instanceof Error)) {
-					e = new Error(e);
-				}
-
-				filterControl?.showFailureMessage(e.message);
-				return Promise.reject(e);
-			});
-	});
-
-	filterControl.attachObserver(this[filterObserverSymbol]);
+    const filterID = this.getOption("filter.id", undefined);
+
+    if (!filterID)
+        throw new Error("filter feature is enabled but no filter id is defined");
+
+    const filterControl = findElementWithIdUpwards(this, filterID);
+    if (!filterControl)
+        throw new Error(
+            "filter feature is enabled but no filter control with id " +
+            filterID +
+            " is found",
+        );
+
+    this[filterObserverSymbol] = new Observer(() => {
+        const query = filterControl.getOption("query", undefined);
+        this.setParameters({query: query});
+        this.fetch()
+            .then((response) => {
+                if (!(response instanceof Response)) {
+                    throw new Error("Response is not an instance of Response");
+                }
+
+                if (response?.ok === true) {
+                    this.dispatchEvent(new CustomEvent("reload", {bubbles: true}));
+                    filterControl?.showSuccess();
+                }
+
+                if (response.bodyUsed === true) {
+                    return handleIntersectionObserver.call(this, response[rawDataSymbol], response, filterControl);
+                }
+
+                response
+                    .text()
+                    .then((jsonAsText) => {
+                        let json;
+                        try {
+                            json = JSON.parse(jsonAsText);
+                        } catch (e) {
+                            let message = e instanceof Error ? e.message : `${e}`;
+                            filterControl?.showFailureMessage(message);
+                            return Promise.reject(e);
+                        }
+
+                        return handleIntersectionObserver.call(this,json, response, filterControl);
+
+
+                    })
+                    .catch((e) => {
+                        filterControl?.showFailureMessage(e.message);
+                    });
+            })
+            .catch((e) => {
+                this.dispatchEvent(
+                    new CustomEvent("error", {bubbles: true, detail: e}),
+                );
+
+                if (!(e instanceof Error)) {
+                    e = new Error(e);
+                }
+
+                filterControl?.showFailureMessage(e.message);
+                return Promise.reject(e);
+            });
+    });
+
+    filterControl.attachObserver(this[filterObserverSymbol]);
+}
+
+function handleIntersectionObserver(json, response, filterControl) {
+
+    const path = new Pathfinder(json);
+
+    const codePath = this.getOption("response.path.code");
+
+    if (path.exists(codePath)) {
+        const code = `${path.getVia(codePath)}`;
+        if (code && code === "200") {
+            filterControl?.showSuccess();
+            return Promise.resolve(response);
+        }
+
+        const messagePath = this.getOption("response.path.message");
+        if (path.exists(messagePath)) {
+            const message = path.getVia(messagePath);
+            filterControl?.showFailureMessage(message);
+            return Promise.reject(new Error(message));
+        }
+
+        return Promise.reject(new Error("Response code is not 200"));
+    }
 }
 
 /**
  * @private
  */
 function initAutoInit() {
-	const autoInit = this.getOption("features.autoInit");
-	validateBoolean(autoInit);
+    const autoInit = this.getOption("features.autoInit");
+    validateBoolean(autoInit);
 
-	if (autoInit !== true) return;
+    if (autoInit !== true) return;
 
-	if (this.getOption("autoInit.intersectionObserver") === true) {
-		initIntersectionObserver.call(this);
-		return;
-	}
+    if (this.getOption("autoInit.intersectionObserver") === true) {
+        initIntersectionObserver.call(this);
+        return;
+    }
 
-	setTimeout(() => {
-		this.fetch().catch(() => {});
-	}, 0);
+    setTimeout(() => {
+        this.fetch().catch(() => {
+        });
+    }, 0);
 }
 
 function initEventHandler() {
-	this[intersectionObserverHandlerSymbol] = (entries) => {
-		entries.forEach((entry) => {
-			if (entry.isIntersecting) {
-				if (entry.intersectionRatio > 0) {
-					this.fetch();
-				}
-
-				// only load once
-				if (
-					this.getOption("autoInit.oneTime") === true &&
-					this[intersectionObserverObserverSymbol] !== undefined
-				) {
-					this[intersectionObserverObserverSymbol].unobserve(this);
-				}
-			}
-		});
-	};
+    this[intersectionObserverHandlerSymbol] = (entries) => {
+        entries.forEach((entry) => {
+            if (entry.isIntersecting) {
+                if (entry.intersectionRatio > 0) {
+                    this.fetch();
+                }
+
+                // only load once
+                if (
+                    this.getOption("autoInit.oneTime") === true &&
+                    this[intersectionObserverObserverSymbol] !== undefined
+                ) {
+                    this[intersectionObserverObserverSymbol].unobserve(this);
+                }
+            }
+        });
+    };
 }
 
 function initIntersectionObserver() {
-	this.classList.add("intersection-observer");
-
-	const options = {
-		root: null,
-		rootMargin: "0px",
-		threshold: 0.1,
-	};
-
-	this[intersectionObserverObserverSymbol] = new IntersectionObserver(
-		this[intersectionObserverHandlerSymbol],
-		options,
-	);
-	this[intersectionObserverObserverSymbol].observe(this);
+    this.classList.add("intersection-observer");
+
+    const options = {
+        root: null,
+        rootMargin: "0px",
+        threshold: 0.1,
+    };
+
+    this[intersectionObserverObserverSymbol] = new IntersectionObserver(
+        this[intersectionObserverHandlerSymbol],
+        options,
+    );
+    this[intersectionObserverObserverSymbol].observe(this);
 }
 
 /**
@@ -422,8 +466,8 @@ function initIntersectionObserver() {
  * @return {string}
  */
 function getTemplate() {
-	// language=HTML
-	return `
+    // language=HTML
+    return `
         <slot></slot>`;
 }
 
diff --git a/source/components/datatable/filter.mjs b/source/components/datatable/filter.mjs
index 60400c11a..81d45b026 100644
--- a/source/components/datatable/filter.mjs
+++ b/source/components/datatable/filter.mjs
@@ -254,7 +254,7 @@ class Filter extends CustomElement {
 				},
 			},
 
-			query: "",
+			query: undefined,
 			defaultQuery: "",
 		});
 	}
@@ -441,7 +441,6 @@ function initFilter() {
  */
 function escapeAttributeValue(input) {
 	if (input === undefined || input === null) {
-		debugger;
 		return input;
 	}
 
@@ -686,11 +685,16 @@ function initEventHandler() {
 }
 
 function initTabEvents() {
+	
 	this[filterTabElementSymbol].addEventListener(
 		"monster-tab-changed",
 		(event) => {
+			
 			const query = event?.detail?.data?.["data-monster-query"];
-			this.setOption("query", query);
+			const q = this.getOption("query");
+			if (query !== q) {
+				this.setOption("query", query);
+			}
 		},
 	);
 
diff --git a/source/data/datasource/server/restapi.mjs b/source/data/datasource/server/restapi.mjs
index bb80fbc88..dbce0b746 100644
--- a/source/data/datasource/server/restapi.mjs
+++ b/source/data/datasource/server/restapi.mjs
@@ -123,6 +123,7 @@ class RestAPI extends Server {
 	 * @throws {Error} the data cannot be read
 	 */
 	read() {
+
 		let init = this.getOption("read.init");
 		if (!isObject(init)) init = {};
 		if (!init["method"]) init["method"] = "GET";
diff --git a/source/dom/customelement.mjs b/source/dom/customelement.mjs
index 634eb6c49..6470a42d5 100644
--- a/source/dom/customelement.mjs
+++ b/source/dom/customelement.mjs
@@ -465,7 +465,7 @@ class CustomElement extends HTMLElement {
 	 * @return {*}
 	 * @since 1.10.0
 	 */
-	getOption(path, defaultValue) {
+	getOption(path, defaultValue=undefined) {
 		let value;
 
 		try {
-- 
GitLab