@logimaxx/kviews.js 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,10 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **`showLoader`** option on `Item` and `Collection` — set to `false` to skip the loading overlay during `loadFromRemote()` (default: `true`).
13
+
10
14
  ### Changed
11
15
 
16
+ - **`Item.loadFromRemote()`** — on an already loaded item, views re-render only when fetched data differs from the current state; the `load` event still fires when the request completes.
17
+ - **`Item.loadFromRemoteDoc()`** — skips updating internal fields when parsed data equals the current item state.
12
18
  - npm package name is **`@logimaxx/kviews.js`** (renamed from `@logimaxx/kviews`). Install with `npm install @logimaxx/kviews.js`.
13
19
 
20
+ ### Fixed
21
+
22
+ - **`Item.perform_update()`** — failed PATCH requests now reject with the original storage/HTTP error instead of masking it with `ReferenceError: patchData is not defined`.
23
+
14
24
  ## [1.2.1]
15
25
 
16
26
  ### Changed
package/README.md CHANGED
@@ -222,7 +222,7 @@ With jQuery, `$.fn.kviews.baseUrl`, `basePath`, and `defaultHeaders` mirror the
222
222
 
223
223
  ### Collection
224
224
 
225
- - `loadFromRemote()` - Load data from API
225
+ - `loadFromRemote()` - Load data from API (optional loading overlay via `showLoader`, default `true`)
226
226
  - `append(itemData)` - Add new item
227
227
  - `render()` - Render collection
228
228
  - `on(event, callback)` - Event listeners
@@ -230,7 +230,7 @@ With jQuery, `$.fn.kviews.baseUrl`, `basePath`, and `defaultHeaders` mirror the
230
230
 
231
231
  ### Item
232
232
 
233
- - `loadFromRemote()` - Load item from API
233
+ - `loadFromRemote()` - Load item from API (re-renders only when data changed; optional overlay via `showLoader`)
234
234
  - `update(data)` - Update item
235
235
  - `delete()` - Delete item
236
236
  - `render()` - Render item
package/dist/index.js CHANGED
@@ -31,6 +31,36 @@ function parseOptions(options) {
31
31
  }
32
32
  throw new Error("Invalid options", options);
33
33
  }
