Collection of tools I regularly use to build apps. Maybe they're useful to somebody else. Maybe not. Most of these are thin wrappers around native API's, like the native <dialog>
element, fetch
API, and URLPattern
.
app-tools's Introduction
app-tools's People
app-tools's Issues
[api] Feature discussion - Pass fetch as param
What do you think about adding a parameter to accept the fetch
implementation that's called? Defaulting to the global fetch
My use case looks something like
import instrumentedFetch from '~/tracing';
...
const response = await instrumentedFetch(resource, init)
In my case, it might be possible for me to replace instrumentedFetch
with something made from this client but a) I could adopt this quicker if I didn't need to b) it seems like it could be useful for testing or other times you're using a fetch
-compatible function that's not necessarily the global fetch
Router Refresh Bug (important, breaks history)
Hi there,
An issue has been noted with the app-tools router, where refreshing (normal and F5) breaks the router navigation of the app. Upon a refresh of a page, after investigating two things happen:
- If on Edge, Chrome, Safari: No matter how far you are in the navigation of the app, if you refresh the page, a single back navigation always takes you back to the default browser page, disregarding previously visited pages of the app. It's like the history is gone. However, if you refresh a page. and then navigate to another page, suddenly the history is restored and back navigation works once more.
- If on Firefox: No matter how far you are in the navigation of the app, if you refresh the page, the history entry gets duplicated. It takes two back navigations in order to get back to the previous page in the history.
On all browsers, once a page is refreshed the entry is always duplicated, it's just that on Edge, Chrome, Safari back navigation upon a refresh of a page breaks the history which I'm not sure why that is, however like I said above if you then navigate to another page after refreshing one, it restores the history but navigating backwards you will see that the original refreshed page is duplicated and it takes two back navs in order to get to the entry before.
To repro:
All I did was create a sample PWA app (under 2 min), taking the steps of installing the PWA package and running the app here: https://docs.pwabuilder.com/#/starter/quick-start
After you start the app, repro the steps above in those browsers specifically, and you will note the problem.
Router throws error when clicking link to custom protocol
Custom protocols, like the ones Github uses when it tries to launch Github Desktop or VS Code, will cause the router to throw an error.
Example custom protocol:
x-github-client://openRepo/https://github.com/pwa-builder/pwabuilder-oculus
If I use App Tools Router, then have a link in my app:
<a href="x-github-client://openRepo/https://github.com/pwa-builder/pwabuilder-oculus">Open in Github Desktop</a>
Clicking on that link will cause the router to throw an error:
Uncaught (in promise) DOMException: Failed to execute 'pushState' on 'History': A history state object with URL 'http://openRepo/https://github.com/pwa-builder/pwabuilder-oculus' cannot be created in a document with origin 'http://localhost:5173' and URL 'http://localhost:5173/detail/abcxyz'.
at Router.navigate (http://localhost:5173/node_modules/.vite/deps/@thepassle_app-tools_router__js.js?v=833e0bfe:186:20)
Investigating the issue, I see that it correctly identifies that the route isn't one of my app routes. However, it then checks if it matches the fallback, and it does:
// In router/index.js
async navigate(url) {
if (typeof url === 'string') {
url = new URL(url, this.baseUrl);
}
// THIS IS THE LINE IN QUESTION:
// The _matchRoute(url) correctly returns falsey, but _matchRoute(this.fallback) returns the fallback route.
// A few lines later we call window.history.pushState(null, '', `${url.pathname}${url.search}`), and this fails with error.
this.route = this._matchRoute(url) || this._matchRoute(this.fallback);
log(`Navigating to ${url.pathname}${url.search}`, { context: this.context, route: this.route });
}
Would you accept a PR fixing this?
shouldNavigate seems to need a name that's not in the docs
Loving these tools, their a real productivity boost, so thanks @thepassle
I'm trying to put a guard on loading a page/element and according to the docs I should just be
{
path: `${baseURL}profile`,
title: 'Profile',
plugins: [
lazy(() => import('./pages/user-profile/user-profile.js')),
{
shouldNavigate: (context) => ({
condition: () => false,
redirect: '/',
}),
beforeNavigation: (context) => {},
afterNavigation: (context) => {},
},
],
render: () => html`<user-profile></user-profile>`,
},
but vscode is telling me:
Property 'name' is missing in type '{ shouldNavigate: (context: Context) => { condition: () => false; redirect: string; }; beforeNavigation: (context: Context) => void; afterNavigation: (context: Context) => void; }' but required in type 'Plugin'.
also, in the page that I'm exporting the route config to I have a this.user
which I am storing in a lit context
object, am I able to access that within the route config, or do I need to subscribe to that in the router.js that does the export?
Router bug: hitting browser back button 2 or more times fails to navigate
I've found a bug in the Router where if you navigate backwards two or more times, it'll never navigate to the previous page.
Minimal repro here (and source here).
Repro steps:
- Navigate to page A
- Navigate to page B
- Navigate to page C
- Click browser back button (result: successfully navigates to page B)
- Click browser back button (result: bug, it navigates to page B, which we're already on)
I don't yet know the source of the bug, but I'd be glad to submit a PR if you're willing to accept it.
[api] Return error Response
I want to a) not throw an error when !response.ok
b) have full access to the Response
(statusText
, body
, headers
) etc
e.g. given
const response = await api.post(`/endpoint`, { bad: 'data' }`
I'd like response
to always be a Response
regardless of what HTTP status code was returned. Is this possible?
(a) seems to be addressed by handleError
but I cannot figure out how to achieve (b). I've tried every combination of handleError
, afterFetch
, etc I can think of but I can never seem to get the body of an error response back (e.g. if /endpoint
returns a 4xx.
Looking at the code, the issue seems to be that .then(handleStatus)
is always called. And handleStatus
converts that Response
to an Error
, losing all the info from the Response
if (!response.ok) {
throw new Error(response.statusText);
}
Do I have that correct? Either way, can you suggest any way to achieve what I'm describing?
Edit: I now see that getting the Response
would be a bigger change than skipping handleStatus
. IIUC, skipping handleStatus
would allow access to the processed body though
[router] Recommended way of handling `#` hash changes navigation
Hello!
I really like how this router behave, having tried some other options before (lit router, nano store router…).
I tried to handle hash navigation, typically for a "table of contents with scroll to heading" feature.
Ideally, I'd like to prevent the router for handling this altogether, and preserve the native behaviour.
So after some fiddling :
- Put logic in
shouldNavigate
- Same with
e.preventDefault
inroute-changed
- Playing with URL patterns (like handling URL with leading
#
) - putting
@click
ande.preventDefault
in all links with#
, then re-doing the native behaviour ourself
None of this paths will allow me to have a "pass-through" for hash changes with scrolling, but maybe I've overlooked something 🤔
I can think of some ways to handle this, one of them is to ignore some links, with a local marker (data attributes) or in a centralised fashion; sometimes you don't want to cater for this in the DOM itself, especially if it's pre-rendered from an external source.
IDK what the best, maybe both, or none?
Other way: have a dedicated handling for hash changes in the library. But that adds more API surface, so that might be undesirable.
Thank you, and Happy New Year 2024!
Router: Open in new tab navigates to the correct url, then navigates to root
I'm seeing some weirdness with "open in new tab" navigations.
Repro steps:
- Open your app at the root URL ("/")
- Middle-click (or CTRL+click) on a relative link on the page (/foo)
- In the new tab that opens, the new page opens and navigates to it (/foo). But after a second or so, it navigates again to the root (/).
This doesn't occur if I quickly switch to the new tab. It only occurs if I wait a few seconds before switching to the new tab.
Looking at where that 2nd phantom navigation comes from, it's from the router itself; nothing in my code spawns the 2nd nav.
I'll see if I can create a minimal repro shortly.
The TS declaration files are not found automatically
I've just installed the router with npm i -S @thepassle/app-tools
. I copy/pasted the example from the router readme and am getting the following TS errors:
Could not find a declaration file for module '@thepassle/app-tools/router.js'. '/[project]/node_modules/@thepassle/app-tools/router.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/thepassle__app-tools` if it exists or add a new declaration (.d.ts) file containing `declare module '@thepassle/app-tools/router.js';`ts(7016)
Router question: redirects with path params
I want this to work, but it doesn't:
const router = new Router({
routes: [
{
path: '/foo/:name',
title: 'Foo',
}
{
path: '/legacy/foo/:name',
title: 'Foo',
plugins: [
redirect('/foo/:name'),
]
}
]
});
If a user navigates to /legacy/foo/bar, I want it to redirect to /foo/bar.
Unfortunately it navigates to the literal URL /foo/:name
What's the best way to make this work? My thought is to create a plugin that finds the path params and inserts the real values for them. But that seems tedious because I'd have to write code to be aware of different kinds of path params, like :name
, :name?
, :name+
, :name(foo|bar)
, etc.
Is there a better way?
[router] plugin hook before rendering
I would like to fully load an api request before doing the actual rendering/navigation.
what should happen
- showing a list of clients
- click on one item in the list
- fetching starts - a global loading indicator is show but no change to current page (e.g. no render)
- fetching finished - render + navigate
what happens
- fetching starts - page is updated with nothing or a generic loading message (e.g. whatever you decide in your render)
- fetching finished - navigation / url change happens
ideas
I tried the beforeNavigation
but that instantly renders and results in the what happens
output
/**
* @param {(context: import('@thepassle/app-tools/router.js').Context) => Promise<object>} promise
* @returns {import('@thepassle/app-tools/router.js').Plugin}
*/
export function resolveData(promise) {
return {
name: 'data',
beforeNavigation: async context => {
const data = await promise(context);
context.data = data;
// + probably some error handling
},
};
}
how about adding a beforeRender
hook?
then the above code will work fine with one line change
- beforeNavigation: async context => {
+ beforeRender: async context => {
code idea
this.route
could be split into this.renderRoute
and this.route
and renderRoute
would only be updated after all beforeRender
hook have been executed.
usage example
{
path: '/client/:id',
title: 'Client',
plugins: [
resolveData(context => api.get(`v1/client?client-id=${context.params.id}`)),
],
render: context => {
// context.data is always available - e.g. navigation would not happen if api call result in an error
return html`<page-client .data=${context.data}></page-client>`;
},
},
what do you think?
Lazy doesn't return promise
I believe I've found a bug in the lazy plugin:
app-tools/router/plugins/lazy.js
Lines 9 to 11 in 53784be
This function doesn't return the result of fn
. This is important, as the result can be a promise, which is awaited later in the router code:
Lines 175 to 178 in 53784be
The simple fix would be to return the result inside lazy:
beforeNavigation: {
return fn();
}
Would you accept a PR for this?
[router] Clicking on the same link twice causes the page to reload
This is happening because _onAnchorClick
method has if (this.url.href === url.href) return;
Is this intended? I think it is better to just prevent the event in this case:
_onAnchorClick = (e) => {
if (
e.defaultPrevented ||
e.button !== 0 ||
e.metaKey ||
e.ctrlKey ||
e.shiftKey
) {
return;
}
const a = e.composedPath().find((el) => el.tagName === 'A');
if (!a || !a.href) return;
const url = new URL(a.href);
// old
// if (this.url.href === url.href) return;
// new
if (this.url.href === url.href) {
e.preventDefault();
return;
}
if (url.host !== window.location.host) return;
if (a.hasAttribute('download') || a.href.includes('mailto:')) return;
const target = a.getAttribute('target');
if (target && target !== '' && target !== '_self') return;
e.preventDefault();
this.navigate(url);
}
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.