@passionio/domain-plugin
v0.0.13
Published
This library is the core engine which loads a set of Passion frontend *domain plugins* according to the provided configuration, and enables the app to render Pages which consist of *views* and *widgets* belonging to those domains.
Downloads
452
Readme
Description
This library is the core engine which loads a set of Passion frontend domain plugins according to the provided configuration, and enables the app to render Pages which consist of views and widgets belonging to those domains.
+-------------------------App-------------------------------+
| |
| +-----------------DomainPluginHost----------------------+ |
| | | |
| | +--DomainPlugin1-+ +-DomainPlugin2-+ | |
| | | <Service> | | <Service> | | |
| | | ▲<views> | | <views> ◄-+ | | |
| | | |<widgets | | <widgets> | | | |
| | +-|-------▲------+ +▲----------|---+ | |
| +------|--------\----------/-----------|----------+ |
| | \ / | |
| | \ / | |
| | \ / | |
| +---|Page1+ +--Page2--+ +--Page3|--+ |
| | | | | Widgets | | | | |
| | View | | | | View | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| +---------+ +---------+ +----------+ |
| |
+-----------------------------------------------------------+
Table of Contents
Usage
Install:
yarn add @passionio/domain-plugin
Use:
import { Page, DomainPluginHost } from '@passionio/domain-plugin'
export const App = () => (
<DomainPluginHost domainPlugins={[{ domainPluginName: 'HelloWorld' }]} importDomainPlugin={name => domainPlugins[name]}>
<Page data={{
type: 'FullPageView',
domainPluginName: 'HelloWorld',
viewName: 'Main',
}} />
</DomainPluginHost>
)
Page
Pages are the main building blocks of a Passion.io app. It can be of one of two types:
- FullPageView - the page consists of a single View which belongs to a single DomainPlugin, and occupies the whole page. To create a Page of this type, one needs to specify the name of the domain plugin from which to pull the view, and the name of the view itself (see examples below).
- Widgetized - consists of one or more widgets laid out in a vertical column, each belonging to a potentially different domain plugin. For each widget the name of the containing domain plugin and the name of the widget need to be specified.
Example 1. A widgetized Page consisting of a widget AcademyCourse
defined in the Courses
plugin, and a Testimonials
widget belonging to a different plugin called Marketing
:
{
type: 'Widgetized',
widgets: [
{
id: 1,
domainPluginName: 'Courses',
widgetName: 'AcademyCourse',
},
{
id: 2,
domainPluginName: 'Marketing',
widgetName: 'Testimonials',
},
]
}
Example 2. A full page view which consists of a single view Main
pulled from the Courses
domain plugin:
{
type: 'FullPageView',
domainPluginName: 'Courses',
viewName: 'Main',
}
To render a Page in your app, you need to nest it as a child within the main DomainPluginHost component. This way the View and Widget components inside the page gain access to the loaded domain plugins and their interfaces:
import { Page, DomainPluginHost } from '@passionio/domain-plugin'
const App = () => (
<DomainPluginHost {...}>
<Page data={{
type: 'FullPageView',
domainPluginName: 'Courses',
viewName: 'Main',
}} />
</DomainPluginHost>
)
DomainPluginHost
This is the main component of this library, which loads the configured libraries and provides the whole internal mechanism to any Pages rendered within it, i.e. to the views and widgets within those pages.
When rendering a <DomainPluginHost>
in your app, you must provide two props:
domainPlugins
- an array of domain plugin descriptors in the form:{ domainPluginName: "Domain1" version: "^1.0.0" }
importDomainPlugin
- an async function which loads a domain plugin by name, e.g.const importDomainPlugin = domainPluginName => { const domainPluginModule = await import(`./${domainPluginName}.js`) return domainPluginModule.default }
Example:
const App = () => (
<DomainPluginHost
domainPlugins={[
{
domainPluginName: 'Courses',
version: '0.0.1'
},
{
domainPluginName: 'Community',
version: '0.0.2'
},
]}
importDomainPlugin={
domainPluginName => {
if (domainPluginName === 'Courses') {
return require('./Courses.js')
}
if (domainPluginName === 'Community') {
return require('./Community.js')
}
throw new Error(`Unknown domain plugin: ${domainPluginName}`)
}
}
>
<Page {...} />
<Page {...} />
</DomainPluginHost>
)
DomainPlugin
A Domain Plugin in this frontend layer represents the frontend side of a whole Domain, into which Passion business is divided, such as Courses, Community, Apps, Authentication, Payments etc. Having in mind we see all such domains as bounded contexts (in the terminology of Domain-Driven Design), each needs its backend service support as well as its frontend counterparts. A mobile app can be thus constructed as an aggregation of a number of such domain plugins. Each such frontend plugin consists of 3 major parts - views, widgets and a service, with the following form:
{
getWidget(name: string): ReactComponent,
getView(name: string): ReactComponent,
getName(): string,
getService(): ReactComponent,
async initialize({ useOwnRestrictedInterface: ReactHook }): void
}
Service
In this context a Service represents the frontend "brain" of a DomainPlugin which determines its full lifecycle and aggregates all its business rules. Examples:
- The Service component of the DomainPlugin-Authentication could immediately (on load) reach out to the local storage and determine if the user is already logged in and provide this information through its open interface to other domain plugins.
- The Service component of the DomainPlugin-Courses could consume the open interface of the DomainPlugin-Authentication Service and load/precache the available courses
- The Service component of the DomainPlugin-Marketing could contain the business rule that says if someone didn't buy anything in 30 days they should receive a notification.
The Service is expected to be a React component because this way it gains access to the entire React ecosystem through the use of hooks (e.g. it can use useEffect
for onload events, useQuery
to communicate to the server, useSelector
to consume the Redux state, useOpenInterface
to consume the Service of another DomainPlugin, etc). It should however return null because its purpose is not to render anything (see views and widgets for that below)
Services can have interfaces through which they publish information to the rest of the system. Interfaces can be open (for any other DomainPlugin to consume) or restricted (only available to the views and widgets of its own DomainPlugin). Therefore the Service is provided with the handles for publishing those two types of interfaces as props. Example:
const UsersService = ({ publishRestrictedInterface, publishOpenInterface }) => {
useEffect(() => {
const username = localStorage.get('username')
if (username) {
// The username is considered the public end-result of this DomainPlugin available to all consumers
publishOpenInterface({
username
})
}, [])
const attemptLogin = useCallback(() => { /* ... */ }, [])
useEffect(() => {
// An action to login is considered private to this DomainPlugin so only its own widgets and views can trigger it correctly
publishRestrictedInterface({
attemptLogin
})
}, [attemptLogin])
}
Once published, both interfaces are available to the corresponding other parties through the use of the corresponding hooks.
To consume an open interface, you can simply import the useOpenInterface
hook from the library and specify which other plugin's open interface you want:
import { useOpenInterface } from '@passionio/domain-plugin'
const CommunityService = () => {
const { username } = useOpenInterface('Users')
useEffect(() => {
if (!username) {
return
}
setupMessageListeners(username)
return () => {
destroyMessageListeners(username)
}
}, [username])
}
Only a view or widget of the same DomainPlugin can consume the restricted interface of the belonging Service component. To achieve this, the library provides this private hook called useOwnRestrictedInterface
only to the DomainPlugin itself through its initialize
function (see signature above). The plugin can use this event to get the handle of the hook and use it to its own convenience.
Example 1. The DomainPlugin-Authentication generates a LoginForm Widget class which uses useOwnRestrictedInterface
from closure:
const createLoginFormWidget = ({ useOwnRestrictedInterface }) => {
const LoginForm = () => {
const { attemptLogin } = useOwnRestrictedInterface()
return (
<input {...} />
<input {...} />
<button onClick={attemptLogin} />
}
}
return LoginForm
}
const widgets = {}
export default {
getWidget(widgetName) {
return widgets[widgetName]
}
initialize({ useOwnRestrictedInterface }) {
widgets.LoginForm = createLoginFormWidget({ useOwnRestrictedInterface })
}
// ...
}
Instead of closure, the authors/team of the DomainPlugin may decide to use a different mechanism, e.g. store useOwnRestrictedInterface
to a globally accessible variable from where all its subcomponents can use it or something similar.
Views and widgets
The views and widgets are React components that a DomainPlugin offers to an App (or any other consumer of the DomainPlugin) as something it can render.
Views
Views are also reusable mini-applications but are supposed to occupy the entire screen
- The DomainPlugin-Courses could designate a
MainView
which is what we know as the "courses tab" today - The DomainPlugin-Authentication could offer a view
Login
with all authentication options, as well as aProfile
view with all user's editable information
Widgets
Widgets are reusable mini-applications that can be laid out as the building blocks of a Widgetized
Pages (see #Page above). Examples:
- The DomainPlugin-Courses could offer widgets like
MostPopularCourse
orUnfinishedLesson
, which the App may render on the widgetized Discover page - The DomainPlugin-Community could offer such widgets as
HottestThread
orLatestMessages
that could also be placed on the widgetized Discover page - The DomainPlugin-Apps could have a
Profile
widget that could be placed in the widgetized Settings page
args
Both widgets and views support the args
prop which can be used to customize the behavior of that component. This way the Creator can specify additional parameters to a widget or view when they put it into a Page, the parameters get saved as the Page config in the database and later when the Page is rendered the args will be passed to the widget / view. Technically speaking, when the args are specified in the data
parameter of a widget or view config, they will be automatically passed to the component:
<Page data={{
type: 'FullPageView',
domainPluginName: 'HelloWorld',
viewName: 'World',
args: {
population: '8b'
},
}} />
// ...will automatically be available to:
const HelloWorldView = ({ args: { population } }) => (
<div>Population: {population}</div>
)
// Same for widgets:
<Page data={{
type: 'Widgetized',
widgets: [
{
domainPluginName: 'ManyWidgets',
widgetName: 'SingleWidget',
args: {
color: 'blue'
},
},
},
}} />
// ...will automatically be available to:
const SingleWidget = ({ args: { color } }) => (
<div style={{ color }}>Colored text</div>
)
UI Components
To share UI components from the parent app or shell, you can pass them through context using the guiElements
prop and access them using the useGuiElements
hook.
import { guiElements } from 'src/guiElements'; // e.g. { ProgressBar: () => null, Button: ... }
import { DomainPluginHost } from '@passionio/domain-plugin';
<DomainPluginHost
...
guiElements={guiElements}
>
import { useGuiElements } from '@passionio/domain-plugin';
const Component = () => {
const { Button } = useGuiElements();
return <Button>Click me</Button>;
};
Typescript support
Full scale examples
- https://github.com/independenc3/domain-plugin-test