Skip to content

Commit

Permalink
⚡ upgrade params (#15)
Browse files Browse the repository at this point in the history
* 🚧 Uses custom transformer

* ⚡ Uses defined properties

* 🐛 Fixes query string parser

* ⚰️ Removes unneeded cleaner

* 💚 Fixes CI checks
  • Loading branch information
ekwoka authored Nov 20, 2023
1 parent bb0820b commit 0b414f0
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 63 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/test-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,31 @@ jobs:

- name: Build Package
run: pnpm run -C ${{ matrix.package }} -r build

check-success:
runs-on: ubuntu-latest
needs: [Lint, Test, Build]
if: success()
outputs:
was_successful: ${{ steps.check-success.outputs.was_successful }}
steps:
- name: Check if all jobs were successful
id: check-success
run: |
echo "was_successful=true" >> $GITHUB_OUTPUT
was-successful:
runs-on: ubuntu-latest
needs: [check-success]
if: always()
steps:
- name: Was Successful
run: |
passed="${{ needs.check-success.outputs.was_successful }}"
if [[ $passed == "true" ]]; then
echo "All checks passed"
exit 0
else
echo "Check(s) failed"
exit 1
fi
125 changes: 72 additions & 53 deletions packages/params/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,74 @@
import type { PluginCallback, InterceptorObject } from 'alpinejs';

export const query: PluginCallback = (Alpine) => {
const reactiveParams: Record<string, string[]> = Alpine.reactive({});
for (const [key, value] of new URLSearchParams(window.location.search))
(reactiveParams[key] ??= []).push(value);
const reactiveParams: Record<string, unknown> = Alpine.reactive(
fromQueryString(location.search),
);

const intoState = () =>
Object.assign({}, history.state ?? {}, {
query: JSON.parse(JSON.stringify(Alpine.raw(reactiveParams))),
});

window.addEventListener('popstate', (event) => {
if (event.state?.query) Object.assign(reactiveParams, event.state.query);
});

Alpine.effect(() => {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(reactiveParams))
for (const v of value) v && params.append(key, v);
history.replaceState(null, '', `?${params.toString()}`);
if (JSON.stringify(reactiveParams) === JSON.stringify(history.state?.query))
return;
history.pushState(intoState(), '', `?${toQueryString(reactiveParams)}`);
});

const bindQuery = <T extends string | string[]>() => {
const bindQuery = <T>(): ((initial: T) => QueryInterceptor<T>) => {
let alias: string;
return Alpine.interceptor<T>(
(initialValue, getter, setter, path) => {
const isArray = Array.isArray(initialValue);
const obj: QueryInterceptor<T> = {
initialValue: undefined as T,
_x_interceptor: true,
initialize(data, path) {
const lookup = alias || path;
reactiveParams[lookup] ??= [];
const initial =
(isArray ? reactiveParams[lookup] : reactiveParams[lookup]?.[0]) ??
initialValue;

setter(initial as T);

Alpine.effect(() => {
const value = getter();
if (Array.isArray(value)) reactiveParams[lookup] = value;
else reactiveParams[lookup][0] = value;
});

Alpine.effect(() => {
const stored = (
isArray ? reactiveParams[lookup] : reactiveParams[lookup]?.[0]
) as T;
setter(stored);
retrieveDotNotatedValueFromData(lookup, reactiveParams) ??
this.initialValue;

const keys = path.split('.');
const final = keys[keys.length - 1];
data = objectAtPath(keys, data);
Object.defineProperty(data, final, {
set(value: T) {
insertDotNotatedValueIntoData(lookup, value, reactiveParams);
},
get() {
return retrieveDotNotatedValueFromData(lookup, reactiveParams) as T;
},
});

return initial as T;
},
(interceptor) =>
Object.assign(interceptor, {
as(name: string) {
alias = name;
return this;
},
}),
);
as(name: string) {
alias = name;
return this;
},
};

return (initial) => {
obj.initialValue = initial;
return obj;
};
};

Alpine.query = <T extends string | string[]>(val: T) =>
bindQuery<T>()(val) as QueryInterceptor<T>;
Alpine.query = <T>(val: T) => bindQuery<T>()(val) as QueryInterceptor<T>;
};

type QueryInterceptor<T extends string | string[]> = InterceptorObject<T> & {
type QueryInterceptor<T> = InterceptorObject<T> & {
as: (name: string) => QueryInterceptor<T>;
};

export default query;

declare module 'alpinejs' {
interface Alpine {
query: <T extends string | string[]>(val: T) => QueryInterceptor<T>;
query: <T>(val: T) => QueryInterceptor<T>;
}
}

Expand All @@ -87,7 +93,7 @@ const buildQueryStringEntries = (
) => {
Object.entries(data).forEach(([iKey, iValue]) => {
const key = baseKey ? `${baseKey}[${iKey}]` : iKey;

if (iValue === undefined) return;
if (!isObjectLike(iValue))
entries.push([
key,
Expand All @@ -102,16 +108,14 @@ const buildQueryStringEntries = (
};

const fromQueryString = (queryString: string) => {
queryString = queryString.replace('?', '');

if (queryString === '') return {};
const data: Record<string, unknown> = {};
if (queryString === '') return data;

const entries = new URLSearchParams(queryString).entries();

const data: Record<string, unknown> = {};
for (const [key, value] of entries) {
// Query string params don't always have values... (`?foo=`)
if (!value) return;
if (!value) continue;

const decoded = value;

Expand All @@ -126,26 +130,41 @@ const fromQueryString = (queryString: string) => {
return data;
};

const insertDotNotatedValueIntoData = (
key: string,
value: unknown,
data: Record<string, unknown>,
) => {
const keys = key.split('.');
const objectAtPath = (keys: string[], data: Record<string, unknown>) => {
const final = keys.pop()!;
while (keys.length) {
const key = keys.shift()!;

// This is where we fill in empty arrays/objects allong the way to the assigment...
if (data[key] === undefined)
data[key] = isNaN(Number(keys[0] ?? final)) ? {} : [];

data = data[key] as Record<string, unknown>;
// Keep deferring assignment until the full key is built up...
}
return data;
};

const insertDotNotatedValueIntoData = (
key: string,
value: unknown,
data: Record<string, unknown>,
) => {
const keys = key.split('.');
const final = keys[keys.length - 1];
data = objectAtPath(keys, data);
data[final] = value;
};

const retrieveDotNotatedValueFromData = (
key: string,
data: Record<string, unknown>,
) => {
const keys = key.split('.');
const final = keys[keys.length - 1];
data = objectAtPath(keys, data);
return data[final];
};

if (import.meta.vitest) {
describe('QueryString', () => {
it('builds query string from objects', () => {
Expand Down
14 changes: 8 additions & 6 deletions packages/params/testSite/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>X RIAS TEST SITE</title>
<script type="module">
<script type="module" lang="ts">
import Alpine from 'alpinets/src';
import query from '../src/index.ts';
Alpine.plugin(query);
Alpine.data('test', () => ({
tab: Alpine.query('tab'),
tab2: Alpine.query('tab2'),
tabs: [undefined, 'company', 'account', 'user'],
tab: Alpine.query({
tab: 'company, [one]',
other: 'this \\$!@#%^&*()["thing"]',
}),
tabs: [undefined, 'company three', 'account', 'user'],
index: 0,
}));
Alpine.start();
</script>
</head>
<body
x-data="test"
x-text="tab + ',' + tab2"
@click="tab2 = tab; tab = tabs[index=(index+1)%tabs.length]"></body>
x-text="JSON.stringify(tab,null,2)"
@click="tab.other = tab.tab; tab.tab = tabs[index=(index+1)%tabs.length]"></body>
</html>
8 changes: 4 additions & 4 deletions size.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"params": {
"minified": {
"pretty": "606 B",
"raw": 606
"pretty": "1.4 kB",
"raw": 1400
},
"brotli": {
"pretty": "329 B",
"raw": 329
"pretty": "718 B",
"raw": 718
}
},
"xajax": {
Expand Down

0 comments on commit 0b414f0

Please sign in to comment.