tao.js Adapter for React
This is a description for part of the first ("original") API for integrating the TAO with React. For a description of the Current API which provides a more React-like declarative approach, take a look here.
To first integrate the TAO with the programming interface provided by React, we use the tao.js
Adapter from the @tao.js/react package.
Similar model to Official Context API
React 16.3.0 introduced the
Official Context API and with it came the Provider and Consumer binary component relationship
that we can use to manage context for our Components in our React Apps.
The relationship between the Adapter and Reactor is very similar to the Context
API components in that just like the Provider is providing values for the Consumer to consume
and use, the Adapter is providing React Components for the Reactor to render into the UI. You
could say that Adapter and Reactor are the yin & yang ☯ of @tao.js/react (couldn't resist).
One of the stark differences to notice however is that the Adapter itself is not a
Component, but it does provide it's attached Components to the Reactor.
Adapting React Components to be Handlers
The purpose of the Adapter is to convert our React Components into handlers for the TAO.
Because React Components are functions in JavaScript, we can take advantage of this to make this
adaptation easy.
The Adapter will work on any React Component, however we will generally design our
Components to take advantage of the props passed to them as adapted from the tao
and data args passed to handlers by the TAO when an Application Context is set.
Fluent Methods
The Adapter provides fluent method chaining for all of the public methods that we use
on the Adapter by returning a reference to the Adapter itself from these methods. For those
unfamiliar, fluent method chaining allows us to make a sequence of calls to the Adapter in a
single statement, like:
adapter
.setDefaultCtx({ t: 'Space', o: 'Portal' })
.addComponentHandler({ a: 'View' }, View)
.addComponentHandler({ a: 'List' }, List);
Importing Adapter
First, before using the Adapter it must be imported along with the TAO which is the argument
passed to the Adapter's constructor:
import TAO from '@tao.js/core';
import { Adapter } from '@tao.js/react';
OR
const TAO = require('@tao.js/core');
const { Adapter } = require('@tao.js/react');
Using a Component as a Handler
Using a Component as a handler for an Application Context is easy.
Instead of calling the regular add[Type]Handler passing a handler function like:
TAO.addInlineHandler({ t: 'Space', a: 'Enter', o: 'Portal' }, (tao, data) => {
// do something here
});
We create an Adapter and use the addComponentHandler method passing along our React Component
like:
import MyComponent from './MyComponent';
const adapter = new Adapter(TAO);
adapter.addComponentHandler({ t: 'Space', a: 'View', o: 'Portal' }, MyComponent);
When we do this, the Adapter adds an Inline Handler to the TAO
that will be called when the {Space,View,Portal} Application Context is set on the TAO.
When this occurs (the AppCon is set to what we're listening for), the Adapter will make the
Component its current component for rendering by the Reactor which is itself a
Component that will handle rendering our Component as its child.
tao and data handler args combined as props
When the adapted handler receives the call from the TAO, it keeps track of the tao and data
args passed to it so they can be included as props for our instantiated Component.
However, since props is a single arg passed to the constructor of our Component, they have
to be combined. Although technically it's the Reactor that is managing this, it's useful for
the explanation here in order to understand how other options to the addComponentHandler and
other methods on the Adapter will instantiate our Components to help us in designing and
creating our Components for use in this way.
The tao and data objects are merged to create the props on our Component so that we
at least end up with the following props passed to our Component:
t: string- thetfrom thetaotrigram arg represents the Terma: string- theafrom thetaotrigram arg represents the Actiono: string- theofrom thetaotrigram arg represents the Orient(ation)[term]: any(optional) - the[term]key is the value oftand the value is the value of the data in the AppCon that is related to the Term (fromdataarg)[action]: any(optional) - the[action]key is the value ofaand the value is the value of the data in the AppCon that is related to the Action (fromdataarg)[orient]: any(optional) - the[orient]key is the value ofoand the value is the value of the data in the AppCon that is related to the Orient(ation) (fromdataarg)
Clearing the Component on an AppCon
Sometimes we may want to explicitly clear the Component when an Application Context is set on
the TAO. The Adapter allows us to set a "Component handler" for an empty component by passing
an empty or omitting the second argument to addComponentHandler:
import MyComponent from './MyComponent';
const adapter = new Adapter(TAO);
adapter
.addComponentHandler({ t: 'Space', a: 'View', o: 'Portal' }, MyComponent)
.addComponentHandler({ t: 'Space', a: 'List', o: 'Portal' }, null)
.addComponentHandler({ t: 'Space', a: 'Edit', o: 'Portal' }); // equivalent to above
TAO.setCtx({ t: 'Space', a: 'View', o: 'Portal' }, Space); // <-- MyComponent set is current for Adapter - rendered in any Reactor using the Adapter
TAO.setCtx({ t: 'Space', a: 'Edit', o: 'Portal' }, Space); // <-- null set as current for Adapter - any Reactor using this Adapter will render null children
Removing a ComponentHandler
Just like you can remove a handler from the TAO, the Adapter allows you to remove a Component
handler by using removeComponentHandler. Because you will generally not add a Component
handler with an anonymous inline function, it's likely you will easily have a reference to the
Components constructor even if that reference is imported.
import MyComponent from './MyComponent';
const adapter = new Adapter(TAO);
adapter.addComponentHandler({ t: 'Space', a: 'View', o: 'Portal' }, MyComponent);
…
// somehwere else
adapter.removeComponentHandler({ t: 'Space', a: 'View', o: 'Portal' }, MyComponent);
Remove ALL ComponentHandler call
As a convenience, the Adapter allows you to remove all trigrams attached to a Component
handler by passing an empty first argument to the removeComponentHandler method:
import MyComponent from './MyComponent';
const adapter = new Adapter(TAO);
adapter
.addComponentHandler({ t: 'Space', a: 'View', o: 'Portal' }, MyComponent)
.addComponentHandler({ t: 'Space', a: 'View', o: 'Admin' }, MyComponent)
.addComponentHandler({ t: 'Space', a: 'List', o: 'Portal' }, MyComponent)
.addComponentHandler({ t: 'Space', a: 'List', o: 'Admin' }, MyComponent);
…
// somehwere else
adapter.removeComponentHandler({}, MyComponent); // <--- Adapter no longer has any handlers for MyComponent
// OR
adapter.removeComponentHandler(, MyComponent); // <--- Adapter no longer has any handlers for MyComponent
Special Note: as an individual Adapter is managing the handler functions added to the TAO,
this way of removing handlers is scoped only to the individual Adapter.
Succinct ComponentHandlers
The Adapter provides 2 mechanisms to reduce the verbosity when working with Component handlers.
Matrixed trigrams
Because we may want to use the same Component to handle several different Application Contexts,
the Adapter allows the trigram values to be Arrays to capture a matrix of trigrams to handle
by our Component. When using this mechanism, the Adapter will use the ternary Cartesian Product
of all possibilities and add handlers to the TAO for each.
import Form from './Form';
const adapter = new Adapter(TAO);
adapter.addComponentHandler({ term: 'Space', action: ['New', 'Edit'], orient: 'Portal' }, Form);
TAO.setCtx({ t: 'Space', a: 'New', o: 'Portal' }); // <-- shows Form with empty fields to create a new Space
TAO.setCtx({ t: 'Space', a: 'Edit', o: 'Portal' }, Space); // <-- shows Form to Edit the Space passed in 2nd arg
Default Context
Because we will usually use a single Adapter to manage a set of Components for our React App
in some area of similarity, the Adapter allows setting the default context used by an Adapter
to avoid repetitive setting of the same aspects when adding or removing Component handlers.
There are 2 ways to set the default context:
defaultCtxsetter propertysetDefaultCtxmethod which is fluent chainable by returning a reference to theAdapteryou've seen this used in the Intro to @tao.js React examples
Additionally, the Adapter has a getter property defaultCtx which returns a copy of the
value of the default context set by one of the methods above so that it cannot be modified outside
of the Adapter.
When using the default context, the Adapter will perform an Object.assign on the default
context and the specific trigram used in the add or remove call so that trigram aspects passed in
to the specific add or remove invocation will overwrite the default when determining the trigram
that is of interest. This applies to matrix trigrams as well, and the cartesian product will be
determined after the merge has taken place.
We can use the defualt context to clean up examples from above:
import View from './View';
import Form from './Form';
const adapter = new Adapter(TAO);
adapter
.setDefaultCtx({ t: 'Space', o: 'Portal' })
.addComponentHandler({ a: 'View' }, View)
.addComponentHandler({ a: 'List' }, null)
.addComponentHandler({ a: ['New', 'Edit'] , Form});
TAO.setCtx({ t: 'Space', a: 'View', o: 'Portal' }, Space); // <-- View set is current for Adapter - rendered in any Reactor using the Adapter
TAO.setCtx({ t: 'Space', a: 'New', o: 'Portal' }); // <-- shows Form with empty fields to create a new Space
TAO.setCtx({ t: 'Space', a: 'Edit', o: 'Portal' }, Space); // <-- shows Form to Edit the Space passed in 2nd arg
TAO.setCtx({ t: 'Space', a: 'List', o: 'Portal' }); // <-- null set as current for Adapter - any Reactor using this Adapter will render null children
Additional props
The Adapter allows us to pass additional props that will be added to our Components when
they are instantiated in the UI by the Reactor.
To do this, we pass an Object with the desired props values in the third argument to
addComponentHandler:
import Form from './Form';
const adapter = new Adapter(TAO);
adapter.addComponentHandler({ t: 'Space', a: ['New', 'Edit'], o: 'Portal' }, Form, {
successCtx: { t: 'Space', a: 'Enter', o: 'Portal' }
});
TAO.setCtx({ t: 'Space', a: 'New', o: 'Portal' }); // <-- Reactor will render Form in UI with merged tao & data props + successCtx prop set above
Addtional props specific to handler
Because we assign the additional props when adding the Component handler, the props assignment
we are making are particular to when that ComponentHandler is called.
We have 3 options to make them more general and not have to repeat them:
- Add
Componenthandlers with matrixed trigrams - Use default context
- Use the
Reactor's additionalpropswhich only count for children rendered by thatReactorand will not be assigned for allComponenthandlers of ourAdapterwhen more than oneReactoris used with theAdapter
Special Note: it should be rather easy to add a defaultProps feature to Adapter allowing
this to have another option. If that seems a need, we can do it.