Skip to content

Instantly share code, notes, and snippets.

@hypeJunction
Last active May 1, 2021 08:27
Show Gist options
  • Save hypeJunction/59d052b28ef83f7786008ed9fb446e0c to your computer and use it in GitHub Desktop.
Save hypeJunction/59d052b28ef83f7786008ed9fb446e0c to your computer and use it in GitHub Desktop.
Solid Design System - Inversion of Control
import React, { useEffect, useState } from "react";
import { DateTimeFormat } from "./DateTimeProvider";
import { FormattedDateTime } from "./FormattedDateTime";
import { ServiceProvider } from "./ServiceProvider";
import { Services, services } from "./Services";
import { useTimer } from "./Timer";
export const now = () => new Date();
export function App() {
const { time } = useTimer();
return (
<ServiceProvider<Services> services={services}>
<FormattedDateTime format={DateTimeFormat.TIME} date={time} />
</ServiceProvider>
);
}
import { ServiceFactoryFn, ServiceFactoryMap } from "./ServiceProvider";
export enum DateTimeFormat {
TIME,
ISO,
RELATIVE,
}
export interface DateTimeFormatterConfig {
preferredTimeFormat: string;
}
export interface DateTimeFormatterProps {
format: DateTimeFormat;
date: Date;
}
export interface DateTimeService extends ServiceFactoryMap {
formatDateTime: ServiceFactoryFn<DateTimeFormatter>;
}
export type DateTimeFormatterFactory = (
config: DateTimeFormatterConfig
) => DateTimeFormatter;
export type DateTimeFormatter = (props: DateTimeFormatterProps) => string;
import moment from "moment";
import {
DateTimeFormat,
DateTimeFormatter,
DateTimeFormatterConfig,
DateTimeFormatterFactory,
DateTimeFormatterProps,
} from "./DateTimeProvider";
const momentFormats = {
[DateTimeFormat.TIME]: (
date: Date,
config: DateTimeFormatterConfig
): string => {
return moment(date).format(config.preferredTimeFormat);
},
[DateTimeFormat.ISO]: (date: Date): string => {
return date.toISOString();
},
[DateTimeFormat.RELATIVE]: (date: Date): string => {
return moment(date).fromNow();
},
};
const momentFormatter: DateTimeFormatterFactory = (
config: DateTimeFormatterConfig
): DateTimeFormatter => (props: DateTimeFormatterProps): string => {
const { format, date } = props;
return momentFormats[format](date, config);
};
export { momentFormatter as dateTimeService };
import { render } from "@testing-library/react";
import { ServiceProvider } from "./ServiceProvider";
import { SinonStub, stub } from "sinon";
import { FormattedDateTime } from "./FormattedDateTime";
import { DateTimeFormat, DateTimeFormatter } from "./DateTimeProvider";
import { Services, services as defaultServices } from "./Services";
import { expect } from "chai";
describe("FormattedDateTime", () => {
describe("given a date object and format type", () => {
it("should use date time service to format date", () => {
const date = new Date();
const formatter: DateTimeFormatter & SinonStub = stub();
const services = {
...defaultServices,
formatDateTime: () => formatter,
};
formatter
.withArgs({
format: DateTimeFormat.TIME,
date,
})
.returns("formatted time");
const { getByText } = render(
<ServiceProvider<Services> services={services}>
<FormattedDateTime format={DateTimeFormat.TIME} date={date} />
</ServiceProvider>
);
expect(formatter).to.have.been.calledWith({
format: DateTimeFormat.TIME,
date,
});
expect(getByText("formatted time")).to.be.visible;
});
});
});
import { DateTimeFormatterProps, DateTimeService } from "./DateTimeProvider";
import React from "react";
import { useServiceProvider } from "./ServiceProvider";
export function FormattedDateTime({ format, date }: DateTimeFormatterProps) {
const { formatDateTime } = useServiceProvider<DateTimeService>();
return <time>{formatDateTime({ format, date })}</time>;
}
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
import { createContext, ReactNode, useContext, useRef } from "react";
export type Container<S extends ServiceFactoryMap> = {
[P in keyof S]: ReturnType<S[P]>;
};
export type ServiceFactoryFn<S> = (container: Container<any>) => S;
export type ServiceFactoryMap = Record<string | symbol, ServiceFactoryFn<any>>;
const ContainerContext = createContext<
Partial<{ services: Container<ServiceFactoryMap> }>
>({});
interface ServiceProviderProps<S extends ServiceFactoryMap> {
services: S;
children: ReactNode;
}
export function ServiceProvider<S extends ServiceFactoryMap>({
services,
children,
}: ServiceProviderProps<S>) {
const setup = (): Container<S> => {
const singletons: Partial<Container<S>> = {};
return new Proxy(services, {
get(instance: S, property: keyof S, receiver: any) {
if (!singletons?.[property]) {
singletons[property] = instance[property](receiver);
}
return singletons[property];
},
}) as Container<S>;
};
const { current } = useRef(setup());
return (
<ContainerContext.Provider value={{ services: current }}>
{children}
</ContainerContext.Provider>
);
}
export function useServiceProvider<
S extends ServiceFactoryMap
>(): Container<S> {
return useContext(ContainerContext).services as Container<S>;
}
import {
Container,
ServiceFactoryFn,
ServiceFactoryMap,
} from "./ServiceProvider";
import { DateTimeFormatterConfig, DateTimeService } from "./DateTimeProvider";
import { dateTimeService } from "./DateTimeService";
const config = {
dateTimeFormatter: {
preferredTimeFormat: "HH:mm:ss",
} as DateTimeFormatterConfig,
};
export interface ConfigService extends ServiceFactoryMap {
config: ServiceFactoryFn<typeof config>;
}
export type Services = ConfigService & DateTimeService;
export const services: Services = {
config: () => config,
formatDateTime: (c: Container<Services>) =>
dateTimeService(c.config.dateTimeFormatter),
};
import { useEffect, useState } from "react";
import { now } from "./App";
export const useTimer = (): { time: Date } => {
const [time, setTime] = useState(now());
useEffect(() => {
const interval = setInterval(() => {
setTime(now());
}, 1000);
return () => clearInterval(interval);
});
return { time };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment