npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ekwoka/alpine-history

v0.2.1

Published

Sync Component an Store values to the URL Search Params!!!

Downloads

236

Readme

Alpine History: Param Persistance for AlpineJS

This exposes a simple magic $query to allow syncing and persisting values in an Alpine Component to the URL query string. This is useful for things like search forms, where you want to be able to share a link to the search results.

Install

npm i @ekwoka/alpine-history

Import to Build (Simple Version):

import Alpine from 'alpinejs';
import Params from '@ekwoka/alpine-history';

Alpine.plugin(Params);

window.Alpine = Alpine;
Alpine.start();

Usage:

When you want to sync a value to the URL query string, simply use the $query magic property when defining your Alpine component:

<div
  x-data="{
  search: $query(''),
}">
  <span x-text="search"></span>
  <input type="text" x-model="search" />
</div>

Now you will see as you type in the input, the URL will update to include the query string ?search=your+input+here. Refresh the page and your value will be restored! It's so easy!

You can even go back and forward in the navigation! It's like magic!

This adds Alpine.query to allow this to be used in contexts that don't have Magics available, like when using Alpine.data.

Options

The Query Interceptor exposes a few handy helpers to customize the behavior. These are available as methods to call after defining your initial value.

.as(name: string)

By default, the query key will be the path in your component from the root until where the query is used. as can be used to customize the name of the query key.

<div
  x-data="{
  search: $query('').as('q'),
}"></div>

This will now use q as the query key instead of search.

When the $query is nested in an object or array, the key in the query param will be visible as key[key][key] in the query string. This can be customized by using as on the parent object or array. For example:

{
  search: {
    query: $query('hello'),
  },
}

would be ?search[query]=hello by default.

.alwaysShow()

By default, if the current value of the query is the same as the defined initial value, the query will not be shown in the URL. This can be overridden by calling .alwaysShow().

{
  search: $query('hello').as('q').alwaysShow(), // ?q=hello
}

.usePush()

By default, the query will be updated using history.replaceState to avoid adding a new entry to the browser history. This can be overridden by calling .usePush(). This should be used when the query is used to handle major state changes for the user, but will likely be less useful for quickly updating minor steps.

{
  episodeId: $query('').as('eid').usePush();
}

Whenever episodeId the URL will be updated with ?eid=123 and a new entry will be added to the browser history.

This plugin also implements a popstate listener to note when the user navigates back or forward in the browser history. This will update the value of the query to match the URL. Two way binding!

.into(fn: Transformer<T>)

Naturally, query params are always strings. If you want to handle numbers or booleans, or other types, you can use the .into method to transform the value before it is used in your component.

{
  episodeId: $query('').as('eid').into(Number);
}

Now, episodeId will be a number instead of a string when loaded from the query string.

This only handles how the value is converted from the query string. It will not affect how the value is converted to a string when updating the query string. Due to this, object types that aren't simple objects or arrays will not work as expected. Speaking of...

Transformer<T>

For TypeScript people, here's the type signature for the transformer:

type Transformer<T> = (val: T | PrimitivesToStrings<T>) => T;

type PrimitivesToStrings<T> = T extends string | number | boolean | null
  ? `${T}`
  : T extends Array<infer U>
    ? Array<PrimitivesToStrings<U>>
    : T extends object
      ? {
          [K in keyof T]: PrimitivesToStrings<T[K]>;
        }
      : T;

Note, the transformer will need to be able to handle being called with the type of the value or a simply parsed structure that equates to all primitives being strings. This is because the transformer will be called with the value when initializing, which can be the provided value, or the one determined from the query string.

When writing your transformer as a literal or typed function, TypeScript should help guide you to a properly formatted transformer.

Additionally, if you have an initial value that contains non-string primitives, the value of the key on the data context will resolve to never which should indicate, if you attempt to use it, that you need to add a transformer.

{
  episodeId: $query(123), // never
  seasonNumber: $query(1).into(Number), // number
}

Arrays and Objects

This plugin supports arrays and objects as well! It will automatically treat objects and arrays as if they are made up of $query interceptors. For example:

{
  search: $query({
    query: 'hello',
    results: ['1', '2', '3'],
  }),
}

will be ?search[query]=hello&search[results][0]=1&search[results][1]=2&search[results][2]=3 by default.

If you have nested primitives like booleans and numbers, you can use .into to transform them, but your transformer will need to handle the nested values.

{
  search: $query({
    query: 'hello',
    results: [1, 2, 3],
  }).into((obj) => {
    obj.results = obj.results.map(Number);
  }),
}

You may choose to use separate $query interceptors to make this simpler.

observeHistory

You might want to use $query and have other tools that make changes to the query. By default, when $query intercepted values change, it is unaware of any other changes made to the URL and those change may be removed.

To handle this, you can import observeHistory and call it (with a History object, or it will default to globalThis.history), and the pushState and replaceState methods will be wrapped to update the reactive params when they are called.

import Alpine from 'alpinets/src';
import { query, observeHistory } from '../src/index.ts';
Alpine.plugin(query);
Alpine.data('test', () => ({
  count: Alpine.query(0).into(Number),
}));
observeHistory();
Alpine.start();

history.pushState({}, '', '?count=123');

This is not needed to handle popState events which are already handled by the plugin.

Reactivity

All normal reactive behaviors apply to the $query interceptor. You can hook up effects to them, and just have a grand old time.

What This Doesn't Do

This plugin does not do anything to manage params not associated with an $query interceptor. This means that if you have a query string like ?search=hello&sort=asc and you only have a $query interceptor for search, the sort param will be perpetuated during query string updates.

This does not directly expose anything for triggering events or handlers on query string changes. As the query interceptors are reactive, you can hook directly into the ones you care about and use Alpine Effects to trigger events or other behaviors.

Author

👤 Eric Kwoka

  • Website: http://thekwoka.net
  • Github: @ekwoka

Show your support

Give a ⭐️ if this project helped you!