34
+ function deepEqual(a, b) {
35
+ if (a === b) {
36
+ return true;
37
+ }
38
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
39
+ return false;
40
+ }
41
+ if (Array.isArray(a) || Array.isArray(b)) {
42
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
43
+ return false;
44
+ }
45
+ for (let i = 0; i < a.length; i++) {
46
+ if (!deepEqual(a[i], b[i])) {
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ }
52
+ const keysA = Object.keys(a);
53
+ const keysB = Object.keys(b);
54
+ if (keysA.length !== keysB.length) {
55
+ return false;
56
+ }
57
+ for (const key of keysA) {
58
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) {
59
+ return false;
60
+ }
61
+ }
62
+ return true;
63
+ }
34
64
  function deepmerge(target, source, optionsArgument) {
35
65
  function defaultArrayMerge(target2, source2, optionsArgument2) {
36
66
  let destination = target2.slice();
@@ -848,6 +878,29 @@ var JsonApiAdapter = class {
848
878
  url.parameters[`page[${type}][limit]`] = pageSize;
849
879
  }
850
880
  }
881
+ /**
882
+ * Read pagination params already present in a collection URL.
883
+ *
884
+ * @param {import('../URL.js').URL} url - Collection URL
885
+ * @param {{ type?: string }} [context]
886
+ * @returns {{ offset?: number, pageSize?: number }}
887
+ */
888
+ extractListQueryFromUrl(url, context = {}) {
889
+ const { type } = context;
890
+ const result = {};
891
+ if (!url || !url.parameters || !type) {
892
+ return result;
893
+ }
894
+ const limitKey = `page[${type}][limit]`;
895
+ const offsetKey = `page[${type}][offset]`;
896
+ if (url.parameters.hasOwnProperty(limitKey)) {
897
+ result.pageSize = url.parameters[limitKey];
898
+ }
899
+ if (url.parameters.hasOwnProperty(offsetKey)) {
900
+ result.offset = url.parameters[offsetKey];
901
+ }
902
+ return result;
903
+ }
851
904
  /**
852
905
  * Serialize plain item data for a create (POST) request.
853
906
  *
@@ -1162,6 +1215,32 @@ var PlainRestAdapter = class {
1162
1215
  }
1163
1216
  url.parameters[this.limitParam] = pageSize;
1164
1217
  }
1218
+ /**
1219
+ * @param {import('../URL.js').URL} url
1220
+ * @param {object} [context]
1221
+ * @returns {{ offset?: number, pageSize?: number }}
1222
+ */
1223
+ extractListQueryFromUrl(url) {
1224
+ const result = {};
1225
+ if (!url || !url.parameters) {
1226
+ return result;
1227
+ }
1228
+ if (url.parameters.hasOwnProperty(this.limitParam)) {
1229
+ result.pageSize = url.parameters[this.limitParam];
1230
+ } else if (url.parameters.hasOwnProperty("pageSize")) {
1231
+ result.pageSize = url.parameters.pageSize;
1232
+ }
1233
+ if (this.paginationStyle === "page" && url.parameters.hasOwnProperty(this.pageParam)) {
1234
+ const page = url.parameters[this.pageParam] * 1;
1235
+ const pageSize = result.pageSize != null ? result.pageSize * 1 : null;
1236
+ if (pageSize) {
1237
+ result.offset = (page - 1) * pageSize;
1238
+ }
1239
+ } else if (url.parameters.hasOwnProperty(this.offsetParam)) {
1240
+ result.offset = url.parameters[this.offsetParam];
1241
+ }
1242
+ return result;
1243
+ }
1165
1244
  /**
1166
1245
  * @param {object|Array} itemData
1167
1246
  * @param {object} [context]
@@ -1824,17 +1903,26 @@ var Item = class _Item {
1824
1903
  */
1825
1904
  loadFromDataSource() {
1826
1905
  let loaders = [];
1827
- const overlay = createOverlay(this);
1828
- this.views.forEach((itemView) => {
1829
- if (itemView.el) {
1830
- let $el = $(itemView.el);
1831
- let loader = overlay.clone();
1832
- loader.insertBefore(itemView.el).width($el.width()).height($el.height());
1833
- loaders.push(loader);
1834
- }
1835
- });
1906
+ const showLoader = this.showLoader !== false;
1907
+ if (showLoader) {
1908
+ const overlay = createOverlay(this);
1909
+ this.views.forEach((itemView) => {
1910
+ if (itemView.el) {
1911
+ let $el = $(itemView.el);
1912
+ let loader = overlay.clone();
1913
+ loader.insertBefore(itemView.el).width($el.width()).height($el.height());
1914
+ loaders.push(loader);
1915
+ }
1916
+ });
1917
+ }
1918
+ const removeLoaders = () => {
1919
+ loaders.forEach((loader) => {
1920
+ loader.remove();
1921
+ });
1922
+ };
1836
1923
  return new Promise((resolve, reject) => {
1837
1924
  if (!this.url) {
1925
+ removeLoaders();
1838
1926
  reject(new Error("No valid URL provided"));
1839
1927
  return;
1840
1928
  }
@@ -1842,13 +1930,16 @@ var Item = class _Item {
1842
1930
  let urlString = this.url.toString ? this.url.toString() : this.url;
1843
1931
  this.storage.read(this, urlString, {}).then((resp) => {
1844
1932
  let data = resp.data;
1845
- this.loadFromRemoteDoc(data).render();
1933
+ const parsedData = this._parseRemoteDoc(data);
1934
+ if (!this._remoteDataEquals(parsedData)) {
1935
+ this._applyParsedRemoteData(parsedData);
1936
+ this.render();
1937
+ }
1846
1938
  this._trigger("load", this);
1847
- loaders.forEach((loader) => {
1848
- loader.remove();
1849
- });
1939
+ removeLoaders();
1850
1940
  resolve(this);
1851
1941
  }).catch((error2) => {
1942
+ removeLoaders();
1852
1943
  dbg("fail to load item resource", this.url, error2);
1853
1944
  if (error2 instanceof Error && error2.jqXHR) {
1854
1945
  this.fail(error2.jqXHR, error2.textStatus || "error", error2.errorThrown || error2);
@@ -1925,13 +2016,12 @@ var Item = class _Item {
1925
2016
  return returnView ? view : this;
1926
2017
  }
1927
2018
  /**
1928
- * Load from a remote API document (format determined by adapter).
1929
- *
2019
+ * Parse a remote API document into canonical item data.
1930
2020
  * @param {object} data - Raw HTTP response body
1931
- * @returns {Item} This instance for chaining
2021
+ * @returns {object} Parsed resource data
2022
+ * @private
1932
2023
  */
1933
- loadFromRemoteDoc(data) {
1934
- dbg("Load from remote doc", data);
2024
+ _parseRemoteDoc(data) {
1935
2025
  if (this.collection && !this.collection.type) {
1936
2026
  const inferredType = this.adapter.inferItemType(data);
1937
2027
  if (inferredType) {
@@ -1939,11 +2029,97 @@ var Item = class _Item {
1939
2029
  }
1940
2030
  }
1941
2031
  this.adapter.validateItemRemoteDoc(data, { collection: this.collection });
1942
- const parsedData = this.adapter.parseItemResponse(data, { collection: this.collection });
2032
+ return this.adapter.parseItemResponse(data, { collection: this.collection });
2033
+ }
2034
+ /**
2035
+ * Apply parsed remote data to this item.
2036
+ * @param {object} parsedData
2037
+ * @private
2038
+ */
2039
+ _applyParsedRemoteData(parsedData) {
1943
2040
  Object.assign(this, parsedData);
1944
2041
  if (this.url) {
1945
2042
  this.url = createURL(this.url);
1946
2043
  }
2044
+ }
2045
+ /**
2046
+ * Compare current item state with parsed remote data.
2047
+ * @param {object} parsedData
2048
+ * @returns {boolean}
2049
+ * @private
2050
+ */
2051
+ _remoteDataEquals(parsedData) {
2052
+ if (this.id == null && (!this.attributes || Object.keys(this.attributes).length === 0)) {
2053
+ return false;
2054
+ }
2055
+ if (String(this.id ?? "") !== String(parsedData.id ?? "")) {
2056
+ return false;
2057
+ }
2058
+ if ((this.type ?? null) !== (parsedData.type ?? null)) {
2059
+ return false;
2060
+ }
2061
+ if (!deepEqual(this.attributes ?? {}, parsedData.attributes ?? {})) {
2062
+ return false;
2063
+ }
2064
+ return deepEqual(
2065
+ this._normalizeRelationshipsForCompare(this.relationships),
2066
+ this._normalizeRelationshipsForCompare(parsedData.relationships)
2067
+ );
2068
+ }
2069
+ /**
2070
+ * Normalize relationships to plain comparable objects.
2071
+ * @param {object} relationships
2072
+ * @returns {object}
2073
+ * @private
2074
+ */
2075
+ _normalizeRelationshipsForCompare(relationships) {
2076
+ if (!relationships || typeof relationships !== "object") {
2077
+ return {};
2078
+ }
2079
+ const normalized = {};
2080
+ Object.keys(relationships).sort().forEach((name) => {
2081
+ normalized[name] = this._normalizeRelationshipValueForCompare(relationships[name]);
2082
+ });
2083
+ return normalized;
2084
+ }
2085
+ /**
2086
+ * @param {*} rel
2087
+ * @returns {*}
2088
+ * @private
2089
+ */
2090
+ _normalizeRelationshipValueForCompare(rel) {
2091
+ if (rel == null) {
2092
+ return null;
2093
+ }
2094
+ if (Array.isArray(rel)) {
2095
+ return rel.map((item) => this._normalizeRelationshipValueForCompare(item));
2096
+ }
2097
+ if (typeof rel !== "object") {
2098
+ return rel;
2099
+ }
2100
+ const normalized = {
2101
+ id: rel.id ?? null,
2102
+ type: rel.type ?? null,
2103
+ attributes: rel.attributes ?? {}
2104
+ };
2105
+ if (rel.relationships) {
2106
+ normalized.relationships = this._normalizeRelationshipsForCompare(rel.relationships);
2107
+ }
2108
+ return normalized;
2109
+ }
2110
+ /**
2111
+ * Load from a remote API document (format determined by adapter).
2112
+ *
2113
+ * @param {object} data - Raw HTTP response body
2114
+ * @returns {Item} This instance for chaining
2115
+ */
2116
+ loadFromRemoteDoc(data) {
2117
+ dbg("Load from remote doc", data);
2118
+ const parsedData = this._parseRemoteDoc(data);
2119
+ if (this._remoteDataEquals(parsedData)) {
2120
+ return this;
2121
+ }
2122
+ this._applyParsedRemoteData(parsedData);
1947
2123
  return this;
1948
2124
  }
1949
2125
  /**
@@ -2263,7 +2439,7 @@ var Item = class _Item {
2263
2439
  }
2264
2440
  resolve(this);
2265
2441
  }).catch((error2) => {
2266
- dbg("Update NOK", this.updateUrl, patchData, error2);
2442
+ dbg("Update NOK", this.updateUrl, payload.body, error2);
2267
2443
  if (error2 instanceof Error && error2.jqXHR) {
2268
2444
  reject(error2);
2269
2445
  } else if (error2.jqXHR) {
@@ -2496,8 +2672,9 @@ var CollectionView = class {
2496
2672
  }
2497
2673
  /**
2498
2674
  * Render the collection view
2675
+ * @private
2499
2676
  */
2500
- render() {
2677
+ _render() {
2501
2678
  dbg("Render _collectionView", this.collection);
2502
2679
  if (this.collection && this.collection.navtype === "page") {
2503
2680
  this.reset();
@@ -2815,6 +2992,10 @@ var Collection = class {
2815
2992
  configurable: true
2816
2993
  });
2817
2994
  let options = Object.assign({}, opts);
2995
+ const explicitListQuery = {
2996
+ pageSize: options.hasOwnProperty("pageSize"),
2997
+ offset: options.hasOwnProperty("offset")
2998
+ };
2818
2999
  Object.assign(this, options);
2819
3000
  if (options.hasOwnProperty("paging") && $(options.paging).length) {
2820
3001
  this.paging = new Paging($(options.paging)[0], this);
@@ -2841,6 +3022,9 @@ var Collection = class {
2841
3022
  throw new Error("Invalid navigations type. Should be page or scroll");
2842
3023
  }
2843
3024
  this.adapter = resolveAdapter(opts.adapter);
3025
+ if (this.url) {
3026
+ this._syncListQueryFromUrl(this.url, explicitListQuery);
3027
+ }
2844
3028
  this.storage = opts.hasOwnProperty("storage") ? opts.storage : new Storage(
2845
3029
  (() => {
2846
3030
  const storageOpts = Object.assign({}, opts.ajaxOpts || {});
@@ -3021,10 +3205,31 @@ var Collection = class {
3021
3205
  this.deleteUrl = typeof this.deleteUrl == "string" ? createURL(this.deleteUrl) : this.deleteUrl ?? createURL(this.url);
3022
3206
  this.updateUrl = typeof this.updateUrl == "string" ? createURL(this.updateUrl) : this.updateUrl ?? createURL(this.url);
3023
3207
  this.insertUrl = typeof this.insertUrl == "string" ? createURL(this.insertUrl) : this.insertUrl ?? createURL(this.url);
3208
+ if (this.adapter) {
3209
+ this._syncListQueryFromUrl(this.url);
3210
+ }
3024
3211
  break;
3025
3212
  }
3026
3213
  return this;
3027
3214
  }
3215
+ /**
3216
+ * Apply pagination params from URL query string to collection state.
3217
+ * @private
3218
+ * @param {import('./URL.js').URL} url
3219
+ * @param {{ pageSize?: boolean, offset?: boolean }} [explicit] - Options explicitly set at init
3220
+ */
3221
+ _syncListQueryFromUrl(url, explicit = {}) {
3222
+ if (!url || !this.adapter || typeof this.adapter.extractListQueryFromUrl !== "function") {
3223
+ return;
3224
+ }
3225
+ const fromUrl = this.adapter.extractListQueryFromUrl(url, { type: this.type });
3226
+ if (fromUrl.pageSize != null && !explicit.pageSize) {
3227
+ this.setPageSize(fromUrl.pageSize);
3228
+ }
3229
+ if (fromUrl.offset != null && !explicit.offset) {
3230
+ this.offset = fromUrl.offset * 1;
3231
+ }
3232
+ }
3028
3233
  /**
3029
3234
  * Receive remote data
3030
3235
  *
@@ -3114,7 +3319,7 @@ var Collection = class {
3114
3319
  this.loadItem(item);
3115
3320
  });
3116
3321
  if (this.view) {
3117
- this.view.render();
3322
+ this.view._render();
3118
3323
  } else {
3119
3324
  dbg("collection does not have a view ", this);
3120
3325
  }
@@ -3146,7 +3351,7 @@ var Collection = class {
3146
3351
  clear() {
3147
3352
  this.items = [];
3148
3353
  if (this.view) {
3149
- this.view.render();
3354
+ this.view._render();
3150
3355
  }
3151
3356
  this._trigger("update", this);
3152
3357
  return this;
@@ -3156,7 +3361,7 @@ var Collection = class {
3156
3361
  */
3157
3362
  render() {
3158
3363
  if (this.view) {
3159
- this.view.render();
3364
+ this.view._render();
3160
3365
  }
3161
3366
  this._trigger("afterrender", this);
3162
3367
  return this;
@@ -3177,15 +3382,21 @@ var Collection = class {
3177
3382
  * @private
3178
3383
  */
3179
3384
  loadFromDataSource() {
3180
- const overlay = createOverlay(this);
3181
3385
  let loader = null;
3182
- if (this.view && this.view.el) {
3386
+ const showLoader = this.showLoader !== false;
3387
+ if (showLoader && this.view && this.view.el) {
3388
+ const overlay = createOverlay(this);
3183
3389
  loader = $(overlay).clone().insertBefore(this.view.el).width($(this.view.el).width()).height($(this.view.el).height());
3184
3390
  }
3391
+ const removeLoader = () => {
3392
+ if (loader) {
3393
+ $(loader).remove();
3394
+ }
3395
+ };
3185
3396
  this._trigger("beforeload", this);
3186
3397
  return new Promise((resolve, reject) => {
3187
3398
  if (!this.url) {
3188
- loader.remove();
3399
+ removeLoader();
3189
3400
  reject(new Error("No valid URL provided"));
3190
3401
  return;
3191
3402
  }
@@ -3201,9 +3412,7 @@ var Collection = class {
3201
3412
  }
3202
3413
  this.receiveRemoteData(res.data);
3203
3414
  this._trigger("load", this);
3204
- if (loader) {
3205
- $(loader).remove();
3206
- }
3415
+ removeLoader();
3207
3416
  if (this.paging) {
3208
3417
  this.paging.render();
3209
3418
  }
@@ -3211,15 +3420,15 @@ var Collection = class {
3211
3420
  }).catch((error2) => {
3212
3421
  if (error2 instanceof Error && error2.jqXHR) {
3213
3422
  this.fail(error2.jqXHR, error2.textStatus || "error", error2.errorThrown || error2);
3214
- loader.remove();
3423
+ removeLoader();
3215
3424
  reject(error2);
3216
3425
  } else if (error2 && error2.jqXHR) {
3217
3426
  this.fail(error2.jqXHR, error2.textStatus, error2.errorThrown);
3218
- loader.remove();
3427
+ removeLoader();
3219
3428
  reject(error2);
3220
3429
  } else {
3221
3430
  this.fail(null, "error", error2);
3222
- loader.remove();
3431
+ removeLoader();
3223
3432
  reject(error2);
3224
3433
  }
3225
3434
  });
@@ -4021,6 +4230,7 @@ export {
4021
4230
  createOverlay,
4022
4231
  createURL,
4023
4232
  dbg,
4233
+ deepEqual,
4024
4234
  deepmerge,
4025
4235
  index_default as default,
4026
4236
  error,