Skip to main content

Context Module

About 4 minModulecontext

GitHub package.json version (subfolder of monorepo)

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, setCurrentContextis 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 bubblesup 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

poj
import { enableContext } from '@equinor/fusion-framework-module-context';
export const configure = (configurator) => enableContext(configurator);

Custom error

You can set your own error message by throwing FusionContextSearchError in
setContextClient'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/typesopen in new window 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 and of 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.

Standard
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'
          }
      };
    }
  });
}

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

GitHub package.json version (subfolder of monorepo)

Example

cookbook -see exampleopen in new window

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;