Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Allows passing encoding callbacks #20

Merged
merged 2 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"vitest.commandLine": "pnpm exec vitest",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
Expand Down
36 changes: 18 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@
},
"devDependencies": {
"@milahu/patch-package": "6.4.14",
"@trivago/prettier-plugin-sort-imports": "4.2.1",
"@types/alpinejs": "3.13.3",
"@types/node": "20.8.10",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"@vitest/ui": "0.34.6",
"alpinejs": "3.13.2",
"esbuild": "0.19.5",
"eslint": "8.52.0",
"happy-dom": "9.1.9",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/alpinejs": "3.13.6",
"@types/node": "20.11.1",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@vitest/ui": "1.2.0",
"alpinejs": "3.13.3",
"esbuild": "0.19.11",
"eslint": "8.56.0",
"happy-dom": "13.1.4",
"husky": "8.0.3",
"lint-staged": "15.0.2",
"lint-staged": "15.2.0",
"npm-run-all": "4.1.5",
"prettier": "3.0.3",
"prettier": "3.2.2",
"pretty-bytes": "6.1.1",
"typescript": "5.2.2",
"vite": "4.5.0",
"vite-plugin-dts": "3.6.3",
"vite-tsconfig-paths": "4.2.1",
"vitest": "0.34.6",
"typescript": "5.3.3",
"vite": "5.0.11",
"vite-plugin-dts": "3.7.0",
"vite-tsconfig-paths": "4.2.3",
"vitest": "1.2.0",
"vitest-dom": "0.1.1"
},
"lint-staged": {
Expand Down Expand Up @@ -73,7 +73,7 @@
}
},
"dependencies": {
"@vue/reactivity": "^3.3.8",
"@vue/reactivity": "^3.4.13",
"alpinets": "link:../alpinets/packages/alpinets"
}
}
12 changes: 6 additions & 6 deletions packages/params/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ 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;
? 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.
Expand Down
45 changes: 45 additions & 0 deletions packages/params/src/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export type Encoding<T> = {
to(value: T): PrimitivesToStrings<T>;
from(value: PrimitivesToStrings<T>): T;
};

export 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;

globalThis.btoa ??= (str: string) => Buffer.from(str).toString('base64');
globalThis.atob ??= (str: string) => Buffer.from(str, 'base64').toString();

export const base64: Encoding<string> = {
to: (value) => btoa(value),
from: (value) => atob(value),
};

export const base64URL: Encoding<string> = {
to: (value) =>
btoa(value).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''),
from: (value) => atob(value.replaceAll('-', '+').replaceAll('_', '/')),
};

if (import.meta.vitest) {
describe('Encoding', () => {
it('should encode and decode base64', () => {
expect(base64.to('hello world')).toBe('aGVsbG8gd29ybGQ=');
expect(base64.to('<<???>>')).toBe('PDw/Pz8+Pg==');
expect(base64.from('aGVsbG8gd29ybGQ=')).toBe('hello world');
expect(base64.from('PDw/Pz8+Pg==')).toBe('<<???>>');
});
it('should encode and decode base64URL', () => {
expect(base64URL.to('hello world')).toBe('aGVsbG8gd29ybGQ');
expect(base64URL.to('<<???>>')).toBe('PDw_Pz8-Pg');
expect(base64URL.from('aGVsbG8gd29ybGQ')).toBe('hello world');
expect(base64URL.from('PDw_Pz8-Pg')).toBe('<<???>>');
});
});
}
102 changes: 66 additions & 36 deletions packages/params/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,67 @@
import type { PluginCallback, InterceptorObject, Alpine } from 'alpinejs';
import { fromQueryString, toQueryString } from './querystring';
import { fromQueryString, toQueryString } from './querystring.js';
import {
retrieveDotNotatedValueFromData,
objectAtPath,
deleteDotNotatedValueFromData,
insertDotNotatedValueIntoData,
} from './pathresolve';
import { UpdateMethod, onURLChange, untrack } from './history';
} from './pathresolve.js';
import { UpdateMethod, onURLChange, untrack } from './history.js';
import { Encoding, PrimitivesToStrings } from './encoding.js';
export { base64, base64URL } from './encoding.js';
export type { Encoding, PrimitivesToStrings } from './encoding.js';

type InnerType<T, S> = T extends PrimitivesToStrings<T>
? T
: S extends Transformer<T>
? T
: T | PrimitivesToStrings<T>;
type _InnerType<T, S> =
T extends PrimitivesToStrings<T>
? T
: S extends Transformer<T>
? T
: T | PrimitivesToStrings<T>;

