crelte 0.2.0 → 0.2.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.
@@ -13,23 +13,25 @@ export type RouteOptions = {
13
13
  *
14
14
  * - `'init'`: is set on the first page load
15
15
  * - `'manual'`: is set when a route is triggered manually via `Router.open`
16
- * - `'live-preview-init'`: is set on the first page load in live preview mode
17
16
  * - `'click'`: is set when a route is triggered by a click event
18
17
  * - `'pop'`: is set when a route is triggered by a popstate event (back/forward)
18
+ * - `'replace'`: is set when a route is replaced via `Router.replaceState`
19
+ * - `'push'`: is set when a route is pushed via `Router.pushState`
20
+ *
21
+ * ## Note
22
+ * `replace` and `push` will not call loadData
19
23
  */
20
24
  export type RouteOrigin =
21
25
  | 'init'
22
- | 'live-preview-init'
23
26
  | 'manual'
24
27
  | 'click'
25
- | 'pop';
28
+ | 'pop'
29
+ | 'replace'
30
+ | 'push';
26
31
 
27
32
  /**
28
33
  * A Route contains information about the current page for example the url and
29
- * the site id
30
- *
31
- * ## Note
32
- * Never update the route directly, clone it before
34
+ * the site
33
35
  */
34
36
  export default class Route {
35
37
  /**
@@ -51,6 +53,13 @@ export default class Route {
51
53
 
52
54
  /**
53
55
  * The scroll position of the current route
56
+ *
57
+ * ## Note
58
+ * This does not have to represent the current scroll position
59
+ * should more be used internally.
60
+ *
61
+ * It might be useful for a new request to specify the wanted
62
+ * scroll position
54
63
  */
55
64
  scrollY: number | null;
56
65
 
@@ -154,6 +163,51 @@ export default class Route {
154
163
  return this.url.hash;
155
164
  }
156
165
 
166
+ /**
167
+ * Checks if there are previous routes which would allow it to go back
168
+ */
169
+ canGoBack(): boolean {
170
+ return !!this.index;
171
+ }
172
+
173
+ /**
174
+ * Gets the search param
175
+ *
176
+ * ## Example
177
+ * ```
178
+ * const route = new Route('https://example.com/foo/bar/?a=1&b=2', null);
179
+ * console.log(route.getSearchParam('a')); // '1'
180
+ * ```
181
+ */
182
+ getSearchParam(key: string): string | null {
183
+ return this.search.get(key);
184
+ }
185
+
186
+ /**
187
+ * Sets the search param or removes it if the value is null or undefined
188
+ *
189
+ * ## Example
190
+ * ```
191
+ * const route = new Route('https://example.com/foo/bar/?a=1&b=2', null);
192
+ * route.setSearchParam('a', '3');
193
+ * console.log(route.url.href); // 'https://example.com/foo/bar/?a=3&b=2'
194
+ *
195
+ * route.setSearchParam('a', null);
196
+ * console.log(route.url.href); // 'https://example.com/foo/bar/?b=2'
197
+ * ```
198
+ */
199
+ setSearchParam(key: string, value?: string | number | null) {
200
+ if (typeof value !== 'undefined' && value !== null) {
201
+ this.search.set(key, value as string);
202
+ } else {
203
+ this.search.delete(key);
204
+ }
205
+ }
206
+
207
+ inLivePreview(): boolean {
208
+ return !!this.search.get('x-craft-live-preview');
209
+ }
210
+
157
211
  /**
158
212
  * Returns if the site matches the url
159
213
  */
@@ -176,7 +230,32 @@ export default class Route {
176
230
  * This checks all properties of the url but search params do not have to be
177
231
  * in the same order
178
232
  */
179
- eq(route: Route) {
233
+ eq(route: Route | null) {
234
+ return (
235
+ route &&
236
+ this.eqUrl(route) &&
237
+ this.eqSearch(route) &&
238
+ this.eqHash(route)
239
+ );
240
+ }
241
+
242
+ /**
243
+ * Checks if the route is equal to another route
244
+ *
245
+ * This does not check the search params or hash
246
+ */
247
+ eqUrl(route: Route | null) {
248
+ return (
249
+ route &&
250
+ this.url.pathname === route.url.pathname &&
251
+ this.url.origin === route.url.origin
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Checks if the search params are equal to another route
257
+ */
258
+ eqSearch(route: Route | null) {
180
259
  const searchEq = (a: URLSearchParams, b: URLSearchParams) => {
181
260
  if (a.size !== b.size) return false;
182
261
 
@@ -191,54 +270,14 @@ export default class Route {
191
270
  .every(([[ak, av], [bk, bv]]) => ak === bk && av === bv);
192
271
  };
193
272
 
194
- return (
195
- route &&
196
- this.url.pathname === route.url.pathname &&
197
- this.url.origin === route.url.origin &&
198
- searchEq(this.search, route.search) &&
199
- this.hash === route.hash
200
- );
201
- }
202
-
203
- /**
204
- * Checks if there are previous routes which would allow it to go back
205
- */
206
- canGoBack(): boolean {
207
- return !!this.index;
208
- }
209
-
210
- /**
211
- * Gets the search param
212
- *
213
- * ## Example
214
- * ```
215
- * const route = new Route('https://example.com/foo/bar/?a=1&b=2', null);
216
- * console.log(route.getSearchParam('a')); // '1'
217
- * ```
218
- */
219
- getSearchParam(key: string): string | null {
220
- return this.search.get(key);
273
+ return route && searchEq(this.search, route.search);
221
274
  }
222
275
 
223
276
  /**
224
- * Sets the search param or removes it if the value is null or undefined
225
- *
226
- * ## Example
227
- * ```
228
- * const route = new Route('https://example.com/foo/bar/?a=1&b=2', null);
229
- * route.setSearchParam('a', '3');
230
- * console.log(route.url.href); // 'https://example.com/foo/bar/?a=3&b=2'
231
- *
232
- * route.setSearchParam('a', null);
233
- * console.log(route.url.href); // 'https://example.com/foo/bar/?b=2'
234
- * ```
277
+ * Checks if the hash is equal to another route
235
278
  */
236
- setSearchParam(key: string, value?: string | number | null) {
237
- if (typeof value !== 'undefined' && value !== null) {
238
- this.search.set(key, value as string);
239
- } else {
240
- this.search.delete(key);
241
- }
279
+ eqHash(route: Route | null) {
280
+ return route && this.hash === route.hash;
242
281
  }
243
282
 
244
283
  /**
@@ -35,6 +35,17 @@ type Internal = {
35
35
  ready: () => any,
36
36
  ) => void;
37
37
 
38
+ // onNothingLoaded get's called if the request did not load new Data
39
+ // since maybe a push or replace was called
40
+ onNothingLoaded: (
41
+ req: Request,
42
+ // call ready once your ready to update the dom
43
+ // this makes sure we trigger a route and site update
44
+ // almost at the same moment and probably the same tick
45
+ // to make sure we don't have any flickering
46
+ ready: () => void,
47
+ ) => void;
48
+
38
49
  onLoad: LoadFn;
39
50
 
40
51
  domReady: (req: Request) => void;
@@ -78,8 +89,6 @@ export default class Router {
78
89
  */
79
90
  private _loadingProgress: Writable<number>;
80
91
 
81
- private _onRouteEv: Listeners<[Route]>;
82
-
83
92
  private _onRequest: Listeners<[Request]>;
84
93
 
85
94
  /** @hidden */
@@ -105,12 +114,11 @@ export default class Router {
105
114
  this._loading = new Writable(false);
106
115
  this._loadingProgress = new Writable(0);
107
116
 
108
- this._onRouteEv = new Listeners();
109
-
110
117
  this._onRequest = new Listeners();
111
118
 
112
119
  this._internal = {
113
120
  onLoaded: () => {},
121
+ onNothingLoaded: () => {},
114
122
  onLoad: () => {},
115
123
  domReady: req => this.inner.domReady(req),
116
124
  initClient: () => this._initClient(),
@@ -167,10 +175,13 @@ export default class Router {
167
175
  /**
168
176
  * Open a new route
169
177
  *
170
- * @param target the target to open can be an url or a route
178
+ * @param target the target to open can be an url, a route or a request
171
179
  * the url needs to start with http or with a / which will be considered as
172
180
  * the site baseUrl
173
181
  *
182
+ * ## Note
183
+ * The origin will always be set to 'manual'
184
+ *
174
185
  * ## Example
175
186
  * ```
176
187
  * import { getRouter } from 'crelte';
@@ -182,16 +193,25 @@ export default class Router {
182
193
  * // the following page will be opened https://example.com/de/foo/bar
183
194
  * ```
184
195
  */
185
- open(target: string | URL | Route, opts: RequestOptions = {}) {
186
- this.inner.open(target, opts);
196
+ open(target: string | URL | Route | Request, opts: RequestOptions = {}) {
197
+ const req = this.inner.targetToRequest(target, {
198
+ ...opts,
199
+ origin: 'manual',
200
+ });
201
+ this.inner.open(req);
187
202
  }
188
203
 
189
204
  /**
190
- * This pushes the state of the route without triggering an event
205
+ * This pushes the new route without triggering a new pageload
191
206
  *
192
207
  * You can use this when using pagination for example change the route object
193
208
  * (search argument) and then call pushState
194
209
  *
210
+ * ## Note
211
+ * This will always set the origin to 'push'
212
+ * And will clear the scrollY value if you not provide a new one via the `opts`
213
+ * This will disableLoadData by default if you not provide an override via the `opts`
214
+ *
195
215
  * ## Example
196
216
  * ```
197
217
  * import { getRouter } from 'crelte';
@@ -204,18 +224,37 @@ export default class Router {
204
224
  * router.pushState(route);
205
225
  * ```
206
226
  */
207
- pushState(route: Route) {
227
+ push(route: Route | Request, opts: RequestOptions = {}) {
228
+ // cancel previous request
208
229
  this.pageLoader.discard();
209
- this.inner.pushState(route);
230
+ const req = this.inner.targetToRequest(route, {
231
+ ...opts,
232
+ origin: 'push',
233
+ scrollY: opts.scrollY ?? undefined,
234
+ disableLoadData: opts.disableLoadData ?? true,
235
+ });
236
+ this.inner.push(req);
210
237
  this.destroyRequest();
211
238
  this.setNewRoute(route);
212
239
  }
213
240
 
241
+ /**
242
+ * @deprecated use push instead
243
+ */
244
+ pushState(route: Route | Request) {
245
+ this.push(route);
246
+ }
247
+
214
248
  /**
215
249
  * This replaces the state of the route without triggering an event
216
250
  *
217
251
  * You can use this when using some filters for example a search filter
218
252
  *
253
+ * ## Note
254
+ * This will always set the origin to 'replace'
255
+ * And will clear the scrollY value if you not provide a new one via the `opts`
256
+ * This will disableLoadData by default if you not provide an override via the `opts`
257
+ *
219
258
  * ## Example
220
259
  * ```
221
260
  * import { getRouter } from 'crelte';
@@ -228,11 +267,24 @@ export default class Router {
228
267
  * router.replaceState(route);
229
268
  * ```
230
269
  */
231
- replaceState(route: Route) {
270
+ replace(route: Route | Request, opts: RequestOptions = {}) {
271
+ // cancel previous request
232
272
  this.pageLoader.discard();
233
- this.inner.replaceState(route);
273
+ const req = this.inner.targetToRequest(route, {
274
+ origin: 'replace',
275
+ scrollY: opts.scrollY ?? undefined,
276
+ disableLoadData: opts.disableLoadData ?? true,
277
+ });
278
+ this.inner.replace(req);
234
279
  this.destroyRequest();
235
- this.setNewRoute(route);
280
+ this.setNewRoute(req);
281
+ }
282
+
283
+ /**
284
+ * @deprecated use replace instead
285
+ */
286
+ replaceState(route: Route | Request) {
287
+ this.replace(route);
236
288
  }
237
289
 
238
290
  /**
@@ -259,13 +311,13 @@ export default class Router {
259
311
  /**
260
312
  * Add a listener for the onRoute event
261
313
  *
262
- * This differs from router.route.subscribe in the way that
263
- * it will only trigger if a new render / load will occur
314
+ * This will trigger every time a new route is set
315
+ * and is equivalent to router.route.subscribe(fn)
264
316
  *
265
317
  * @returns a function to remove the listener
266
318
  */
267
319
  onRoute(fn: (route: Route) => void): () => void {
268
- return this._onRouteEv.add(fn);
320
+ return this.route.subscribe(fn);
269
321
  }
270
322
 
271
323
  /**
@@ -297,6 +349,10 @@ export default class Router {
297
349
  ): Promise<ServerInited> {
298
350
  this.inner.initServer();
299
351
 
352
+ this._internal.onNothingLoaded = (_req, ready) => {
353
+ ready();
354
+ };
355
+
300
356
  const prom: Promise<ServerInited> = new Promise(resolve => {
301
357
  this._internal.onLoaded = (success, req, ready) => {
302
358
  const props = ready();
@@ -360,7 +416,11 @@ export default class Router {
360
416
  this._onRequest.trigger(req);
361
417
 
362
418
  // route prepared
363
- this.pageLoader.load(req, { changeHistory });
419
+ if (!req.disableLoadData) {
420
+ this.pageLoader.load(req, { changeHistory });
421
+ } else {
422
+ this._onNothingLoaded(req, { changeHistory });
423
+ }
364
424
  }
365
425
 
366
426
  private destroyRequest() {
@@ -389,15 +449,28 @@ export default class Router {
389
449
 
390
450
  const route = req.toRoute();
391
451
 
392
- const updateRoute = () => {
452
+ // call the client or server saying we are ready for a new render
453
+ this._internal.onLoaded(resp.success, req, () => {
393
454
  this.setNewRoute(route);
455
+ return resp.data;
456
+ });
457
+ }
394
458
 
395
- this._onRouteEv.trigger(route.clone());
396
- };
459
+ private async _onNothingLoaded(req: Request, more: LoadedMore) {
460
+ // check if the render was cancelled
461
+ if (await req._renderBarrier.ready()) return;
397
462
 
398
- this._internal.onLoaded(resp.success, req, () => {
399
- updateRoute();
400
- return resp.data;
463
+ // when the data is loaded let's update the route of the inner
464
+ // this is will only happen if no other route has been requested
465
+ // in the meantime
466
+ more.changeHistory();
467
+
468
+ const route = req.toRoute();
469
+
470
+ // call the client or server saying there was an update in the route
471
+ // but no new data was loaded so no render should happen
472
+ this._internal.onNothingLoaded(req, () => {
473
+ this.setNewRoute(route);
401
474
  });
402
475
  }
403
476
 
package/LICENSE.md DELETED
@@ -1,41 +0,0 @@
1
- Copyright © Dunkel
2
-
3
-
4
- Permission is hereby granted to any person obtaining a copy of this software
5
- (the “Software”) to use, copy, modify, merge, publish and/or distribute copies
6
- of the Software, and to permit persons to whom the Software is furnished to do
7
- so, subject to the following conditions:
8
-
9
- 1. **Don’t plagiarize.** The above copyright notice and this license shall be
10
- included in all copies or substantial portions of the Software.
11
-
12
- 2. **Don’t use the same license on more than one project.** Each licensed copy
13
- of the Software shall be actively installed in no more than one production
14
- environment at a time.
15
-
16
- 3. **Don’t mess with the licensing features.** Software features related to
17
- licensing shall not be altered or circumvented in any way, including (but
18
- not limited to) license validation, payment prompts, feature restrictions,
19
- and update eligibility.
20
-
21
- 4. **Pay up.** Payment shall be made immediately upon receipt of any notice,
22
- prompt, reminder, or other message indicating that a payment is owed.
23
-
24
- 5. **Follow the law.** All use of the Software shall not violate any applicable
25
- law or regulation, nor infringe the rights of any other person or entity.
26
-
27
- Failure to comply with the foregoing conditions will automatically and
28
- immediately result in termination of the permission granted hereby. This
29
- license does not include any right to receive updates to the Software or
30
- technical support. Licensees bear all risk related to the quality and
31
- performance of the Software and any modifications made or obtained to it,
32
- including liability for actual and consequential harm, such as loss or
33
- corruption of data, and any necessary service, repair, or correction.
34
-
35
- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
39
- LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN
40
- AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
41
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.