dojo dragon main logo

Resource Middleware

The resource middleware is required to use resource templates and access resources. The middleware optionally automatically decorates the widget with the required resource property depending on whether an interface is passed to the createResourceMiddleware factory. The resource property is used by the widget to interact with any resource passed. The resource middleware passed to the widget is a factory that returns back the complete API for working with resources. The simplest scenario is using the resource middleware to return data for a requested page. This is done using the getOrRead API that requires the template and the resource options: getOrRead(template, options()). The getOrRead function is designed to be reactive, so if the data is not immediately available - i.e. the resource is asynchronous and not already been read for the provided options - it will return undefined for each page requested allowing the widget to deal with the "loading" data scenario.

The resource property consists of the template and an optional set of options which are used to interact with the template's resource store. As the options can be undefined, they needed to be defaulted with options created using the createOptions API. The createOptions function takes an identifer used to track the options across renders. This id can usually use the widget's id injected into the widget along with the properties, children and middleware.

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

interface ResourceData {
	value: string;
	label: string;
}

const resource = createResourceMiddleware<ResourceData>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, resource }) {
	const { getOrRead, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	};
	const [items] = getOrRead(template, options());

	if (!items) {
		return <div>Loading...</div>;
	}
	return <ul>{items.map((item) => <li>{item.label}</li>)}</ul>;
});

resource middleware API

createOptions()

createOptions creates a new instance of options that can be used with the resource API. An id is required in order to identify the option's instance across renders. The result of the createOptions function should be used when working with the getOrRead, isLoading, isFailed, and find APIs. It is important to use options rather than constructing a new ResourceOptions object in order to ensure that resources correctly invalidate when options have changed.

const options = createOptions('id');

The resulting options variable is a function that can be used to set and get the instances option data.

interface ResourceOptions<S> {
	page: number | number[];
	query: ResourceQuery<S>;
	size: number;
}

getOrRead()

The getOrRead function takes a template, the ResourceOptions, and optionally any initOptions that are required. getOrRead returns an array of data for each of the pages requested in the passed ResourceOptions. If the data is not already available, it will perform a read using the passed template. Once the data has been set in the resource, the widget will be invalidated in a reactive manner.

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { getOrRead, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();
	const [pageTenItems] = getOrRead(options({ page: 10, size: 30 }));

	if (!pageTenItems) {
		return <div>Loading...</div>;
	}
	return <ul>{pageTenItems.map((item) => <li>{item.label}</li>)}</ul>;
});

The query object can be passed to specify a filter for a property of the data. If a transform was passed with the template then this query object will be mapped back to the original resource's key when passed to the resource template's read function.

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { getOrRead, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();
	const [pageTenItems] = getOrRead(template, options({ page: 10, size: 30, query: { value: 'query-value' } }));

	if (!pageTenItems) {
		return <div>Loading...</div>;
	}
	return <ul>{pageTenItems.map((item) => <li>{item.label}</li>)}</ul>;
});

Multiple pages can be passed in the options. Each of the pages requested will be returned in the resulting array. When requesting multiple pages it's not safe to check the first array value to determine if the getOrRead call could be fulfilled as its API will return any pages that were available and make the requests for the rest. To check the status of the request, the options can be passed into the isLoading API. The pages are return in the same order that they are specified in the options. If desired, it may be useful to use .flat() on the pages array once it has been fully loaded to consolidate individual page results into a single list.

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { getOrRead, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();
	// [pageOne, pageTwo, pageThree, pageFour]
	const items = getOrRead(options({ page: [1, 2, 3, 4], size: 30 }));

	if (!isLoading(options())) {
		return <div>Loading...</div>;
	}
	return <ul>{items.flat().map((item) => <li>{item.label}</li>)}</ul>;
});

meta()

The meta API returns the current meta information for the resources, including the current options themselves. The MetaResponse also contains the registered total of the resource, which can be used to determine conditional logic such as virtual rendering.

meta(template, options, request): MetaResponse;
meta(template, options, initOptions, request): MetaResponse;

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { meta, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();

	// get the meta info for the current options
	const metaInfo = meta(template, options());

	if (metaInfo && metaInfo.total > 0) {
		// do something if there is a known total
	}
});

By default, calling meta will not initiate a request. If there is no meta info - i.e., getOrRead has not been called - it will never populate them and invalidate without a separate call to getOrRead. An additional parameter, request, can be passed as true in order to make a request for the passed options if there is no existing meta information.

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { meta, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();

	// get the meta info for the current options and make a `getOrRead`
	// request if there is no existing meta information. Once the request
	// is completed the widget will re-render with the meta information
	const metaInfo = meta(template, options(), true);

	if (metaInfo && metaInfo.total > 0) {
		// do something if there is a known total
	}
});

find()

The find function takes the template, ResourceFindOptions, and initOptions if required by the template. find returns the ResourceFindResult or undefined depending on if the item could be found. The ResourceFindResult contains the identified item, the index of the resource data-set, the page the item belongs to (based on the page size set in the options property in the ResourceFindOptions), and the index of the item on that page. If the find result is not already known to the resource store and the request is asynchronous then the find call will return undefined and invalidate the widget once the find result available.

The ResourceFindOptions requires a starting index, start, the ResourceOptions, options, the type of search, type (contains is the default find type), and a query object for the find operation:

interface ResourceFindOptions {
	options: ResourceOptions;
	start: number;
	type: 'exact' | 'contains' | 'start';
	query: ResourceQuery;
}

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { find, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();
	const item = find(template, { options: options(), start: 0, type: 'contains', query: { value: 'find query' } });

	if (item) {
		return <div>{/* do something with the item */}</div>;
	}
	return <div>Loading</div>;
});

isLoading()

The isLoading function takes a template, ResourceOptions or ResourceFindOptions object, and initOptions if required by the template. isLoading returns a boolean to indicate if there is an in-flight read underway for the passed options.

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { getOrRead, isLoading, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();
	const [items] = getOrRead(template, options({ page: 1, size: 30 }));

	if (!isLoading(template, options())) {
		return <div>Loading...</div>;
	}
	return <ul>{items().map((item) => <li>{item.label}</li>)}</ul>;
});

isFailed()

The isFailed function takes a template, ResourceOptions or ResourceFindOptions object and initOptions if required by the template. isFailed returns a boolean to indicate if there is a in-flight read underway for the passed options.

MyResourceAwareWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { createResourceMiddleware } from '@dojo/framework/core/middleware/resources';

const resource = createResourceMiddleware<{ value: string }>();

const factory = create({ resource });

export default factory(function MyDataAwareWidget({ id, properties, middleware: { resource } }) {
	const { getOrRead, isLoading, createOptions } = resource;
	const {
		resource: { template, options = createOptions(id) }
	} = properties();
	const [items] = getOrRead(template, options({ page: 1, size: 30 }));

	if (!isFailed(template, options())) {
		return <div>Failed...!</div>;
	}
	return <ul>{items().map((item) => <li>{item.label}</li>)}</ul>;
});