npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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 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 DomainPlugin-Courses could offer widgets like MostPopularCourse or UnfinishedLesson, which the App may render on the widgetized Discover page
  • The DomainPlugin-Community could offer such widgets as HottestThread or LatestMessages 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

Typescript docs

Full scale examples

  • https://github.com/independenc3/domain-plugin-test