Context Module
Concept
By design all instances of the context module is synced by derived modules observer
its nearest ancestor current context and ancestors listen to change events
that bubbles up.
- a module might choose to
stopPropagation
which will not share it`s context with ancestors and sibling- a module might choose to
preventDefault
when an ancestor changes context- a module might not be able to validate and resolve provided context
Parent context changes (triggered initially)
When parent context changes, setCurrentContext
is called with option { resolve: true, validate: true }
Incompatible context
When a child module fails to resolve context parent, current context for the module is not set!
read how to configure - resolve context and validate context
Setting context
- validate [optional] - will validate context before setting current context
- resolve [optional] - will try to resolve provided context if validation fails
Caution
by default context bubbles
up to ancestors (which is a feature, to allow context between instances),
BUT if need to constrain context within runtime scope of current modules instance use:
modules.event.addEventListener('onCurrentContextChange', e => {
if(e.source === modules.context){
e.stopPropagation();
}
})
we might in future include config flag for propagation
Resolving context
When setting context and validation fails, the module will try to resolve a related context
onSetContextResolve
before resolving context when setting context, the onSetContextResolve
is fired
/** disable resolve of context, NOT RECOMMENDED */
module.addEventListener('onSetContextResolve', (e) => e.preventDefault());
Warning
if no context type is configure fo the module, all context will validate , see configure resolve context
Configuration
import { enableContext } from '@equinor/fusion-framework-module-context';
import { enableContext } from '@equinor/fusion-framework-react-module-context';
export const configure = (configurator) => enableContext(configurator);
Custom error
You can set your own error message by throwing FusionContextSearchError
insetContextClient
's query
.
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import {
ContextItem,
FusionContextSearchError,
enableContext,
} from '@equinor/fusion-framework-react-module-context';
export const configure: AppModuleInitiator = (configurator) => {
enableContext(configurator, async (builder) => {
builder.setContextClient({
get: async () => {
return undefined as unknown as ContextItem;
},
query: async () => {
throw new FusionContextSearchError({
title: 'This is a custom error',
description: 'This error is intentional',
});
},
});
});
};
Options
setContextType
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
/** optional filter for query types, array of string */
builder.setContextType(['project']);
});
}
array of context types which queries are filtered by.
A complete list of valid context types can be fetched from /contexts/types endpoint
setValidateContext
By default the provider will only check if the context item is within provided context types (support legacy usage)
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
builder.setValidateContext((item) => item.title.match(/a/));
});
}
setResolveContext
By default this method will use the query function for resolving related context and return the first valid context item.
This option allows the developer to fine tune how related context is resolved.
note this must return an observable, use
from
when async andof
when sync
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
builder.setValidateContext(function(){
this.relatedContexts({ item, filter: myCustomFilter }).pipe(
map((x) => x.filter((item) => this.validateContext(item))),
map((values) => {
const value = values.shift();
if (!value) {
throw Error('failed to resolve context');
}
if (values.length) {
console.warn(
'ContextProvider::relatedContext',
'multiple items found 🤣',
values
);
}
return value;
})
)
});
});
}
setContextFilter
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
/** optional filter of query result */
builder.setContextFilter((items) => items.filter(item => item.title.match(/a/)));
});
}
setContextParameterFn
Set method which generates the parameters for the query function. see Query Context.
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
builder.setContextParameterFn((args) => {
const { search, type } = args;
// Modify search and type ??
return {
search,
filter: {
type,
externalId: 'foobar36-8890-4b16-b973-9e13b9a72c26'
}
};
}
});
}
/** helper method for generating odata */
import buildQuery from 'odata-query';
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
builder.setContextParameterFn((args) => {
const { search, type } = args;
return buildQuery({
search,
filter: {
type: {
in: type,
},
},
});
}
});
}
QueryClient
currently setContextParameterFn
requires an return type of string | QueryContextParameters
,
but this method is creating the parameters to the query function.
If using a custom client with custom parameters, use this method to generate the custom parameters.
If there is a demand for generic query parameters we will in the future make the return type more generic.
setContextClient
export const configure = (configurator) => {
enableContext(configurator, (builder) => {
/** request another module that is enabled */
const httpProvider = await builder.requireInstance('http');
const client = httpProvider.createClient('my-api');
/**
* By default the Framework will resolve the context service
* @see {@link QueryCtorOptions} for advance configuration of query client
* @see [ObservableInput - RxJS](https://rxjs.dev/api/index/type-alias/ObservableInput)
* @return object for getting and querying context
*/
return builder.setContextClient({
get: (args) => client.json$(`/api/context/${args.id}`),
query: (args) =>
client.json$(`/api/context/search/`, {
method: 'post',
body: JSON.stringify(args),
}),
/** optional, note will clear context if invalid context provided **/
resolve: (item, filter) =>
client.json$(`/api/context/${item.id}/resolve/`, {
method: 'post',
body: JSON.stringify(filter),
}),
});
});
}
query post request processor, called after query is executed
FusionContextSearchError
When configuring the client, one could customize the error message when the client fails to resolve context
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import {
FusionContextSearchError,
enableContext,
} from '@equinor/fusion-framework-react-module-context';
export const configure: AppModuleInitiator = (configurator) => {
enableContext(configurator, async (builder) => {
builder.setContextFilter((items) => {
if (items.length === 0) {
throw new FusionContextSearchError({
title: 'This is a custom error',
description:
'Could not find any items in the context. This error is intentional',
});
}
return items;
});
// builder.setContextClient({
// get: async () => {
// return undefined as unknown as ContextItem;
// },
// query: async () => {
// throw new FusionContextSearchError({
// title: 'This is a custom error',
// description: 'This error is intentional',
// });
// },
// });
});
};
export default configure;
Events
onCurrentContextChange
dispatch before current context changes
onCurrentContextChanged
dispatch when current context changed
onParentContextChanged
dispatch when parent context changed
onSetContextResolve
dispatch before resolving context when setting current context
onSetContextResolved
dispatch when context resolved when setting current context
onSetContextResolveFailed
dispatch when failed to resolve context when setting current context
onSetContextValidationFailed
dispatch when failed to validate context when setting current context
will only trigger when not resolving context
React
Example
config.ts
import type { AppModuleInitiator } from '@equinor/fusion-framework-react-app';
import { enableContext } from '@equinor/fusion-framework-react-module-context';
import { enableNavigation } from '@equinor/fusion-framework-module-navigation';
import buildQuery from 'odata-query';
export const configure: AppModuleInitiator = (configurator, conf) => {
enableContext(configurator, async (builder) => {
builder.setContextType(['projectmaster']); // set contextType to match against
builder.setContextParameterFn(({ search, type }) => {
return buildQuery({
search,
filter: {
type: {
in: type,
},
},
});
});
});
// include this line to enable navigation on ctx changes
enableNavigation(configurator, conf.env.basename);
};
export default configure;
App.tsx
import { useModuleCurrentContext } from '@equinor/fusion-framework-react-module-context';
import { useRelatedContext } from './useRelatedContext';
export const App = () => {
const { currentContext } = useModuleCurrentContext();
// const { value: relatedContext } = useRelatedContext(['EquinorTask']);
const { value: relatedContext } = useRelatedContext();
return (
<>
<section>
<h3>Current Context:</h3>
<pre>{JSON.stringify(currentContext, null, 4)}</pre>
</section>
<section>
<h3>Related Context:</h3>
<pre>{JSON.stringify(relatedContext, null, 4)}</pre>
</section>
</>
);
};
export default App;