diff --git a/README.md b/README.md
index 645f543..c23d2cb 100644
--- a/README.md
+++ b/README.md
@@ -13,17 +13,17 @@ This extension is a wrapper for [https://github.com/mmomtchev/sqlite-wasm-http].
The http backend is kept open for the lifetime of the element having the corresponding `hx-db` attribute.
Following events are emitted:
-- `htmx:sql:loadstart` when a query execution begins
+- `htmx:xhr:loadstart` when a query execution begins
- `event.detail.xhr.db` is a promise for SQLite.Promiser
-- `htmx:sql:progress` for each row of the result
+- `htmx:xhr:progress` for each row of the result
- `event.detail.xhr.db` is a promise for SQLite.Promiser
- - `event.detail.xhr.response` is the received data row
-- `htmx:sql:loadend` when a query execution is finished (all rows are received) but before any swapping is
+ - `event.detail.xhr.responseJSON` is the received data row as JSON
+ - `event.detail.xhr.response` is the received data row as stringified JSON
+- `htmx:xhr:loadend` when a query execution is finished (all rows are received) but before any swapping is
performed.
- `event.detail.xhr.db` is a promise for SQLite.Promiser
- - `event.detail.xhr.response` is all received data rows
-
-In addition, `htmx:beforeOnLoad` will contain `event.detail.xhr.response` with an array of all received data rows.
+ - `event.detail.xhr.responseJSON` is all received data rows as JSON
+ - `event.detail.xhr.response` is all received data rows as stringified JSON
### Install
@@ -40,18 +40,26 @@ Include the following in your page:
```html
-
+
```
### Usage
-`hx-sql` elements need a useless `hx-boost` attribute to get "registered" to Htmx. Please let me know if there is a better way to do this.
+Use one of attributes
+- hx-get="SELECT ..."
+- hx-put="UPDATE ..."
+- hx-delete="DELETE ..."
+- hx-post="INSERT ..."
+- hx-post="ALTER ..."
+- hx-post="CREATE ..."
+- hx-post="DROP ..."
+- hx-post="TRUNCATE ..."
#### Use a database relative to current page over HTTP
```html
-
+
```
@@ -59,7 +67,7 @@ Include the following in your page:
```html
-
+
```
@@ -67,7 +75,7 @@ Include the following in your page:
```html
-
+
```
@@ -82,24 +90,23 @@ Include the following in your page:
-
+
```
#### Handle response rows one-by-one with javascript
```html
-
+
```
#### Handle the whole result with javascript
```html
-
+
```
@@ -107,13 +114,13 @@ Include the following in your page:
```html
-
+
```
-#### Use _value_ as the query if hx-sql is empty
+#### Use _value_ as the query if path is just "this.value"
```html
-
+
name
age
@@ -124,5 +131,5 @@ Include the following in your page:
#### Override default config
```html
-
+
```
diff --git a/dist/sqlite.js b/dist/sqlite.js
index f308dfc..fbe3bbf 100644
--- a/dist/sqlite.js
+++ b/dist/sqlite.js
@@ -7,137 +7,7 @@ Extension to use SQLite database backend for Htmx over:
(function(){
var api;
var httpBackendConfig;
-
- // lot's of copying from htmx source. It should probably export some of this stuff as functions in internal API?
- var getParameters = function (elt) {
- var results = api.getInputValues(elt, 'post');
- var rawParameters = results.values;
- var expressionVars = api.getExpressionVars(elt);
- var allParameters = api.mergeObjects(rawParameters, expressionVars);
- var filteredParameters = api.filterValues(allParameters, elt);
- return filteredParameters;
- };
-
- // lot's of copying from htmx source. It should probably export some of this stuff as functions in internal API?
- var swapAndSettle = function(data, elt, responseInfo) {
- var target = api.getTarget(elt);
- if (target == null || target == api.DUMMY_ELT) {
- api.triggerErrorEvent(elt, 'htmx:targetError', {target: api.getAttributeValue(elt, "hx-target")});
- return;
- }
-
- var beforeSwapDetails = {shouldSwap: true, target: target, elt: elt};
- if (!api.triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return;
- target = beforeSwapDetails.target;
-
- responseInfo.target = target;
- responseInfo.failed = false;
- responseInfo.successful = true;
-
- var serverResponse = data.length == 0 ? "" : JSON.stringify(data);
- if (beforeSwapDetails.shouldSwap) {
- api.withExtensions(elt, function (extension) {
- serverResponse = extension.transformResponse(serverResponse, undefined, elt);
- });
-
- var swapSpec = api.getSwapSpecification(elt, undefined);
-
- target.classList.add(htmx.config.swappingClass);
-
- var doSwap = function () {
- try {
- var activeElt = document.activeElement;
- var selectionInfo = {};
- try {
- selectionInfo = {
- elt: activeElt,
- start: activeElt ? activeElt.selectionStart : null,
- end: activeElt ? activeElt.selectionEnd : null
- };
- } catch (e) {
- // safari issue - see https://github.com/microsoft/playwright/issues/5894
- }
-
- var settleInfo = api.makeSettleInfo(target);
- api.selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, undefined);
-
- if (selectionInfo.elt &&
- !api.bodyContains(selectionInfo.elt) &&
- api.getRawAttribute(selectionInfo.elt, "id")) {
- var newActiveElt = document.getElementById(api.getRawAttribute(selectionInfo.elt, "id"));
- var focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll };
- if (newActiveElt) {
- if (selectionInfo.start && newActiveElt.setSelectionRange) {
- try {
- newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
- } catch (e) {
- // the setSelectionRange method is present on fields that don't support it, so just let this fail
- }
- }
- newActiveElt.focus(focusOptions);
- }
- }
-
- target.classList.remove(htmx.config.swappingClass);
- settleInfo.elts.forEach(function (elt) {
- if (elt.classList) {
- elt.classList.add(htmx.config.settlingClass);
- }
- api.triggerEvent(elt, 'htmx:afterSwap', responseInfo);
- });
-
- var doSettle = function () {
- settleInfo.tasks.forEach(function (task) {
- task.call();
- });
- settleInfo.elts.forEach(function (elt) {
- if (elt.classList) {
- elt.classList.remove(htmx.config.settlingClass);
- }
- api.triggerEvent(elt, 'htmx:afterSettle', responseInfo);
- });
- }
-
- if (swapSpec.settleDelay > 0) {
- setTimeout(doSettle, swapSpec.settleDelay)
- } else {
- doSettle();
- }
- } catch (e) {
- api.triggerErrorEvent(elt, 'htmx:swapError', responseInfo);
- throw e;
- }
- };
-
- var shouldTransition = htmx.config.globalViewTransitions
- if(swapSpec.hasOwnProperty('transition')){
- shouldTransition = swapSpec.transition;
- }
-
- if(shouldTransition &&
- api.triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
- typeof Promise !== "undefined" && document.startViewTransition){
- var settlePromise = new Promise(function (_resolve, _reject) {
- settleResolve = _resolve;
- settleReject = _reject;
- });
- var innerDoSwap = doSwap;
- doSwap = function() {
- document.startViewTransition(function () {
- innerDoSwap();
- return settlePromise;
- });
- }
- }
-
-
- if (swapSpec.swapDelay > 0) {
- setTimeout(doSwap, swapSpec.swapDelay)
- } else {
- doSwap();
- }
- }
- };
+ var sqlConfig;
htmx.defineExtension('sqlite', {
init: function (internalAPI) {
@@ -147,102 +17,132 @@ Extension to use SQLite database backend for Htmx over:
timeout: 10000, // 10s
cacheSize: 4096 // 4 MB
};
+ sqlConfig = {
+ rowMode: 'object'
+ }
},
+
onEvent: function (name, evt) {
- if (name === "htmx:afterProcessNode") {
+ if (name === "htmx:beforeRequest") {
let elt = evt.detail.elt;
- if (elt.hasAttribute('hx-sql')) {
- var triggerSpecs = api.getTriggerSpecs(elt);
- triggerSpecs.forEach(function(triggerSpec) {
- var nodeData = api.getInternalData(elt);
- api.addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
- if (htmx.closest(elt, htmx.config.disableSelector)) {
- cleanUpElement(elt);
- return;
- }
-
- // use Htmx parameters as bind variables
- var binds = {};
- Object.entries(getParameters(elt)).forEach(function([k,v]) {
- binds['$' + k] = v;
- });
+ var rc = evt.detail.requestConfig;
+ var sql;
+ if (rc.path === 'this.value' ||
+ rc.verb.toUpperCase() === 'GET' && rc.path.toUpperCase().startsWith('SELECT ') ||
+ rc.verb.toUpperCase() === 'PUT' && rc.path.toUpperCase().startsWith('UPDATE ') ||
+ rc.verb.toUpperCase() === 'DELETE' && rc.path.toUpperCase().startsWith('DELETE ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('INSERT ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('ALTER ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('CREATE ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('DROP ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('TRUNCATE ')) {
+ sql = rc.path;
+ } else {
+ return true;
+ }
- var sql = elt.getAttribute('hx-sql');
- if (sql == "") {
- // use form field value if hx-sql is empty
- sql = elt.value;
- if (!htmx.closest(elt, '[hx-target]')) {
- throw new Error("Attribute 'hx-target' is required when 'hx-sql' is empty");
- }
- }
+ if (sql === "this.value") {
+ // use field value if path is just "SELECT"
+ sql = elt.value;
+ if (!evt.detail.target) {
+ throw new Error("Attribute 'hx-target' is required when value is used is empty");
+ }
+ }
- var dbElem = htmx.closest(elt, '[hx-db]');
- if (!dbElem) {
- throw new Error("Attribute 'hx-db' is required in the ancestor hierarchy");
- }
+ var dbElem = htmx.closest(elt, '[hx-db]');
+ if (!dbElem) {
+ throw new Error("Attribute 'hx-db' is required in the ancestor hierarchy");
+ }
+ var dbURI = dbElem.getAttribute('hx-db');
+
+ var onload = evt.detail.xhr.onload;
+ var onerror = evt.detail.xhr.onerror;
+ evt.detail.xhr = {
+ status: 200,
+ getAllResponseHeaders: function() {
+ return this.getResponseHeader("Content-Type:application/json");
+ },
+ getResponseHeader: function(headerName) {
+ if (headerName.toLowerCase() === "content-type") {
+ return "application/json";
+ }
+ return undefined;
+ }
+ };
- var configElem = htmx.closest(elt, '[hx-db-config]');
- var conf = configElem ? JSON.parse(configElem.getAttribute('hx-db-config')) : {};
- var config = { ...httpBackendConfig,
- ...conf
- };
- var dbURI = dbElem.getAttribute('hx-db');
-
- var backend;
- if (dbElem._htmx_sqlite_http_backend) {
- backend = dbElem._htmx_sqlite_http_backend;
- } else {
- backend = dbURI.match(/^https?:/) ? { http: sqliteWasmHttp.createHttpBackend(config) }
- : {};
- dbElem._htmx_sqlite_http_backend = backend;
- }
- dbElem.addEventListener('htmx:beforeCleanupElement', function(ev) {
- if (ev.detail.elt == dbElem && dbElem._htmx_sqlite_http_backend && dbElem._htmx_sqlite_http_backend.http) {
- dbElem._htmx_sqlite_http_backend.http.close();
- delete dbElem._htmx_sqlite_http_backend;
- }
- });
+ // use Htmx parameters as bind variables
+ var binds = {};
+ Object.entries(evt.detail.requestConfig.parameters).forEach(function([k,v]) {
+ binds['$' + k] = v;
+ });
- var allRows = [];
- sqliteWasmHttp.createSQLiteThread(backend)
- .then(function(db) {
- db('open', {
- filename: encodeURI(dbURI.match(/^https?:\/\//) ? dbURI : dbURI.replace(/^opfs:/, '').replace(/^https?:/, 'file:')),
- vfs: dbURI.replace(/:.*/,'').replace('https', 'http')
- });
- return db;
- })
- .then(function(db) {
- api.triggerEvent(elt, 'htmx:sql:loadstart', { elt: elt, xhr: { db: db } });
- db('exec', {
- sql: sql,
- bind: binds,
- rowMode: "object",
- callback: function(data) {
- if (data.row) {
- allRows.push(data.row);
- api.triggerEvent(elt, 'htmx:sql:progress', { elt: elt, xhr: { db: db, response: data.row } });
- } else {
- api.triggerEvent(elt, 'htmx:sql:loadend', { elt: elt, xhr: { db: db } });
+ var configElem = htmx.closest(elt, '[hx-request]');
+ var conf = configElem ? JSON.parse(configElem.getAttribute('hx-request')) : {};
+ var config = {
+ ...httpBackendConfig,
+ ...sqlConfig,
+ ...conf
+ };
+
+ var backend;
+ if (dbElem._htmx_sqlite_http_backend) {
+ backend = dbElem._htmx_sqlite_http_backend;
+ } else {
+ backend = dbURI.match(/^https?:/) ? { http: sqliteWasmHttp.createHttpBackend(config) }
+ : {};
+ dbElem._htmx_sqlite_http_backend = backend;
+ }
+ dbElem.addEventListener('htmx:beforeCleanupElement', function(ev) {
+ if (ev.detail.elt == dbElem && dbElem._htmx_sqlite_http_backend && dbElem._htmx_sqlite_http_backend.http) {
+ dbElem._htmx_sqlite_http_backend.http.close();
+ delete dbElem._htmx_sqlite_http_backend;
+ }
+ });
- db('close', {}) // This closes the DB connection
- .then(function() {
- db.close(); // This terminates the SQLite worker
- });
-
- var responseInfo = { elt: elt, xhr: { db: db, response: allRows }, target: api.getTarget(elt) };
- var cancel = !api.triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo);
- if (responseInfo.target && !cancel) {
- swapAndSettle(allRows, elt, responseInfo);
- }
- }
- }
- });
- });
+ var allRows = [];
+ sqliteWasmHttp.createSQLiteThread(backend)
+ .then(function(db) {
+ db('open', {
+ filename: encodeURI(dbURI.match(/^https?:\/\//) ? dbURI : dbURI.replace(/^opfs:/, '').replace(/^https?:/, 'file:')),
+ vfs: dbURI.replace(/:.*/,'').replace('https', 'http')
+ });
+ return db;
+ })
+ .then(function(db) {
+ api.triggerEvent(elt, 'htmx:xhr:loadstart', { elt: elt, xhr: {...evt.detail.xhr, db: db} });
+ db('exec', {
+ sql: sql,
+ bind: binds,
+ rowMode: config.rowMode,
+ callback: function(data) {
+ if (data.row) {
+ allRows.push(data.row);
+ api.triggerEvent(elt, 'htmx:xhr:progress', { elt: elt, xhr: { ...evt.detail.xhr, db: db, response: JSON.stringify(data.row), responseJSON: data.row } });
+ } else {
+ api.triggerEvent(elt, 'htmx:xhr:loadend', { elt: elt, xhr: { ...evt.detail.xhr, db: db, response: allRows.length == 0 ? '' : JSON.stringify(allRows), responseJSON: allRows } });
+
+ db('close', {}) // This closes the DB connection
+ .then(function() {
+ db.close(); // This terminates the SQLite worker
+ });
+
+ evt.detail.xhr.responseJSON = allRows;
+ evt.detail.xhr.response = allRows.length == 0 ? '' : JSON.stringify(allRows);
+ onload();
+ }
+ }
+ })
+ .catch(function(data) {
+ if (data.result) {
+ evt.detail.error = data.result.message;
+ }
+ onerror();
});
});
- }
+
+ // return false to stop Htmx processing. We are calling load() ourselves.
+ return false;
}
}
});
diff --git a/dist/sqlite.min.js b/dist/sqlite.min.js
index bd4096e..72653e0 100644
--- a/dist/sqlite.min.js
+++ b/dist/sqlite.min.js
@@ -1 +1 @@
-(function(){var f;var u;var g=function(t){var e=f.getInputValues(t,"post");var r=e.values;var n=f.getExpressionVars(t);var a=f.mergeObjects(r,n);var i=f.filterValues(a,t);return i};var d=function(t,s,l){var o=f.getTarget(s);if(o==null||o==f.DUMMY_ELT){f.triggerErrorEvent(s,"htmx:targetError",{target:f.getAttributeValue(s,"hx-target")});return}var e={shouldSwap:true,target:o,elt:s};if(!f.triggerEvent(o,"htmx:beforeSwap",e))return;o=e.target;l.target=o;l.failed=false;l.successful=true;var c=t.length==0?"":JSON.stringify(t);if(e.shouldSwap){f.withExtensions(s,function(t){c=t.transformResponse(c,undefined,s)});var h=f.getSwapSpecification(s,undefined);o.classList.add(htmx.config.swappingClass);var r=function(){try{var t=document.activeElement;var e={};try{e={elt:t,start:t?t.selectionStart:null,end:t?t.selectionEnd:null}}catch(t){}var r=f.makeSettleInfo(o);f.selectAndSwap(h.swapStyle,o,s,c,r,undefined);if(e.elt&&!f.bodyContains(e.elt)&&f.getRawAttribute(e.elt,"id")){var n=document.getElementById(f.getRawAttribute(e.elt,"id"));var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!htmx.config.defaultFocusScroll};if(n){if(e.start&&n.setSelectionRange){try{n.setSelectionRange(e.start,e.end)}catch(t){}}n.focus(a)}}o.classList.remove(htmx.config.swappingClass);r.elts.forEach(function(t){if(t.classList){t.classList.add(htmx.config.settlingClass)}f.triggerEvent(t,"htmx:afterSwap",l)});var i=function(){r.tasks.forEach(function(t){t.call()});r.elts.forEach(function(t){if(t.classList){t.classList.remove(htmx.config.settlingClass)}f.triggerEvent(t,"htmx:afterSettle",l)})};if(h.settleDelay>0){setTimeout(i,h.settleDelay)}else{i()}}catch(t){f.triggerErrorEvent(s,"htmx:swapError",l);throw t}};var n=htmx.config.globalViewTransitions;if(h.hasOwnProperty("transition")){n=h.transition}if(n&&f.triggerEvent(s,"htmx:beforeTransition",l)&&typeof Promise!=="undefined"&&document.startViewTransition){var a=new Promise(function(t,e){settleResolve=t;settleReject=e});var i=r;r=function(){document.startViewTransition(function(){i();return a})}}if(h.swapDelay>0){setTimeout(r,h.swapDelay)}else{r()}}};htmx.defineExtension("sqlite",{init:function(t){f=t;u={maxPageSize:4096,timeout:1e4,cacheSize:4096}},onEvent:function(t,e){if(t==="htmx:afterProcessNode"){let r=e.detail.elt;if(r.hasAttribute("hx-sql")){var n=f.getTriggerSpecs(r);n.forEach(function(t){var e=f.getInternalData(r);f.addTriggerHandler(r,t,e,function(a,t){if(htmx.closest(a,htmx.config.disableSelector)){cleanUpElement(a);return}var r={};Object.entries(g(a)).forEach(function([t,e]){r["$"+t]=e});var e=a.getAttribute("hx-sql");if(e==""){e=a.value;if(!htmx.closest(a,"[hx-target]")){throw new Error("Attribute 'hx-target' is required when 'hx-sql' is empty")}}var n=htmx.closest(a,"[hx-db]");if(!n){throw new Error("Attribute 'hx-db' is required in the ancestor hierarchy")}var i=htmx.closest(a,"[hx-db-config]");var s=i?JSON.parse(i.getAttribute("hx-db-config")):{};var l={...u,...s};var o=n.getAttribute("hx-db");var c;if(n._htmx_sqlite_http_backend){c=n._htmx_sqlite_http_backend}else{c=o.match(/^https?:/)?{http:sqliteWasmHttp.createHttpBackend(l)}:{};n._htmx_sqlite_http_backend=c}n.addEventListener("htmx:beforeCleanupElement",function(t){if(t.detail.elt==n&&n._htmx_sqlite_http_backend&&n._htmx_sqlite_http_backend.http){n._htmx_sqlite_http_backend.http.close();delete n._htmx_sqlite_http_backend}});var h=[];sqliteWasmHttp.createSQLiteThread(c).then(function(t){t("open",{filename:encodeURI(o.match(/^https?:\/\//)?o:o.replace(/^opfs:/,"").replace(/^https?:/,"file:")),vfs:o.replace(/:.*/,"").replace("https","http")});return t}).then(function(n){f.triggerEvent(a,"htmx:sql:loadstart",{elt:a,xhr:{db:n}});n("exec",{sql:e,bind:r,rowMode:"object",callback:function(t){if(t.row){h.push(t.row);f.triggerEvent(a,"htmx:sql:progress",{elt:a,xhr:{db:n,response:t.row}})}else{f.triggerEvent(a,"htmx:sql:loadend",{elt:a,xhr:{db:n}});n("close",{}).then(function(){n.close()});var e={elt:a,xhr:{db:n,response:h},target:f.getTarget(a)};var r=!f.triggerEvent(a,"htmx:beforeOnLoad",e);if(e.target&&!r){d(h,a,e)}}}})})})})}}}})})();
\ No newline at end of file
+(function(){var f;var v;var b;htmx.defineExtension("sqlite",{init:function(e){f=e;v={maxPageSize:4096,timeout:1e4,cacheSize:4096};b={rowMode:"object"}},onEvent:function(e,a){if(e==="htmx:beforeRequest"){let r=a.detail.elt;var t=a.detail.requestConfig;var s;if(t.path==="this.value"||t.verb.toUpperCase()==="GET"&&t.path.toUpperCase().startsWith("SELECT ")||t.verb.toUpperCase()==="PUT"&&t.path.toUpperCase().startsWith("UPDATE ")||t.verb.toUpperCase()==="DELETE"&&t.path.toUpperCase().startsWith("DELETE ")||t.verb.toUpperCase()==="POST"&&t.path.toUpperCase().startsWith("INSERT ")||t.verb.toUpperCase()==="POST"&&t.path.toUpperCase().startsWith("ALTER ")||t.verb.toUpperCase()==="POST"&&t.path.toUpperCase().startsWith("CREATE ")||t.verb.toUpperCase()==="POST"&&t.path.toUpperCase().startsWith("DROP ")||t.verb.toUpperCase()==="POST"&&t.path.toUpperCase().startsWith("TRUNCATE ")){s=t.path}else{return true}if(s==="this.value"){s=r.value;if(!a.detail.target){throw new Error("Attribute 'hx-target' is required when value is used is empty")}}var i=htmx.closest(r,"[hx-db]");if(!i){throw new Error("Attribute 'hx-db' is required in the ancestor hierarchy")}var n=i.getAttribute("hx-db");var h=a.detail.xhr.onload;var p=a.detail.xhr.onerror;a.detail.xhr={status:200,getAllResponseHeaders:function(){return this.getResponseHeader("Content-Type:application/json")},getResponseHeader:function(e){if(e.toLowerCase()==="content-type"){return"application/json"}return undefined}};var o={};Object.entries(a.detail.requestConfig.parameters).forEach(function([e,t]){o["$"+e]=t});var l=htmx.closest(r,"[hx-request]");var d=l?JSON.parse(l.getAttribute("hx-request")):{};var c={...v,...b,...d};var u;if(i._htmx_sqlite_http_backend){u=i._htmx_sqlite_http_backend}else{u=n.match(/^https?:/)?{http:sqliteWasmHttp.createHttpBackend(c)}:{};i._htmx_sqlite_http_backend=u}i.addEventListener("htmx:beforeCleanupElement",function(e){if(e.detail.elt==i&&i._htmx_sqlite_http_backend&&i._htmx_sqlite_http_backend.http){i._htmx_sqlite_http_backend.http.close();delete i._htmx_sqlite_http_backend}});var x=[];sqliteWasmHttp.createSQLiteThread(u).then(function(e){e("open",{filename:encodeURI(n.match(/^https?:\/\//)?n:n.replace(/^opfs:/,"").replace(/^https?:/,"file:")),vfs:n.replace(/:.*/,"").replace("https","http")});return e}).then(function(t){f.triggerEvent(r,"htmx:xhr:loadstart",{elt:r,xhr:{...a.detail.xhr,db:t}});t("exec",{sql:s,bind:o,rowMode:c.rowMode,callback:function(e){if(e.row){x.push(e.row);f.triggerEvent(r,"htmx:xhr:progress",{elt:r,xhr:{...a.detail.xhr,db:t,response:JSON.stringify(e.row),responseJSON:e.row}})}else{f.triggerEvent(r,"htmx:xhr:loadend",{elt:r,xhr:{...a.detail.xhr,db:t,response:x.length==0?"":JSON.stringify(x),responseJSON:x}});t("close",{}).then(function(){t.close()});a.detail.xhr.responseJSON=x;a.detail.xhr.response=x.length==0?"":JSON.stringify(x);h()}}}).catch(function(e){if(e.result){a.detail.error=e.result.message}p()})});return false}}})})();
\ No newline at end of file
diff --git a/package.json b/package.json
index 9cdc0d2..2bfd623 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "htmx-sqlite",
- "version": "1.0.0",
+ "version": "1.9.0",
"description": "Htmx extension to use SQLite database backend over HTTP or OPFS",
"author": "Jyri-Matti Lähteenmäki ",
"keywords": [
diff --git a/src/sqlite.js b/src/sqlite.js
index d39a5c2..fbe3bbf 100644
--- a/src/sqlite.js
+++ b/src/sqlite.js
@@ -7,137 +7,7 @@ Extension to use SQLite database backend for Htmx over:
(function(){
var api;
var httpBackendConfig;
-
- // lot's of copying from htmx source. It should probably export some of this stuff as functions in internal API?
- var getParameters = function (elt) {
- var results = api.getInputValues(elt, 'post');
- var rawParameters = results.values;
- var expressionVars = api.getExpressionVars(elt);
- var allParameters = api.mergeObjects(rawParameters, expressionVars);
- var filteredParameters = api.filterValues(allParameters, elt);
- return filteredParameters;
- };
-
- // lot's of copying from htmx source. It should probably export some of this stuff as functions in internal API?
- var swapAndSettle = function(data, elt, responseInfo) {
- var target = api.getTarget(elt);
- if (target == null || target == api.DUMMY_ELT) {
- api.triggerErrorEvent(elt, 'htmx:targetError', {target: api.getAttributeValue(elt, "hx-target")});
- return;
- }
-
- var beforeSwapDetails = {shouldSwap: true, target: target, elt: elt};
- if (!api.triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return;
- target = beforeSwapDetails.target;
-
- responseInfo.target = target;
- responseInfo.failed = false;
- responseInfo.successful = true;
-
- var serverResponse = data.length == 0 ? "" : JSON.stringify(data);
- if (beforeSwapDetails.shouldSwap) {
- api.withExtensions(elt, function (extension) {
- serverResponse = extension.transformResponse(serverResponse, undefined, elt);
- });
-
- var swapSpec = api.getSwapSpecification(elt, undefined);
-
- target.classList.add(htmx.config.swappingClass);
-
- var doSwap = function () {
- try {
- var activeElt = document.activeElement;
- var selectionInfo = {};
- try {
- selectionInfo = {
- elt: activeElt,
- start: activeElt ? activeElt.selectionStart : null,
- end: activeElt ? activeElt.selectionEnd : null
- };
- } catch (e) {
- // safari issue - see https://github.com/microsoft/playwright/issues/5894
- }
-
- var settleInfo = api.makeSettleInfo(target);
- api.selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, undefined);
-
- if (selectionInfo.elt &&
- !api.bodyContains(selectionInfo.elt) &&
- api.getRawAttribute(selectionInfo.elt, "id")) {
- var newActiveElt = document.getElementById(api.getRawAttribute(selectionInfo.elt, "id"));
- var focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll };
- if (newActiveElt) {
- if (selectionInfo.start && newActiveElt.setSelectionRange) {
- try {
- newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
- } catch (e) {
- // the setSelectionRange method is present on fields that don't support it, so just let this fail
- }
- }
- newActiveElt.focus(focusOptions);
- }
- }
-
- target.classList.remove(htmx.config.swappingClass);
- settleInfo.elts.forEach(function (elt) {
- if (elt.classList) {
- elt.classList.add(htmx.config.settlingClass);
- }
- api.triggerEvent(elt, 'htmx:afterSwap', responseInfo);
- });
-
- var doSettle = function () {
- settleInfo.tasks.forEach(function (task) {
- task.call();
- });
- settleInfo.elts.forEach(function (elt) {
- if (elt.classList) {
- elt.classList.remove(htmx.config.settlingClass);
- }
- api.triggerEvent(elt, 'htmx:afterSettle', responseInfo);
- });
- }
-
- if (swapSpec.settleDelay > 0) {
- setTimeout(doSettle, swapSpec.settleDelay)
- } else {
- doSettle();
- }
- } catch (e) {
- api.triggerErrorEvent(elt, 'htmx:swapError', responseInfo);
- throw e;
- }
- };
-
- var shouldTransition = htmx.config.globalViewTransitions
- if(swapSpec.hasOwnProperty('transition')){
- shouldTransition = swapSpec.transition;
- }
-
- if(shouldTransition &&
- api.triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
- typeof Promise !== "undefined" && document.startViewTransition){
- var settlePromise = new Promise(function (_resolve, _reject) {
- settleResolve = _resolve;
- settleReject = _reject;
- });
- var innerDoSwap = doSwap;
- doSwap = function() {
- document.startViewTransition(function () {
- innerDoSwap();
- return settlePromise;
- });
- }
- }
-
-
- if (swapSpec.swapDelay > 0) {
- setTimeout(doSwap, swapSpec.swapDelay)
- } else {
- doSwap();
- }
- }
- };
+ var sqlConfig;
htmx.defineExtension('sqlite', {
init: function (internalAPI) {
@@ -147,102 +17,132 @@ Extension to use SQLite database backend for Htmx over:
timeout: 10000, // 10s
cacheSize: 4096 // 4 MB
};
+ sqlConfig = {
+ rowMode: 'object'
+ }
},
+
onEvent: function (name, evt) {
- if (name === "htmx:afterProcessNode") {
+ if (name === "htmx:beforeRequest") {
let elt = evt.detail.elt;
- if (elt.hasAttribute('hx-sql')) {
- var triggerSpecs = api.getTriggerSpecs(elt);
- triggerSpecs.forEach(function(triggerSpec) {
- var nodeData = api.getInternalData(elt);
- api.addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
- if (htmx.closest(elt, htmx.config.disableSelector)) {
- cleanUpElement(elt);
- return;
- }
-
- // use Htmx parameters as bind variables
- var binds = {};
- Object.entries(getParameters(elt)).forEach(function([k,v]) {
- binds['$' + k] = v;
- });
+ var rc = evt.detail.requestConfig;
+ var sql;
+ if (rc.path === 'this.value' ||
+ rc.verb.toUpperCase() === 'GET' && rc.path.toUpperCase().startsWith('SELECT ') ||
+ rc.verb.toUpperCase() === 'PUT' && rc.path.toUpperCase().startsWith('UPDATE ') ||
+ rc.verb.toUpperCase() === 'DELETE' && rc.path.toUpperCase().startsWith('DELETE ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('INSERT ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('ALTER ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('CREATE ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('DROP ') ||
+ rc.verb.toUpperCase() === 'POST' && rc.path.toUpperCase().startsWith('TRUNCATE ')) {
+ sql = rc.path;
+ } else {
+ return true;
+ }
- var sql = elt.getAttribute('hx-sql');
- if (sql == "") {
- // use form field value if hx-sql is empty
- sql = elt.value;
- if (!htmx.closest(elt, '[hx-target]')) {
- throw new Error("Attribute 'hx-target' is required when 'hx-sql' is empty");
- }
- }
+ if (sql === "this.value") {
+ // use field value if path is just "SELECT"
+ sql = elt.value;
+ if (!evt.detail.target) {
+ throw new Error("Attribute 'hx-target' is required when value is used is empty");
+ }
+ }
- var dbElem = htmx.closest(elt, '[hx-db]');
- if (!dbElem) {
- throw new Error("Attribute 'hx-db' is required in the ancestor hierarchy");
- }
+ var dbElem = htmx.closest(elt, '[hx-db]');
+ if (!dbElem) {
+ throw new Error("Attribute 'hx-db' is required in the ancestor hierarchy");
+ }
+ var dbURI = dbElem.getAttribute('hx-db');
+
+ var onload = evt.detail.xhr.onload;
+ var onerror = evt.detail.xhr.onerror;
+ evt.detail.xhr = {
+ status: 200,
+ getAllResponseHeaders: function() {
+ return this.getResponseHeader("Content-Type:application/json");
+ },
+ getResponseHeader: function(headerName) {
+ if (headerName.toLowerCase() === "content-type") {
+ return "application/json";
+ }
+ return undefined;
+ }
+ };
- var configElem = htmx.closest(elt, '[hx-db-config]');
- var conf = configElem ? JSON.parse(configElem.getAttribute('hx-db-config')) : {};
- var config = { ...httpBackendConfig,
- ...conf
- };
- var dbURI = dbElem.getAttribute('hx-db');
-
- var backend;
- if (dbElem._htmx_sqlite_http_backend) {
- backend = dbElem._htmx_sqlite_http_backend;
- } else {
- backend = dbURI.match(/^https?:/) ? { http: sqliteWasmHttp.createHttpBackend(config) }
- : {};
- dbElem._htmx_sqlite_http_backend = backend;
- }
- dbElem.addEventListener('htmx:beforeCleanupElement', function(ev) {
- if (ev.detail.elt == dbElem && dbElem._htmx_sqlite_http_backend && dbElem._htmx_sqlite_http_backend.http) {
- dbElem._htmx_sqlite_http_backend.http.close();
- delete dbElem._htmx_sqlite_http_backend;
- }
- });
+ // use Htmx parameters as bind variables
+ var binds = {};
+ Object.entries(evt.detail.requestConfig.parameters).forEach(function([k,v]) {
+ binds['$' + k] = v;
+ });
- var allRows = [];
- sqliteWasmHttp.createSQLiteThread(backend)
- .then(function(db) {
- db('open', {
- filename: encodeURI(dbURI.match(/^https?:\/\//) ? dbURI : dbURI.replace(/^opfs:/, '').replace(/^https?:/, 'file:')),
- vfs: dbURI.replace(/:.*/,'').replace('https', 'http')
- });
- return db;
- })
- .then(function(db) {
- api.triggerEvent(elt, 'htmx:sql:loadstart', { elt: elt, xhr: { db: db } });
- db('exec', {
- sql: sql,
- bind: binds,
- rowMode: "object",
- callback: function(data) {
- if (data.row) {
- allRows.push(data.row);
- api.triggerEvent(elt, 'htmx:sql:progress', { elt: elt, xhr: { db: db, response: data.row } });
- } else {
- api.triggerEvent(elt, 'htmx:sql:loadend', { elt: elt, xhr: { db: db, response: allRows } });
+ var configElem = htmx.closest(elt, '[hx-request]');
+ var conf = configElem ? JSON.parse(configElem.getAttribute('hx-request')) : {};
+ var config = {
+ ...httpBackendConfig,
+ ...sqlConfig,
+ ...conf
+ };
+
+ var backend;
+ if (dbElem._htmx_sqlite_http_backend) {
+ backend = dbElem._htmx_sqlite_http_backend;
+ } else {
+ backend = dbURI.match(/^https?:/) ? { http: sqliteWasmHttp.createHttpBackend(config) }
+ : {};
+ dbElem._htmx_sqlite_http_backend = backend;
+ }
+ dbElem.addEventListener('htmx:beforeCleanupElement', function(ev) {
+ if (ev.detail.elt == dbElem && dbElem._htmx_sqlite_http_backend && dbElem._htmx_sqlite_http_backend.http) {
+ dbElem._htmx_sqlite_http_backend.http.close();
+ delete dbElem._htmx_sqlite_http_backend;
+ }
+ });
- db('close', {}) // This closes the DB connection
- .then(function() {
- db.close(); // This terminates the SQLite worker
- });
-
- var responseInfo = { elt: elt, xhr: { response: allRows }, target: api.getTarget(elt) };
- var cancel = !api.triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo);
- if (responseInfo.target && !cancel) {
- swapAndSettle(allRows, elt, responseInfo);
- }
- }
- }
- });
- });
+ var allRows = [];
+ sqliteWasmHttp.createSQLiteThread(backend)
+ .then(function(db) {
+ db('open', {
+ filename: encodeURI(dbURI.match(/^https?:\/\//) ? dbURI : dbURI.replace(/^opfs:/, '').replace(/^https?:/, 'file:')),
+ vfs: dbURI.replace(/:.*/,'').replace('https', 'http')
+ });
+ return db;
+ })
+ .then(function(db) {
+ api.triggerEvent(elt, 'htmx:xhr:loadstart', { elt: elt, xhr: {...evt.detail.xhr, db: db} });
+ db('exec', {
+ sql: sql,
+ bind: binds,
+ rowMode: config.rowMode,
+ callback: function(data) {
+ if (data.row) {
+ allRows.push(data.row);
+ api.triggerEvent(elt, 'htmx:xhr:progress', { elt: elt, xhr: { ...evt.detail.xhr, db: db, response: JSON.stringify(data.row), responseJSON: data.row } });
+ } else {
+ api.triggerEvent(elt, 'htmx:xhr:loadend', { elt: elt, xhr: { ...evt.detail.xhr, db: db, response: allRows.length == 0 ? '' : JSON.stringify(allRows), responseJSON: allRows } });
+
+ db('close', {}) // This closes the DB connection
+ .then(function() {
+ db.close(); // This terminates the SQLite worker
+ });
+
+ evt.detail.xhr.responseJSON = allRows;
+ evt.detail.xhr.response = allRows.length == 0 ? '' : JSON.stringify(allRows);
+ onload();
+ }
+ }
+ })
+ .catch(function(data) {
+ if (data.result) {
+ evt.detail.error = data.result.message;
+ }
+ onerror();
});
});
- }
+
+ // return false to stop Htmx processing. We are calling load() ourselves.
+ return false;
}
}
});
diff --git a/test/sqlite.js b/test/sqlite.js
index 10b3cf4..d369353 100644
--- a/test/sqlite.js
+++ b/test/sqlite.js
@@ -5,18 +5,18 @@ describe("sqlite extension", function() {
beforeEach(function (done) {
clearWorkArea();
- let div = make('
')
- div.addEventListener('htmx:sql:loadend', () => {
+ let div = make('
')
+ div.addEventListener('htmx:xhr:loadend', () => {
done();
});
});
describe('General stuff', function () {
it('can make multiple queries', function (done) {
- var div = make('
');
+ var div = make('
');
var handler = () => {
div.innerText.should.equal('[{"name":"foo"}]');
- var div2 = make('
');
+ var div2 = make('
');
var handler2 = () => {
div2.innerText.should.equal('[{"name":"foo"},{"name":"bar"}]');
done();
@@ -27,7 +27,7 @@ describe("sqlite extension", function() {
});
it('empty result clears content', function (done) {
- var div = make('
');
+ var div = make('
');
div.addEventListener('htmx:afterSwap', () => {
div.innerText.should.equal('');
done();
@@ -35,10 +35,10 @@ describe("sqlite extension", function() {
});
it('events', function (done) {
- var div = make('
');
- div.addEventListener('htmx:sql:loadstart', () => {
- div.addEventListener('htmx:sql:progress', () => {
- div.addEventListener('htmx:sql:loadend', () => {
+ var div = make('
');
+ div.addEventListener('htmx:xhr:loadstart', () => {
+ div.addEventListener('htmx:xhr:progress', () => {
+ div.addEventListener('htmx:xhr:loadend', () => {
done();
});
});
@@ -50,14 +50,14 @@ describe("sqlite extension", function() {
e.reason.should.equal('Timeout while waiting on backend');
done();
});
- make(`
`);
+ make(`
`);
});
});
describe('Bind parameters', function () {
it('hx-include', function (done) {
make(' ');
- var div = make('
');
+ var div = make('
');
var handler = () => {
div.innerText.should.equal('[{"name":"foo"}]');
done();
@@ -66,7 +66,7 @@ describe("sqlite extension", function() {
});
it('whole form', function (done) {
- var div = make('');
+ var div = make('');
var handler = () => {
div.innerText.should.equal('[{"name":"foo"}]');
done();
@@ -75,10 +75,10 @@ describe("sqlite extension", function() {
});
});
- describe('Use value of form field when hx-sql is empty', function () {
+ describe('Use value of form field when path is "this.value"', function () {
it('input', function (done) {
let target = make('
');
- make(' ');
+ make(' ');
var handler = () => {
target.innerText.should.equal('[{"name":"foo"}]');
done();
@@ -88,7 +88,7 @@ describe("sqlite extension", function() {
it('textarea', function (done) {
let target = make('
');
- make('');
+ make('');
var handler = () => {
target.innerText.should.equal('[{"name":"foo"}]');
done();
@@ -98,7 +98,7 @@ describe("sqlite extension", function() {
it('select', function (done) {
let target = make('
');
- make('INSERT INTO mytable VALUES(\'foo\'); SELECT * FROM mytable; INSERT INTO mytable VALUES(\'bar\'); SELECT * FROM mytable; ');
+ make('INSERT INTO mytable VALUES(\'foo\'); SELECT * FROM mytable; INSERT INTO mytable VALUES(\'bar\'); SELECT * FROM mytable; ');
var handler = () => {
target.innerText.should.equal('[{"name":"bar"}]');
done();