GithubHelp home page GithubHelp logo

app-tools's Introduction

@thepassle/app-tools

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.

Packages

app-tools's People

Contributors

artiga033 avatar dakmor avatar jfsiii avatar judahgabriel avatar thepassle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

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:

  1. 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.
  2. 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:

  1. Navigate to page A
  2. Navigate to page B
  3. Navigate to page C
  4. Click browser back button (result: successfully navigates to page B)
  5. 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 in route-changed
  • Playing with URL patterns (like handling URL with leading #)
  • putting @click and e.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:

  1. Open your app at the root URL ("/")
  2. Middle-click (or CTRL+click) on a relative link on the page (/foo)
  3. 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

  1. showing a list of clients
  2. click on one item in the list
  3. fetching starts - a global loading indicator is show but no change to current page (e.g. no render)
  4. fetching finished - render + navigate

what happens

  1. fetching starts - page is updated with nothing or a generic loading message (e.g. whatever you decide in your render)
  2. 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:

beforeNavigation: () => {
fn();
}

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:

app-tools/router/index.js

Lines 175 to 178 in 53784be

for (const plugin of plugins) {
try {
await plugin?.beforeNavigation?.(this.context);
} catch(e) {

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 photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.