Application
Prerequisites
You know the IGNIS Container API (bind({ key }), toValue, toClass, get({ key })) and have read the Quickstart.
Quick Reference
| Export | Kind | Purpose |
|---|---|---|
AbstractArdorApplication | class | Container subclass with the lifecycle and registration helpers |
BaseArdorApplication | class | Empty subclass of AbstractArdorApplication - the one to extend |
IArdorApplication | interface | Contract implemented by the application |
IApplicationInfo | interface | Shape returned by getAppInfo() |
CoreBindings.APPLICATION_INSTANCE | key | Bound to the application itself in preConfigure() |
CoreBindings.APPLICATION_INFO | key | Bound to the result of getAppInfo() in preConfigure() |
ArdorApplication | component | Reads the three default providers from the container and renders CoreAdmin |
IApplication | interface | Props of ArdorApplication |
The application is the container
AbstractArdorApplication extends the IGNIS Container. There is no separate container property. You call this.bind(...) and this.get(...) directly on the application, and you pass the application instance wherever a Container is expected.
BaseArdorApplication adds nothing on top of AbstractArdorApplication. Extend it and implement the two abstract methods.
abstract class AbstractArdorApplication extends Container implements IArdorApplication {
abstract bindContext(): ValueOrPromise<void>;
abstract getAppInfo(): ValueOrPromise<IApplicationInfo>;
preConfigure(): ValueOrPromise<void>;
postConfigure(): ValueOrPromise<void>;
injectable<T>(scope: string, value: TClass<T>, tags?: Array<string>): void;
service<T>(value: TClass<T>): void;
start(): Promise<void>;
}
abstract class BaseArdorApplication extends AbstractArdorApplication {}import {
BaseArdorApplication,
CoreBindings,
type IApplicationInfo,
type IRestDataProviderOptions,
} from '@venizia/ardor';
class ProductService {
list() {
return ['book', 'pen'];
}
}
export class ShopApplication extends BaseArdorApplication {
getAppInfo(): IApplicationInfo {
return { name: 'shop', version: '1.0.0', description: 'Shop admin' };
}
bindContext() {
const options: IRestDataProviderOptions = { url: 'https://api.example.com' };
this.bind({ key: CoreBindings.REST_DATA_PROVIDER_OPTIONS }).toValue(options);
this.service(ProductService);
}
}
export async function boot() {
const app = new ShopApplication();
await app.start();
return app;
}getAppInfo()
getAppInfo() is abstract. It returns an IApplicationInfo:
interface IApplicationInfo {
name: string;
version: string;
description: string;
author?: { name: string; email: string; url?: string };
[extra: string | symbol]: any;
}preConfigure() binds whatever getAppInfo() returns under CoreBindings.APPLICATION_INFO. The return value is bound as-is - it is not awaited. Return a plain object so consumers that resolve APPLICATION_INFO (for example the REST data provider, which receives applicationInfo in IGetRequestPropsParams) get an object and not a Promise.
start() lifecycle
start() runs two steps in order and awaits each one:
abstract class AbstractArdorApplication extends Container {
...
async start() {
await this.preConfigure();
await this.postConfigure();
}
}preConfigure()
The default implementation does three things, in this order:
- Binds
CoreBindings.APPLICATION_INSTANCEtothis. - Binds
CoreBindings.APPLICATION_INFOtothis.getAppInfo(). - Returns
this.bindContext().
Because start() awaits the return value, an async bindContext() finishes before postConfigure() runs. If you override preConfigure(), call super.preConfigure() or the two core keys are never bound and bindContext() is never called.
bindContext()
Abstract. This is where you bind provider options, the default providers, and your own services. There is no base implementation, so do not call super.bindContext().
postConfigure()
A no-op by default. Override it for work that needs the bindings from bindContext() to exist, such as resolving a service and calling an async method on it.
import { BaseArdorApplication, type IApplicationInfo } from '@venizia/ardor';
class ConfigService {
async load() {
return { featureFlags: ['beta'] };
}
}
export class ShopApplication extends BaseArdorApplication {
getAppInfo(): IApplicationInfo {
return { name: 'shop', version: '1.0.0', description: 'Shop admin' };
}
bindContext() {
this.service(ConfigService);
}
async postConfigure() {
const config = this.get<ConfigService>({ key: 'services.ConfigService' });
await config.load();
}
}injectable() and service()
injectable() registers a class under a computed key. It is one of the few positional signatures in ARDOR.
abstract class AbstractArdorApplication extends Container {
...
injectable<T>(scope: string, value: TClass<T>, tags?: Array<string>): void;
}What it does:
- Key is
`${scope}.${value.name}`- forinjectable('services', ProductService)the key isservices.ProductService. - The binding is
toClass(value). - Scope is set to
BindingScopes.SINGLETON. One instance per application, resolved lazily on firstget. - Tags, if given, are applied with
setTags(...tags).
service() is the shorthand for the services scope:
abstract class AbstractArdorApplication extends Container {
...
service<T>(value: TClass<T>): void {
this.injectable('services', value);
}
}import { BaseArdorApplication, type IApplicationInfo } from '@venizia/ardor';
class ProductRepository {}
class EmailService {}
class ProductService {}
export class ShopApplication extends BaseArdorApplication {
getAppInfo(): IApplicationInfo {
return { name: 'shop', version: '1.0.0', description: 'Shop admin' };
}
bindContext() {
this.injectable('repositories', ProductRepository); // key: repositories.ProductRepository
this.injectable('services', EmailService, ['notifications']); // key: services.EmailService
this.service(ProductService); // key: services.ProductService
}
}Resolve a registered class with get({ key }) on the application, or with useInjectable inside React (see Hooks).
ArdorApplication - handing the container to React
ArdorApplication is the root component. It takes the started application as container, resolves the three default providers from it, and renders react-admin's CoreAdmin.
interface IApplication extends Omit<CoreAdminProps, 'children'> {
container: Container;
enableDebug?: boolean;
reduxStore: Store;
suspense: ReactNode;
resources: Array<ResourceProps>;
customRoutes?: {
routes: Array<RouteProps>;
};
}What the component does:
- Inside a
useMemokeyed oncontainerand the remaining props, it callscontainer.get({ key })forCoreBindings.DEFAULT_REST_DATA_PROVIDER,CoreBindings.DEFAULT_AUTH_PROVIDERandCoreBindings.DEFAULT_I18N_PROVIDER. These three keys must be bound before the component renders - see Data provider, Auth provider and i18n. - Wraps the tree in
ApplicationContext.Providerwith{ container, registry: container, logger }. The logger isLogger.getInstance({ scope: 'ArdorApplication', enableDebug }). - Wraps that in
ReduxProviderwithreduxStore, thenReact.Suspensewithsuspenseas the fallback. - Renders
CoreAdminwithdataProvider,authProvider,i18nProviderand every otherCoreAdminPropsprop you passed. - Renders one
Resourceper entry inresources, and oneRouteper entry incustomRoutes.routesinsideCustomRoutes. Route keys areroute.id ?? route.path.
import { ArdorApplication } from '@venizia/ardor';
export const Root = () => (
<ArdorApplication
container={container}
reduxStore={store}
suspense={<Spinner />}
resources={[{ name: 'products', list: ProductList }]}
customRoutes={{ routes: [{ path: '/about', element: <div>About</div> }] }}
/>
);Everything under ArdorApplication can reach the container with useApplicationContext() or useInjectable() and the logger with useApplicationLogger().
Common pitfalls
- Rendering before
start()resolves.ArdorApplicationcallscontainer.getfor the three default provider keys during render. Awaitapp.start()first, then mount. - Calling
super.bindContext().bindContext()is abstract. There is nothing to call. - Overriding
preConfigure()withoutsuper.preConfigure(). The override then skips bindingAPPLICATION_INSTANCEandAPPLICATION_INFOand never runsbindContext(). - Async
getAppInfo().preConfigure()binds the return value without awaiting it. Return a plain object. - Expecting
this.container. The application is the container. Usethis.bind(...)andthis.get(...). Legacy ra-core-infra code that usedthis.container.bind({ key, value })becomesthis.bind({ key }).toValue(value)- see the migration guide. - Keys depend on the runtime class name.
injectable()builds the key fromvalue.name. If a build step renames classes,get({ key: 'services.ProductService' })no longer matches. Use the stable keys in Binding keys for the core bindings. - Two instances of a service. Registering the same class under two scopes gives two singletons, one per key.