Hooks
import { DependencyList, useEffect, useState } from "react";
import { debounceTime, fromEvent, map, Observable, Subject } from "rxjs";
export interface IRect {
width: number;
height: number;
}
export interface IData<T> {
data: T | undefined;
revision: number;
loading: boolean;
loaded: boolean;
initialLoading: boolean;
setData: (data: T) => unknown,
updateData: () => unknown
}
export function useEffectAsync(func: () => unknown, deps?: DependencyList) {
useEffect(() => {
func();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
export interface IUseDataState<T> {
data: T | undefined;
trigger: number;
revision: number;
loading: boolean;
loaded: boolean;
initialLoading: boolean;
}
export function useData<T>(fetchFn: () => Promise<T>, deps: any = []): IData<T> {
const [state, setState] = useObjectState<IUseDataState<T>>({
revision: 0,
trigger: 0,
loaded: false,
loading: true,
initialLoading: true,
data: undefined
});
useEffectAsync(async () => {
if (!state.initialLoading) {
setState({ loading: true, loaded: false });
}
let newState: Partial<IUseDataState<T>> = {};
try {
const newData = await fetchFn();
newState = { data: newData, loaded: true, revision: state.revision + 1 };
} catch (e) {
console.error(e);
newState = { data: undefined, revision: state.revision + 1 };
} finally {
setState({ ...newState, loading: false, initialLoading: false });
}
}, [...deps, state.trigger]);
return {
data: state.data,
revision: state.revision,
loading: state.loading,
loaded: state.loaded,
initialLoading: state.initialLoading,
setData: (data: T) => setState({ data: data, revision: state.revision + 1 }),
updateData: () => setState({ trigger: state.trigger + 1 })
} as IData<T>;
}
export function useIsMobile(): boolean {
const bp = 768;
const [isMobile, setIsMobile] = useState(window.innerWidth < bp);
useEffect(() => {
const updateSize = (windowSize: number): void => {
setIsMobile(windowSize < bp);
};
const resizeEvent = fromEvent(window, "resize");
resizeEvent.pipe(
map(x => (x.target as Window).innerWidth),
debounceTime(500)
).subscribe(updateSize);
}, []);
return isMobile;
}
export function useObservable<T>(subject: Observable<T | undefined>, initialValue?: T): T | undefined {
const [data, setData] = useState<T | undefined>(initialValue);
useEffect(() => {
if (!subject) { return; }
const subscription = subject.subscribe((x: any) => {
setData(x);
});
return () => subscription.unsubscribe();
}, [subject]);
return data;
}
export function useElementSize(ref: React.RefObject<HTMLElement>) {
const [size, setSize] = useState<IRect>({ height: 0, width: 0 });
useEffect(() => {
const element = ref.current;
if (!element) return;
const onSizeChanged = () => {
if (!element) return;
const rect = element.getBoundingClientRect();
setSize(rect);
};
const observer = new (window as any).ResizeObserver(onSizeChanged);
observer.observe(element);
return () => observer.unobserve(element);
}, [ref]);
return size;
}
export function useObjectState<T>(initialValue: T): [T, (c: Partial<T>) => any] {
const [item, setItem] = useState<T>(initialValue);
const handleChange = (changes: Partial<T>) => setItem(existing => ({ ...existing, ...changes }));
return [item, handleChange];
}
export function useStateDebounce<T>(time: number, initialValue: T): [T, (v: T) => any] {
const [value, setValue] = useState<T>(initialValue)
const [values] = useState(() => new Subject<T>())
useEffect(() => {
const sub = values.pipe(debounceTime(time)).subscribe(setValue)
return () => sub.unsubscribe()
}, [time, values])
return [value, (v: T) => values.next(v)];
}
Components
import React from "react";
export function isNullOrEmpty(text: string | undefined) {
if (text === undefined || text === null || String(text).trim() === "")
return true;
return false;
}
export function TextWithLines(props: { text: string | undefined }) {
const { text } = props;
const lines = String(text ?? "").replace(/\r\n(?![.!?]|\s*[A-Z])/g, " ");
return lines.split(/\r\n/).map((line, index) => (
<React.Fragment key={index}>
{line.trim()}
<br />
</React.Fragment>
));
}
type AutoTextWithLinesProps = {
text: string | undefined
} & React.HTMLAttributes<HTMLDivElement>; // 👈 alle standaard div-props
export function AutoTextWithLines({ text, ...restOfProps }: AutoTextWithLinesProps) {
if (isNullOrEmpty(text)) return <></>;
return <div {...restOfProps}><TextWithLines text={text} /></div>;
}
// -----
import { Property } from "csstype";
import React, { ReactNode } from "react";
export function Align(props: { h?: Property.JustifyItems; v?: Property.AlignItems; children: ReactNode; }) {
let style: React.CSSProperties = {};
if (props.h || props.v)
style = { ...style, display: "grid" };
if (props.h) {
style = { ...style, width: "100%", justifyItems: props.h };
}
if (props.v) {
style = { ...style, height: "100%", alignItems: props.v };
}
return <div style={style}>
{props.children}
</div>;
}
/**
* Custom hook om een queryparameter in de hash (#) van de URL te lezen en te schrijven.
* Werkt met hash-routing zoals: /#/stock/by-location?location=1234
* werkt url bij, maar triggert geen refresh
*/
export function useHashSearchParam(paramName: string) {
const getCurrentValue = useCallback(() => {
const [, queryPart] = window.location.hash.split("?");
return new URLSearchParams(queryPart ?? "").get(paramName) ?? "";
}, [paramName]);
const [value, setValue] = useState<string>(getCurrentValue);
// Update lokale state als gebruiker via back/forward navigeert
useEffect(() => {
const handler = () => setValue(getCurrentValue());
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, [getCurrentValue]);
// Update hash zonder herlaad/navigatie
const setHashParam = useCallback(
(newValue?: string) => {
const hash = window.location.hash || "";
const [pathPart, queryPart] = hash.split("?");
const params = new URLSearchParams(queryPart ?? "");
if (newValue) params.set(paramName, newValue);
else params.delete(paramName);
const newHash =
params.toString().length > 0
? `${pathPart}?${params.toString()}`
: pathPart;
window.history.replaceState({}, "", `${window.location.pathname}${newHash}`);
setValue(newValue ?? "");
},
[paramName]
);
return [value, setHashParam] as const;
}
NPM packages
linq-to-typescript (https://www.npmjs.com/package/linq-to-typescript) react-icons (https://react-icons.github.io/react-icons/) antd (https://ant.design/) framer-motion dayjs d3 date-fns copy-to-clipboard rxjs shortid nanoid jwt-decode
Forms
import create from 'zustand';
const useFormStore = create((set) => ({
formData: {},
setField: (key: string, value: any) =>
set((state) => ({ formData: { ...state.formData, [key]: value } })),
}));
const FormField = ({ value, onChange }) => {
return <input value={value || ''} onChange={(e) => onChange(e.target.value)} />;
};
const App = () => {
const { formData, setField } = useFormStore();
return (
<div>
<h3>Flexibele Formulier Velden</h3>
<label>Naam:</label>
<FormField value={formData.naam} onChange={(val) => setField('naam', val)} />
<label>Email:</label>
<FormField value={formData.email} onChange={(val) => setField('email', val)} />
<button onClick={() => console.log('Submit:', formData)}>Verzenden</button>
</div>
);
};
export default App;
import create from 'zustand';
interface IArticleFields {
title: string;
description: string;
price: number;
}
interface FormState {
formData: Partial<IArticleFields>; // Partial zodat velden optioneel zijn
setField: <K extends keyof IArticleFields>(key: K, value: IArticleFields[K]) => void;
}
const useFormStore = create<FormState>((set) => ({
formData: {},
setField: (key, value) =>
set((state) => ({
formData: { ...state.formData, [key]: value },
})),
}));
const App = () => {
const { formData, setField } = useFormStore();
return (
<div>
<h3>Artikel Formulier</h3>
<label>Titel:</label>
<FormField value={formData.title} onChange={(val) => setField("title", val)} />
<label>Beschrijving:</label>
<FormField value={formData.description} onChange={(val) => setField("description", val)} />
<label>Prijs:</label>
<FormField
value={formData.price ? formData.price.toString() : ""}
onChange={(val) => setField("price", Number(val))}
/>
<button onClick={() => console.log("Submit:", formData)}>Verzenden</button>
</div>
);
};
import { ReactElement } from "react";
import { useFormField } from "./useFormField";
//const [value, setValue] = useState(() => formStore.getValues()[field]);
interface FormFieldProps {
field: keyof IArticleFields;
children: ReactElement<{ value: any; onChange: (val: any) => void }>;
}
const FormField = ({ field, children }: FormFieldProps) => {
const [value, setValue] = useFormField(field);
return React.cloneElement(children, {
value,
onChange: (e: any) => setValue(e.target ? e.target.value : e),
});
};
export default FormField;
import { BehaviorSubject } from "rxjs";
export class FormStore<T extends Record<string, any>> {
private subjects: { [K in keyof T]: BehaviorSubject<T[K]> };
constructor(initialValues: T) {
this.subjects = Object.keys(initialValues).reduce((acc, key) => {
acc[key as keyof T] = new BehaviorSubject(initialValues[key as keyof T]);
return acc;
}, {} as { [K in keyof T]: BehaviorSubject<T[K]> });
}
getField$ = <K extends keyof T>(key: K) => this.subjects[key].asObservable();
setField = <K extends keyof T>(key: K, value: T[K]) => this.subjects[key].next(value);
getValues = (): T => Object.keys(this.subjects).reduce((acc, key) => {
acc[key as keyof T] = this.subjects[key as keyof T].getValue();
return acc;
}, {} as T);
}
import { useEffect, useState } from "react";
import { BehaviorSubject } from "rxjs";
export const useBehaviorSubject = <T>(subject: BehaviorSubject<T>) => {
const [value, setValue] = useState<T>(subject.getValue());
useEffect(() => {
const subscription = subject.subscribe(setValue);
return () => subscription.unsubscribe();
}, [subject]);
return [value, (val: T) => subject.next(val)] as const;
};
import { ReactElement } from "react";
import { BehaviorSubject } from "rxjs";
import { useBehaviorSubject } from "./useBehaviorSubject";
interface FormFieldProps<T> {
field$: BehaviorSubject<T>;
children: ReactElement<{ value: T; onChange: (val: any) => void }>;
}
const FormField = <T,>({ field$, children }: FormFieldProps<T>) => {
const [value, setValue] = useBehaviorSubject(field$);
return React.cloneElement(children, {
value,
onChange: (e: any) => setValue(e.target ? e.target.value : e),
});
};
export default FormField;
const App = () => {
const formStore = new FormStore<IArticleFields>({
title: "",
description: "",
price: 0,
});
return (
<div>
<h3>Artikel Formulier</h3>
<label>Titel:</label>
<FormField field$={formStore.getField$("title")}>
<input />
</FormField>
<label>Beschrijving:</label>
<FormField field$={formStore.getField$("description")}>
<textarea />
</FormField>
<label>Prijs:</label>
<FormField field$={formStore.getField$("price")}>
<input type="number" />
</FormField>
<button onClick={() => console.log("Submit:", formStore.getValues())}>Verzend</button>
</div>
);
};
export default App;
More helper code
import { App } from "antd";
import { IColumnMetaData } from "domain/base/IListView";
import { EventNames } from "domain/events/EventNames";
import DOMPurify from "dompurify";
import { DependencyList, Dispatch, ReactNode, RefObject, SetStateAction, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useSearchParams } from "react-router-dom";
import { BehaviorSubject, debounceTime, fromEvent, map, Observable, Subject } from "rxjs";
import { appEmit, appSubscribe } from "../domain/DomainPorts";
import { ISubscribable } from "./Mutables/ISubscribable";
import { makeMut } from "./Mutables/Mutable";
import { IRect } from "./ParentSize";
import { getDate } from "./tshelper";
import { IEventSubscription } from "domain/events/IEventHandler";
export const When = (value: boolean, e: React.ReactNode) => value ? e : <></>;
export const OnlyWhen = (value: boolean, fn: () => React.JSX.Element) => value ? fn() : <></>;
export const OnlyWhenFalse = (value: boolean, fn: () => React.JSX.Element) => !value ? fn() : <></>;
export const WhenElse = (value: boolean, e: React.ReactNode, n: React.ReactNode) => value ? e : n;
export interface IDataSource<T> {
data: T[];
loading: boolean;
setData: Dispatch<SetStateAction<T[]>>;
updateData: () => any;
}
export interface IData<T> {
data: T;
loading: boolean;
revision: number;
loaded: boolean;
setData: Dispatch<SetStateAction<T>>;
updateData: () => any;
}
export interface ILazyLoadingData<T> {
data?: T;
loading: boolean;
loaded: boolean;
isChanged: boolean;
load: () => any;
}
export function RenderHtml(htmlString: string): ReactNode {
return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlString) }} />;
}
export function mapData<T>(columns: IColumnMetaData[], data: any[]) {
const obj = {} as Record<string, any>;
columns.forEach(({ name: fieldName }, i) => {
obj[fieldName] = data[i];
});
obj.key = data[0];
return obj as T;
}
export function useDatasource<T>(fetchFn: () => Promise<T[]>, deps: any[] = []): IDataSource<T> {
const [trigger, setTrigger] = useState<boolean>(false);
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
async function fetch() {
setLoaded(false);
setLoading(true);
try {
const newData = await fetchFn();
setData(newData);
setLoaded(true);
} finally {
setLoading(false);
}
}
fetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, trigger]);
return {
data,
loading,
loaded,
setData,
updateData: () => setTrigger(!trigger)
} as IDataSource<T>;
}
export function useEffectAsync(func: () => unknown, deps?: DependencyList) {
useEffect(() => {
func();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
// New useData version:
export interface IUseDataState<T> {
data: T | undefined;
trigger: number;
revision: number;
loading: boolean;
loaded: boolean;
initialLoading: boolean;
}
export function useData<T>(fetchFn: (currentData: IUseDataState<T> | undefined) => Promise<T>, deps: any = []): IData<T> {
const [state, setState] = useObjectState<IUseDataState<T>>({
revision: 0,
trigger: 0,
loaded: false,
loading: true,
initialLoading: true,
data: undefined
});
useEffectAsync(async () => {
if (!state.initialLoading) {
setState({ loading: true, loaded: false });
}
let newState: Partial<IUseDataState<T>> = {};
try {
const newData = await fetchFn(state);
newState = { data: newData, loaded: true, revision: state.revision + 1 };
} catch (e) {
console.error(e);
newState = { data: undefined, revision: state.revision + 1 };
} finally {
setState({ ...newState, loading: false, initialLoading: false });
}
}, [...deps, state.trigger]);
return {
data: state.data,
revision: state.revision,
loading: state.loading,
loaded: state.loaded,
initialLoading: state.initialLoading,
setData: (data: T) => setState({ data: data, revision: state.revision + 1 }),
updateData: () => setState({ trigger: state.trigger + 1 })
} as IData<T>;
}
export function useLazyLoadData<T>(fetchFn: () => Promise<T>, isDefaultChanged = false, deps: any = []): ILazyLoadingData<T> {
const [data, setData] = useState<T>();
const [loading, setLoading] = useState(false);
const [loaded, setLoaded] = useState(false);
const [isChanged, setIsChanged] = useState(isDefaultChanged);
async function loadData() {
setLoading(true);
setLoaded(false);
try {
const newData = await fetchFn();
setData(newData);
setLoaded(true);
setIsChanged(false);
} catch (e) {
console.error(e);
setData(undefined);
} finally {
setLoading(false);
}
}
useEffect(() => {
setIsChanged(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return {
data,
loading,
loaded,
isChanged,
load: () => { loadData(); }
};
}
export function useEvent(element: Node, eventName: string, listener: (e: any) => any, deps: any[] = []) {
useEffect(() => {
element.addEventListener(eventName, listener);
return () => element.removeEventListener(eventName, listener);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
export function useEventRef(refElement: React.RefObject<any>, eventName: string, listener: (e: any) => any, deps: any[] = []) {
useEffect(() => {
const element = refElement?.current;
if (element) {
element.addEventListener(eventName, listener);
return () => element.removeEventListener(eventName, listener);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
export const useIsMobile = (): boolean => {
const bp = 768;
const [isMobile, setIsMobile] = useState(window.innerWidth < bp);
useEffect(() => {
const updateSize = (windowSize: number): void => {
setIsMobile(windowSize < bp);
};
const resizeEvent = fromEvent(window, "resize");
resizeEvent.pipe(
map(x => (x.target as Window).innerWidth),
debounceTime(500)
).subscribe(updateSize);
}, []);
return isMobile;
};
export function useSubject<T>(subject: Subject<T | undefined>, initialValue?: T): [T | undefined, (val: T | undefined) => any] {
const [data, setData] = useState<T | undefined>(initialValue);
useEffect(() => {
if (!subject) { return; }
const subscription = subject.subscribe((x: any) => {
setData(x);
});
return () => subscription.unsubscribe();
}, [subject]);
function updateData(val: T | undefined) {
subject.next(val);
}
return [data, updateData];
}
type ChangeFn<T> = (next: T | undefined) => any;
type SubscribeFn<T> = (nextFn: ChangeFn<T>) => IEventSubscription;
export function useSubscription<T>(subscribeFn: SubscribeFn<T>, deps: any[] = []): T | undefined {
const [data, setData] = useState<T | undefined>(undefined);
useEffect(() => {
function onChange(next: T | undefined) {
setData(next);
}
const subscription = subscribeFn(onChange);
return () => subscription.unsubscribe();
}, deps);// eslint-disable-line react-hooks/exhaustive-deps
return data;
}
export function useAppSubscribe<T>(event: EventNames, deps: any[] = []): T | undefined {
return useSubscription<T>((fn) => appSubscribe<T>(event, fn), deps);
}
export function useBehaviorSubject<T>(subject: BehaviorSubject<T>): T {
return useObservable(subject, subject.value)!;
}
export function useObservable<T>(subject: Observable<T | undefined>, initialValue?: T): T | undefined {
const [data, setData] = useState<T | undefined>(initialValue);
useEffect(() => {
if (!subject) { return; }
const subscription = subject.subscribe((x: any) => {
setData(x);
});
return () => subscription.unsubscribe();
}, [subject]);
return data;
}
export function useSubscribable<T>(subscribableObj: ISubscribable<T>, onChangeFn: (next: T | undefined) => any, deps: any[] = []): void {
// eslint-disable-next-line react-hooks/exhaustive-deps
const subscribeFn = useMemo(() => onChangeFn, deps);
useEffect(() => {
const subscription = subscribableObj.subscribe(subscribeFn);
return () => subscription.unsubscribe();
}, [subscribableObj, subscribeFn]);
}
export function useElementSize(ref: React.RefObject<HTMLElement>) {
const [size, setSize] = useState<IRect>({ height: 0, width: 0 });
useEffect(() => {
const element = ref.current;
if (!element) return;
const onSizeChanged = () => {
if (!element) return;
const rect = element.getBoundingClientRect();
setSize(rect);
};
const observer = new (window as any).ResizeObserver(onSizeChanged);
observer.observe(element);
return () => observer.unobserve(element);
}, [ref]);
return size;
}
export function useObjectState<T>(initialValue: T): [T, (c: Partial<T>) => any] {
const [item, setItem] = useState<T>(initialValue);
const handleChange = (changes: Partial<T>) => setItem(existing => ({ ...existing, ...changes }));
return [item, handleChange];
}
export function useStateDebounce<T>(time: number, initialValue: T): [T, (v: T) => any] {
const [value, setValue] = useState<T>(initialValue)
const [values] = useState(() => new Subject<T>())
useEffect(() => {
const sub = values.pipe(debounceTime(time)).subscribe(setValue)
return () => sub.unsubscribe()
}, [time, values])
return [value, (v: T) => values.next(v)];
}
export function decodeStateFromString<T>(stateStr: string) {
return JSON.parse(atob(stateStr)) as T
}
export function encodeStateAsString<T>(state: T) {
return btoa(JSON.stringify(state));
}
// De hook die de toestand beheert en de URL-parameters bijwerkt
export function useObjectStateInSearchParams<T>(nameOfUrlParam = "state"): [T, Dispatch<SetStateAction<T>>] {
const [searchParams, setSearchParams] = useSearchParams();
const state = searchParams.get(nameOfUrlParam);
const initialProdPlanFilter = state ? decodeStateFromString<T>(state) : {} as T;
const [value, setValue] = useState<T>(initialProdPlanFilter);
// Effect om de URL bij te werken wanneer de toestand verandert
useEffect(() => {
const encodedState = encodeStateAsString(value);
searchParams.set(nameOfUrlParam, encodedState);
setSearchParams(searchParams, { replace: true });
}, [value, searchParams, setSearchParams, nameOfUrlParam]);
return [value, setValue];
}
export function useDebounce(callback: (...args: any[]) => any, delay: number) {
const timeoutRef = useRef<any>(null);
useEffect(() => {
// Cleanup the previous timeout on re-render
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const debouncedCallback = (...args: any[]) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
};
return debouncedCallback;
}
export function formatNumberWithEuro(amount: number | null | undefined) {
if (amount == null || isNaN(amount)) {
return '€ 0,00';
}
return new Intl.NumberFormat('nl-NL', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
}
export function formatDateWithTime(dateString: any): string {
const date = getDate(dateString);
if (!date) return '-';
if (isNaN(date.getTime())) return '-'; // invalid date
const datePart = date.toLocaleDateString('nl-NL', { timeZone: 'UTC' });
const hours = date.getUTCHours();
const minutes = date.getUTCMinutes();
if (hours === 0 && minutes === 0) {
return datePart; // No time if midnight
}
const timePart = date.toLocaleTimeString('nl-NL', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC'
});
return `${datePart} ${timePart}`;
}
// threshold, between 0 and 1, 1 is fully visible on screen.
export function useIsVisible(
ref: RefObject<HTMLElement>,
threshold: number = 0.01 // default: zodra iets in beeld is
) {
const [isVisible, setIsVisible] = useState(false);
const observer = useMemo(() => {
return new IntersectionObserver(
([entry]) => {
setIsVisible(entry.intersectionRatio >= threshold);
},
{ threshold }
);
}, [threshold]);
useEffect(() => {
if (!ref.current) return;
const current = ref.current;
observer.observe(current);
return () => {
observer.unobserve(current);
observer.disconnect();
};
}, [observer, ref]);
return isVisible;
}
// threshold, between 0 and 1, 1 is fully visible on screen.
export function useVisibleThresholdVertical(ref: RefObject<HTMLElement>) {
const thresholdObservableValue = useMemo(() => makeMut(0), []);
const observer = useMemo(() => {
return new IntersectionObserver(
([entry]) => {
const intersectionRatioY = entry.intersectionRect.height / entry.boundingClientRect.height;
//entry.intersectionRatio
thresholdObservableValue.setValue(_ => intersectionRatioY);
},
{
root: document.body,
threshold: 1, // 0.00 → 1.00
}
);
}, [thresholdObservableValue]);
useEffect(() => {
if (!ref.current) return;
const current = ref.current;
observer.observe(current);
return () => {
observer.unobserve(current);
observer.disconnect();
};
}, [observer, ref]);
return thresholdObservableValue;
}
export function parseToDate(dateLike: unknown): Date | null {
if (!dateLike) return null;
if (dateLike instanceof Date) return dateLike;
const parsed = new Date(dateLike as string | number);
return isNaN(parsed.getTime()) ? null : parsed;
}
export function getDutchMonthName(date: Date | null): string {
if (!date) return "";
return new Intl.DateTimeFormat('nl-NL', { month: 'long' }).format(date);
}
export function getFirstDayOfPreviousMonth(date: Date = new Date()): Date {
return new Date(date.getFullYear(), date.getMonth() - 1, 1);
}
export function useLocalStorageWithEvents(key: string, initialValue: any, uniqueId: string) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const isSettingValue = useRef(false);
useEffect(() => {
const handleLocalStorageChange = (data: any) => {
// Controleer of het event van een andere bron komt
if (data.key === key && data.uniqueId === uniqueId && !isSettingValue.current) {
setStoredValue(data.newValue);
}
};
const subscription = appSubscribe("localStorageChange", handleLocalStorageChange);
return () => {
subscription.unsubscribe();
};
}, [key, uniqueId]);
const setValue = (value: any) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
isSettingValue.current = true;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
isSettingValue.current = false;
// Emit the event with uniqueId
appEmit(EventNames.localStorageChange, { key, newValue: valueToStore, uniqueId });
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
export interface IDestroyable { destroy: () => any }
export interface IModalInstance { showModel: (fn: (ref: IDestroyable) => any) => void }
export function useAppModel(): IModalInstance {
const app = App.useApp();
function showModel(fn: (ref: IDestroyable) => any) {
let model: IDestroyable | undefined = undefined;
const destroyable: IDestroyable = {
destroy: () => model?.destroy()
}
model = app.modal.success(fn(destroyable));
}
return {
showModel: showModel
};
}
/**
* Geeft de zichtbare verticale ratio van het element binnen de container.
* @returns Een waarde tussen 0 en 1 (0 = niet zichtbaar, 1 = volledig zichtbaar)
*/
export function visibleThresholdVertical(el: HTMLElement, containerRect: DOMRect): number {
const elRect = el.getBoundingClientRect();
const visibleTop = Math.max(elRect.top, containerRect.top);
const visibleBottom = Math.min(elRect.bottom, containerRect.bottom);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const elHeight = elRect.height;
if (elHeight === 0) return 0; // Voorkom delen door 0
const visibleRatio = visibleHeight / elHeight;
return Math.min(1, Math.max(0, visibleRatio)); // Clamp naar [0, 1]
}
export interface IVisibleChecker {
visibleThresholdVertical(e: HTMLElement): number;
}
export function useVisibleCheckerForRef(ref: RefObject<HTMLElement>) {
const visibleChecker = useMemo(() => makeMut<IVisibleChecker>({ visibleThresholdVertical: (_) => 0 }), []);
useEffect(() => {
if (!ref.current) return;
const e = ref.current;
function onScroll() {
const containerRect = e.getBoundingClientRect();
const isVisible = (el: HTMLElement) => visibleThresholdVertical(el, containerRect);
visibleChecker.setValue(_ => ({ visibleThresholdVertical: isVisible }));
}
e.addEventListener("scroll", onScroll, { passive: true });
const observer = new (window as any).ResizeObserver(onScroll);
observer.observe(e);
onScroll();
return () => {
e.removeEventListener("scroll", onScroll);
observer.unobserve(e);
};
}, [ref, visibleChecker]);
return visibleChecker;
}
export function useLatest<T>(value: T) {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
/**
* Hook to extract the 'id' parameter from the URL.
* Returns undefined if the id is 'new', otherwise returns the id as string.
* This allows for creating new items (POST) vs editing existing items (GET).
*/
export function useIdParam(): string | undefined {
const { id } = useParams<{ id: string }>();
return id === 'new' ? undefined : id;
}