@passionio/feature-plugin
TypeScript icon, indicating that this package has built-in type declarations

0.0.12 • Public • Published

Description

This library is the core engine which loads a set of Passion frontend features according to the provided configuration, and enables the app to render Pages which consist of views and widgets belonging to those features.

  +-------------------------App-------------------------------+
  |                                                           |
  |  +-----------------FeatureSet----------------------+      |
  |  |                                                 |      |
  |  |    +-----Feature1---+    +---Feature2----+      |      |
  |  |    |  <Service>     |    | <Service>     |      |      |
  |  |    | ▲<views>       |    | <views> ◄-+   |      |      |
  |  |    | |<widgets      |    | <widgets> |   |      |      |
  |  |    +-|-------▲------+    +▲----------|---+      |      |
  |  +------|--------\----------/-----------|----------+      |
  |         |         \        /            |                 |
  |         |          \      /             |                 |
  |         |           \    /              |                 |
  |     +---|Page1+   +--Page2--+   +--Page3|--+              |
  |     |   |     |   | Widgets |   |       |  |              |
  |     |  View   |   |         |   |   View   |              |
  |     |         |   |         |   |          |              |
  |     |         |   |         |   |          |              |
  |     |         |   |         |   |          |              |
  |     |         |   |         |   |          |              |
  |     +---------+   +---------+   +----------+              |
  |                                                           |
  +-----------------------------------------------------------+
      

Table of Contents

Usage

Install:

yarn add @passionio/feature-plugin

Use:

  import { Page, FeatureSet } from '@passionio/feature-plugin'
  
  export const App = () => (
    <FeatureSet featureConfig={[{ featureName: 'HelloWorld' }]} importFeature={featureName => features[featureName]}>
       <Page data={{
         type: 'FeatureView',
         featureName: 'HelloWorld',
         viewName: 'Main',
       }} />
    </FeatureSet>
  )

Page

Pages are the main building blocks of a Passion.io app. It can be of one of two types:

  • FeatureView - the page consists of a single View which belongs to a single Feature plugin, and occupies the whole page. To create a Page of this type one needs to specify the name of the feature 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 feature. For each widget the name of the parent feature and the name of the widget needs to be specified.

Example 1. A widgetized Page consisting of a widget AcademyCourse defined in the Courses feature, and a Testimonials widget belonging to a different feature called Marketing:

  {
    type: 'Widgetized',
    widgets: [
      {
        id: 1,
        featureName: 'Courses',
        widgetName: 'AcademyCourse',
      },
      {
        id: 2,
        featureName: 'Marketing',
        widgetName: 'Testimonials',
      },
    ]
  }

Example 2. A feature view Page which consists of a single view Main pulled from the Courses feature:

  {
    type: 'FeatureView',
    featureName: 'Courses',
    viewName: 'Main',
  }

To render a Page in your app, you need to nest it as a child within the main FeatureSet component. This way the View and Widget components inside the page gain access to the loaded features and their interfaces:

  import { Page, FeatureSet } from '@passionio/feature-plugin'
  
  const App = () => (
    <FeatureSet {...}>
       <Page data={{
         type: 'FeatureView',
         featureName: 'Courses',
         viewName: 'Main',
       }} />
    </FeatureSet>
  )

FeatureSet

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 <FeatureSet> in your app, you must provide two props:

  • featureConfig - an array of feature descriptors in the form:
      {
        featureName: "Feature1"
        version: "^1.0.0"
      }
  • importFeature - an async function which loads a feature by name, e.g.
      const importFeature = featureName => {
        const featureModule = await import(`./${featureName}.js`)
        return featureModule.default
      }

Example:

const App = () => (
  <FeatureSet
    featureConfig={[
      {
        featureName: 'Courses',
        version: '0.0.1'
      },
      {
        featureName: 'Community',
        version: '0.0.2'
      },
    ]}
    importFeature={
      featureName => {
        if (featureName === 'Courses') {
          return require('./Courses.js')
        }
        if (featureName === 'Community') {
          return require('./Community.js')
        }
        throw new Error(`Unknown feature: ${featureName}`)
      }
    }
  >
    <Page {...} />
    <Page {...} />
  </FeatureSet>
)

Feature

A Feature in this frontend layer represents the frontend side of a whole conceptual "feature" that Passion offers to its users, such as Courses, Community, Apps, Users, Creator Payments etc. Having in mind we see all such features 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 seen as an aggregation of a number of such features or feature plugins. Each such feature frontend plugin consists of 3 major parts - views, widgets and a frontend service - and is expected to take 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 the Feature which should determine its full lifecycle. Examples:

  • The Service component of the Users Feature 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 feature plugins.
  • The Service component of the Courses Feature could consume the open interface of the Users Feature Service and load/precache the available courses
  • The Community Feature Service could initialize the websockets or any polling mechanism to start receiving messages from the server

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 another Feature Service, 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 Feature to consume) or restricted (only available to the views and widgets of its own Feature). 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 Feature available to all consumers
        publishOpenInterface({
          username
        })
    }, [])
    
    const attemptLogin = useCallback(() => { /* ... */ }, [])
    useEffect(() => {
      // An action to login is considered private to this Feature 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, one can simply import the useOpenInterface hook from the library and specify which feature's open interface it wants:

  import { useOpenInterface } from '@passionio/feature-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 Feature can consume the restricted interface of the belonging Service component. To achieve this, the library provides this private hook called useOwnRestrictedInterface only to the Feature itself through its initialize function (see signature above). The feature can use this event to get the handle of the hook and use it to its own convenience.

Example 1. The Users Feature 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 Feature authors / team 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 Feature offers to an App (or any other consumer of the Feature) as something it can render.

Views

Views are also reusable mini-applications but are supposed to occupy the entire screen

  • The Courses Feature could designate a MainView which is what we know as the "courses tab" today
  • The Users Feature could offer a view Login with all authentication options, as well as a Profile 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 Courses Feature could offer widgets like MostPopularCourse or UnfinishedLesson, which the App may render on the widgetized Discover page
  • The Community Feature could offer such widgets as HottestThread or LatestMessages that could also be placed on the widgetized Discover page
  • The Users Feature 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: 'FeatureView',
    featureName: '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: [
      {
        featureName: '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 { FeatureSet } from '@passionio/feature-plugin';

<FeatureSet
  ...
  guiElements={guiElements}
>
import { useGuiElements } from '@passionio/feature-plugin';

const Component = () => {
  const { Button } = useGuiElements();
  return <Button>Click me</Button>;
};

Typescript support

Typescript docs

Full scale examples

Readme

Keywords

none

Package Sidebar

Install

npm i @passionio/feature-plugin

Weekly Downloads

0

Version

0.0.12

License

UNLICENSED

Unpacked Size

84.8 kB

Total Files

20

Last publish

Collaborators

  • passionio