/**
* This is the InterceptorObject that is returned from the `query` function.
* When inside an Alpine Component or Store, these interceptors are initialized.
* This hooks up setter/getter methods to to replace the object itself
* and sync the query string params
*/
class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
implements InterceptorObject<InnerType<T, S>>
{
class QueryInterceptor<T> implements InterceptorObject<T> {
_x_interceptor = true as const;
private alias: string | undefined = undefined;
private transformer?: S;
private encoder: Encoding<T> = {
to: (v) => v as PrimitivesToStrings<T>,
from: (v) => v as T,
};
private method: UpdateMethod = UpdateMethod.replace;
private show: boolean = false;
public initialValue: InnerType<T, S>;
public initialValue: T;
constructor(
initialValue: T,
private Alpine: Pick<Alpine, 'effect'>,
private reactiveParams: Record<string, unknown>,
) {
this.initialValue = initialValue as InnerType<T, S>;
this.initialValue = initialValue;
}
/**
* Self Initializing interceptor called by Alpine during component initialization
* @param {object} data The Alpine Data Object (component or store)
* @param {string} path dot notated path from the data root to the interceptor
* @returns {T} The value of the interceptor after initialization
*/
initialize(data: Record<string, unknown>, path: string): InnerType<T, S> {
initialize(data: Record<string, unknown>, path: string): T {
const {
alias = path,
Alpine,
initialValue,
method,
reactiveParams,
show,
transformer,
encoder,
} = this;
const initial = (retrieveDotNotatedValueFromData(alias, reactiveParams) ??
initialValue) as InnerType<T, S>;
const existing = retrieveDotNotatedValueFromData(
alias,
reactiveParams,
) as PrimitivesToStrings<T> | null;
const initial = existing ? encoder.from(existing) : initialValue;

const keys = path.split('.');
const final = keys.pop()!;
Expand All @@ -63,19 +71,26 @@ class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
set: (value: T) => {
!show && value === initialValue
? deleteDotNotatedValueFromData(alias, reactiveParams)
: insertDotNotatedValueIntoData(alias, value, reactiveParams);
: insertDotNotatedValueIntoData(
alias,
encoder.to(value),
reactiveParams,
);
},
get: () => {
const value = (retrieveDotNotatedValueFromData(alias, reactiveParams) ??
initialValue) as T;
const existing = retrieveDotNotatedValueFromData(
alias,
reactiveParams,
) as PrimitivesToStrings<T> | null;
const value = existing ? encoder.from(existing) : initialValue;
return value;
},
enumerable: true,
});

Alpine.effect(paramEffect(alias, reactiveParams, method));

return (transformer?.(initial) ?? initial) as InnerType<T, S>;
return initial;
}
/**
* Changes the keyname for using in the query string
Expand All @@ -90,10 +105,9 @@ class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
* Transforms the value of the query param before it is set on the data
* @param {function} fn Transformer function
*/
into(fn: Transformer<T>): QueryInterceptor<T, Transformer<T>> {
const self = this as QueryInterceptor<T, Transformer<T>>;
self.transformer = fn;
return self;
into(fn: Transformer<T>): QueryInterceptor<T> {
this.encoder.from = fn;
return this;
}
/**
* Always show the initial value in the query string
Expand All @@ -109,6 +123,14 @@ class QueryInterceptor<T, S extends Transformer<T> | undefined = undefined>
this.method = UpdateMethod.push;
return this;
}
/**
* Registers encoding and decoding functions to transform the value
* before it is set on the query string
*/
encoding(encoder: Encoding<T>): QueryInterceptor<T> {
this.encoder = encoder;
return this;
}
}

export const query: PluginCallback = (Alpine) => {
Expand Down Expand Up @@ -195,6 +217,7 @@ const setParams = (params: Record<string, unknown>, method: UpdateMethod) => {
if (import.meta.vitest) {
describe('QueryInterceptor', async () => {
const Alpine = await import('alpinejs').then((m) => m.default);
const { base64URL } = await import('./encoding');
afterEach(() => {
vi.restoreAllMocks();
});
Expand Down Expand Up @@ -351,17 +374,24 @@ if (import.meta.vitest) {
);
expect(history.replaceState).not.toHaveBeenCalled();
});
it('can have a defined encoding', async () => {
vi.spyOn(history, UpdateMethod.replace);
const paramObject = Alpine.reactive({});
const data = { foo: '' };
new QueryInterceptor(data.foo, Alpine, paramObject)
.encoding(base64URL)
.initialize(data, 'foo');
data.foo = '<<???>>';
await Alpine.nextTick();
expect(data).toEqual({ foo: '<<???>>' });
expect(paramObject).toEqual({ foo: 'PDw_Pz8-Pg' });
expect(history.replaceState).toHaveBeenCalledWith(
{ query: { foo: 'PDw_Pz8-Pg' } },
'',
'?foo=PDw_Pz8-Pg',
);
});
});
}

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;

export { observeHistory } from './history';
2 changes: 1 addition & 1 deletion packages/params/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/xajax/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/xrias/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/xrouter/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export default defineConfig({
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
},
sourcemap: true,
},
test: {
globals: true,
Expand Down
Loading