dojo dragon main logo

Dojo Resources

Dojo Resources is designed to provide a consistent pattern to make widgets "resource aware". Resources can be configured to work with any type of data sources. A resource is essentially a Dojo managed data store with built-in caching, pagination, filtering, and custom Apis. Coupled with the resource middleware, resources allow consistent, source-agnostic data access for widgets, without the widgets requiring knowledge about the fetching implementation or the raw data format.

Feature Description
Support for any type of data source Resources can be used backed by pre-loaded data or data from an external source.
Single data source Resources allow creation of a single source of data for a given template that can be shared between multiple widgets using the data middleware.
Support for async and sync data reads Resource templates can read data in any way they like - once data becomes available, the resource middleware reactively invalidates any affected widgets.
Data transforms Allows specifying the data format that a widget requires, and transparently transforms source data into the expected output format for the widget to consume
Consistent Resource Options Resource options objects are passed to apis designed to read data.
Sharable Resource Options Resource options can be shared between widgets via the resource middleware, allowing multiple widgets to react to resource changes, such as page changes

Basic Usage

In order to work with Dojo resources, widgets need to use the resource middleware created with the createResourceMiddleware factory from @dojo/framework/middleware/resources. There are two types of "resource-aware" widgets: widgets that expose a resource on their property API and widgets that need to use a resource internally. The same factory is used to create both types of middleware, but the main difference is for widgets that require resources to be passed via properties, a resource type is needed on creation.

interface ResourceData {
	id: string;
	name: string;
}

// Add `resource` to the widgets API
const resource = createResourceMiddleware<ResourceData>();

// For using resources internally only, no property is added to the
// widget property API
const resource = createResourceMiddleware();

Using the resource middleware enables working with resource templates in your widget. Resources templates are created using the createResourceTemplate factory from @dojo/framework/middleware/resources. If a custom template is not passed to the createResourceTemplate factory the default resources template will be used.

To create a default template requires a generic to define the type of the resource data and the idKey of the resource data, this means the property that resources will treat as the unique id of the data.

interface ResourceData {
	id: string;
	name: string;
}

const template = createResourceTemplate<ResourceData>('id');

The default template requires initialization with an id to identify the instance of the resource and array of data for the resource, this can be used when working with a data set that is already loaded. Template's that require initialization need to be called to create the "stamped" template that can be used with resources.

template({ id: 'id', data: [{ id: '1', name: 'First' }] });

For the basic usage scenario to passing a template does not require importing and using the resource middleware inside your widget. If the resource data interface and the required data interface for the widget match and no custom options are required then the template can be passed directly to the resource property. If the default template is required, then a template is not required as Dojo Resources will automatically create a default template using an id, data and idKey passed as an object to resources.

App.tsx

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

interface ResourceData {
	id: string;
	name: string;
}

const myTemplate = createResourceTemplate<{ id: string }>();
const factory = create();

const App = factory(function App({ id }) {
	return (
		<div>
			<ResourceAwareWidget resource={template({ id, data })} />
			<ResourceAwareWidget resource={{ id, data, idKey: 'id' }} />
		</div>
});

Customizing a template's data source

Dojo resources can be configured for a user-defined data source, such as a RESTful API. This is done by creating a resource template with a custom implementation of the read API, using the createResourceTemplate factory. The read function receives the request that contains details including the offset, page size, and a set of controls which includes a put that is used to "set" the read response.

userResourceTemplate.tsx

import { createResourceTemplate } from '@dojo/framework/core/middleware/resources';

interface RemoteResourceData {
	id: string;
	name: string;
}

export default createResourceTemplate<RemoteResourceData>({
	idKey: 'id',
	read: async (request: ResourceReadRequest, controls: ResourceControls) => {
		// The template is injected with read request, offset, size and query
		const { offset, size } = request;
		// The request details are used to determine the data to fetch
		const response = await fetch(`https://my.user.endpount.com?offset=${offset}&size=${size}`);
		const data = await response.json();
		// The template needs to set the response using the resource controls put function
		// along with the original request
		controls.put({ data: data.data, total: data.total }, request);
	}
});

Accessing data within a widget

A "resource aware" widget needs to use the resource middleware which provides an API to work with the resource template. The resource middleware needs to be created using the createResourceMiddleware factory from @dojo/framework/core/middleware/resources, passing an interface that defines the expected resource data structure for the widget.

Accessing data in the widget is performed using the get function from the return from by passing the template to the resource.template factory. The get function requires the standard options, offset, size and query. Dojo resources provides a standard mechanism, createOptions from the resource middleware, for creating and managing options, including ensuring that widgets are invalidated when "shared" options are updated. The createOptions factory accepts a function that is called when the options are updated allow the owner of the options to control the changes, with the current and next options are passed to the setter.

const { createOptions } = resource;

// by default, the setter will normally need to merge the next
// options over the current options
const options = createOptions((curr, next) => {
	return { ...curr, ...next };
});

// however if certain queries are always required for the resource
// such as an "id' that can be always set
const options = createOptions((curr, next) => {
	return { ...curr, ...next, query: { ...next.query, id: 'my-id' } };
});

By default the get function will not try to read from the template, only attempting to fullfil the request with existing data. To ensure that Dojo resources tries to read the data if the request cannot be fulfilled a "read" function needs to be passed to the get function.

const {
	createOptions,
	get,
	template: { read }
} = resource.template(myTemplate);

const options = createOptions((curr, next) => ({ ...curr, ...next }));
const data = get(options(), { read });

If the read function on the resource template is asynchronous the result could be undefined while the data is being fetched and the widget will re-render when the data is available.

ResourceAwareWidget.tsx

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

interface ResourceData {
	value: string;
}

const resource = createResourceMiddleware<ResourceData>('value');

const factory = create({ resource });

export const DataAwareWidget = factory(function DataAwareWidget({ id, properties, middleware: { resource } }) {
	const {
		resource: { template, options = resource.createOptions(id) }
	} = properties();
	const {
		get,
		template: { read }
	} = resource.template(template);

	const items = get(options(), { read });
	if (items) {
		return <ul>{items.map((item) => <li>{item.value}</li>)}</ul>;
	}
	return <div>Loading...</div>;
});