An Intro to Architecture: The Registry

Hey Reactionaries! I don't think this topic has been covered in depth yet on the blog or in our Docs. In the last week of our Introduction to Reaction's Architecture series, we're taking a good look at plugin architecture.

We're going to dive in super deep on the register.js file, which each and every Reaction plugin requires. My hope is that once you've finished reading this post, you'll have a solid foundational understanding of how to create your own Reaction register.js file for plugins from scratch. You'll also have a good library of examples to look at when you get stuck.

When you write your registry file, you will call the Reaction.registerPackage method in a file that lives in /imports/plugins. I'll show you a few examples where we use reactionApps to refer to a plugin. In general, remember that Reaction package = plugin = reactionApp. I'll generally refer to them as plugins in this post.

The Registry is the defining file of any Reaction plugin. You can think of the register.js file as similar, but not identical to, the package.json file you'd use to define an npm package. There are plenty of examples to refer to in our GitHub repo, since almost all of our core functionality has been written as plugins. We have two directories of core plugins, core and included.

Here's a gutted example of a register.js file:

Reaction.registerPackage({  
  label: "PackageName",
  name: "reaction-example-package",
  icon: "fa fa-package",
  autoEnable: true,
  settings: {
    name: "Marketplace",
    enabled: true,
    public: {
      somePublicSetting: true
    }
  },
  registry: [] // Array of registry objects - optional
  layout: [] // Array of layout objects - optional
});

Once registered, plugins are published to the client in the Packages publication. Inspecting this publication will give you some insight as to how and what parts of plugins are published to different parts of the app and how plugin settings can be public or private.

I'm breaking this guide down into 3 sections. First, we'll discuss what I'm calling the top-level properties, which consist of the following: label, name, icon, autoEnable, settings, registry, and layout. After that, we'll look at the architecture of a registry object. And finally, we'll round off the deep dive by taking a look at the architecture of a layout object.

Let's get to it.

Top-Level Properties

Name

name: "reaction-dashboard"  

The name is typically used to refer to the plugin internally, and it must be unique. If you're building your own, you should namespace your plugin to avoid conflicts, e.g. yourorg-plugin-name.

i18n Namespace

The plugin name is also used to determine the i18n namespace for the plugin. You can see how this is used in the reaction-ui Package en.json file and then implemented in filter.js.

Label

label: "Dashboard"  

The label is displayed wherever the client refers to the plugin with a text label.

Icon

icon: "fa fa-th"  

The icon is a set of classes that are used to define an icon. The example above refers to a Font Awesome icon, but with some extra work, it could include other types of icons as well.

AutoEnable

autoEnable: true  

The autoEnable flag tells the app whether or not the plugin should be enabled on load, or if it must be turned on after the app has been started. Most core plugins have this set to true.

Settings

settings: {  
  public: {
    allowGuestCheckout: true
  },
  mail: {
    user: "",
    password: "",
    host: "",
    port: ""
  },
  openexchangerates: {
    appId: "",
    refreshPeriod: "every 1 hour"
  },
  paymentMethod: {
    defaultPaymentMethod: ""
  }
}

The settings property is required, but it can also just be an empty object, {}.

The settings object permits you to define both public and private settings and defaults for a plugin.

Anything that you put into the public property inside settings will be published to the client for all users. Everything that is outside of the public property will be private. API keys, passwords and other sensitive information should never be stored inside the public property. You should also never commit code that has any sensitive information to your repo. Any settings you create here can be added to an admin dashboard so that users can insert sensitive information through a secure connection.

These settings can be retrieved with the getPackageSettings method. The settings object (see also: Reaction Registry Schema) is a blackbox, which means that the schema will permit anything inside the object. For more info, check out the blackbox docs for meteor-simple-schema.

Registry

registry: [...RegistryObjects],  

The registry array of registry objects is where you can define routes and templates. We'll get into the properties in much more detail below. This setting is optional.

Layout

layout: [...LayoutObjects]  

The layout array of layout objects is where you can define routes and templates. We'll get into all of the properties in much more detail below. This setting is optional.

The Registry Property

The registry property takes an array of objects that use the Registry Schema to define them. Below, you'll find an example from our Core Accounts plugin:

registry: [{  
    route: "/dashboard/accounts",
    name: "accounts",
    provides: "dashboard",
    label: "Accounts",
    description: "Manage how members sign into your shop.",
    icon: "fa fa-users",
    container: "core",
    template: "accountsDashboard",
    workflow: "coreAccountsWorkflow",
    priority: 1
  }, {
    route: "/account/profile/verify:email?",
    label: "Account Verify",
    name: "account/verify",
    workflow: "coreAccountsWorkflow",
    template: "verifyAccount"
  }, {
    label: "Account Settings",
    icon: "fa fa-sign-in",
    provides: "settings",
    route: "/dashboard/account/settings",
    container: "accounts",
    workflow: "coreAccountsWorkflow",
    template: "accountsSettings"
  }, {
    route: "/dashboard/accounts",
    name: "dashboard/accounts",
    workflow: "coreAccountsWorkflow",
    provides: "shortcut",
    label: "Accounts",
    icon: "fa fa-users",
    priority: 1,
    container: "dashboard",
    template: "accountsDashboard"
  }, {
    route: "/account/profile",
    template: "accountProfile",
    name: "account/profile",
    label: "Profile",
    icon: "fa fa-user",
    provides: "userAccountDropdown"
  }],

Reading through this real-world example should give you a framework for the different ways you can use the registry for your plugin.

Provides

provides: "dashboard",  

People frequently ask about this property, so I'd like to go more in-depth with it. At the core, this property tells us how a part of the app should be used. Different provides values will have different requirements in terms of the other properties that should be listed alongside them.

The best way to understand exactly how to structure a registry entry that provides a certain service, template, or route is to look at an example of how a core plugin does it. I've provided links to different parts of our code for each valid provides value. Feel free to ask more in-depth questions, and we can continue to make this document better.

Currently, the following are valid provides values:

provides: "addressValidation" - Registers an address validation service to perform address validation. This isn't associated with a route.

provides: "addressValidation" requires:

  • label
  • name
  • provides

For more examples, check out our Avalara integration: Avalara register.js, Avalara Address Validation Method Definition, and Avalara validateAddress method.

provides: "catalogSettings" - This will register a template to appear in the catalogSettings panel of the dashboard.

provides: "catalogSettings" requires:

  • label
  • name
  • template
  • provides

To see an example, check out our Revisions plugin.

provides: "dashboard" - This will cause a link with an icon and a label to appear in the Action Panel. The link will only be visible for users with appropriate permissions. This value is very similar to provides: "settings".

To see where these are inserted, check out the packageListContainer. For an example of how to insert these into your plugins, check out our Accounts plugin.

provides: "dashboard" requires:

  • container (unused)
  • description (unused)
  • icon
  • label
  • priority (unused)
  • provides
  • name
  • route (unused)
  • template
  • workflow (unused)

provides: "paymentMethod" - This will register a payment method form template. These payment forms will get rendered at checkout if the payment method is enabled. For examples, check out our Stripe plugin method. Each enabled payment method is listed in the dropdown to select a default payment method. These are selected from registry entries that provide "paymentMethod" in the payment settings.js file.

provides: "paymentMethod" requires:

  • template
  • icon
  • provides

provides: "paymentSettings" - This will register a template to appear in the paymentSettings panel of the dashboard. See the register.js of our Stripe plugin as a reference.

provides: "paymentSettings" requires:

  • label
  • template
  • provides

provides: "settings" - This will register a link with an icon and a label to appear in the dashboard settings section. Link will only be visible for users with appropriate permissions. This value is very similar to provides: "dashboard".

For examples of where and how to insert these into your plugis, see the pageListContainer and the Shipping register.js.

provides: "settings" requires:

  • label
  • name
  • description (unused)
  • icon
  • template
  • provides

provides: "shippingSettings" - This will register a template to appear in the shippingSettings panel of the dashboard. For an example, check out our Flat Rate Shipping plugin.

provides: "shippingSettings" requires:

  • label
  • template
  • icon
  • name
  • description
  • provides

provides: "shortcut" - This adds a link to an admin dropdown menu. When a user image or name is clicked on, a list of shortcuts (which the user has been given permission to access) will appear. shortcuts and userAccountDropdowns are fairly similar, except shortcuts will check for permissions before displaying a link, while userAccountDropdowns will display the link to all logged-in users.

Shortcuts get passed into mainDropdown.js and then rendered to the DropDownMenu component in the same file. See the Accounts register.js for an example of a shortcut in the registry.

provides: "shortcut" requires:

  • route (unused)
  • name
  • workflow (unused)
  • label
  • icon
  • priority (unused)
  • container (unused)
  • template
  • provides

provides: "social" - This will register a template to appear in the product social template. The only core plugin that currently uses this setting is the Social plugin (example here).

provides: "social" requires:

  • template
  • provides

provides: "taxCodes" - This will register getTaxCodesMethod. We find the taxCodes provider in variantFormContainer.js. See an example via our Avalara plugin.

provides: "taxCodes" requires:

  • label
  • name
  • provides

provides: "taxSettings" - This will register a template to appear in the Tax section of the dashboard. See our core Taxes plugin register.js for an example.

provides: "taxSettings" requires:

  • label
  • name
  • icon
  • template
  • provides

provides: "ui-search" - Registers a template to appear in the search UI. Currently, the search UI only uses a single template, so unless you remove the core search UI plugin, adding additional search templates won't really have an effect. For examples, check out the search UI register.js, which gets pulled into the navbar in navbarContainer.js.

provides: "ui-search" requires:

  • name
  • template
  • provides

provides: "userAccountDropdown" - This will add a link to a logged-in user's dropdown menu. When you click on a user's image or name when logged in, you'll seeing a list of shortcuts they have permission to access. shortcuts and userAccountDropdowns are similar. The primary difference is that shortcuts will check for permissions before displaying a link, while userAccountDropdowns display the link to all logged-in users. userAccountDropdowns get passed into mainDropdown.js, then rendered to the DropDownMenu component in the same file. Currently, the only plugin that registers a userAccountDropdown is the Accounts plugin register.js.

provides: "shortcut" requires:

  • route
  • name
  • label
  • icon
  • template
  • provides

Route

route: "/dashboard/accounts"  

The route property registers the supplied route with Reaction. Once registered, you'll be able to visit this route by navigating directly to example.com/shop-prefix/your-route. If you have more than one shop, you'll need to use the shop prefix when visiting a route directly. See also: Routing Docs, getShopPrefix, and Router.initPackageRoutes.

The route property is named by the name property, if provided.

Name

name: "accounts"  

The name property permits calling this route with Router.go - e.g.

Router.go("accounts");

The name property also becomes a permission or role that users can have. Having a role that corresponds to the name within a registered route permits the user to visit that route (but only for the shopId they have that role for). For more details, check out our Routing Docs.

Template

template: "accountsDashboard"  

The template property can be assigned to a Blaze template, although where it is used depends on the value of provides. If you'd like to use a React component in place of a Blaze template, you just need to wrap your component in a Blaze template. There's an example of this in the Email plugin template file.

Workflow

workflow: "coreProductWorkflow"  

Workflows are currently used to reference a layout for a template. We'll dig into this a bit in the Layouts section, but a workflow is essentially a unique structure within a given layout. Some registry entries require a workflow to be specified, and this will determine the structure around the template. This applies for the layoutHeader, layoutFooter, notFound template, etc.

Mostly, these are used when registering a route (with no provides property). An example of this can be found in the product-variant register.js.

Layout

layout: "printLayout"  

The layout property can be used to force the app to render a specific layout. The primary use for this is to define routes with a custom workflow where perhaps a header, footer, etc. is not necessary. We leverage this in the orders register.js to define a PDF print layout for orders.

Description

description  

This was primarily used in the Reaction Packages Grid. This field permits you to set a description for your registry entry. Although it's now deprecated, it's still useful for conveying information to other programmers. It may also potentially see use in the app again.

Icon

icon: "fa fa-globe"  

Again, the icon is a set of classes that are used to define an icon. The example above refers to a Font Awesome icon, but with some extra work, it could include other types of icons as well.

Label

label: "Orders"  

The label is the human readable name for a registry entry. This should be originally written in English. You may then use i18n files within each plugin to translate them into other languages.

Container

container: "core"  

This is used to group plugins. Saved for later use.

The container is used to define which part of the app a plugin belongs to. This permits us to put different registry entries in their appropriate sections, eg. permissions and shortcuts.

Priority

priority: 3  

The priority property takes a positive integer as its value. It determines the order that this registry entry is loaded. Lower values will load first.

Enabled

enabled: true  

This determines if a registry entry is enabled or not.

Permissions

permissions: [{  
  label: "Create Product",
  permission: "createProduct"
}]

The permissions property allows you to define a new permission. The permissions label will be used for reference, and it will show up in the permissions list. If the registry entry is permission-controlled, it will also become the requirement for access. This property is used by the createProduct shortcut.

Audience

The audience property is used to define what permissions are required to view a step in a workflow. Primarily this is used in our checkout workflow. This property will almost certainly not work the way you expect it to if it's used in a plugin. It's much more complex than simply defining which roles can access a route.

Meta

meta: {  
  actionView: {
    dashboardSize: "md"
  }
}

Currently, the only setting used within the meta property is to set the dashboardSize for the actionView. The Email register.js is an example of this.

The Layout Property

The registry property takes an array of objects that are defined by the Layout Schema. Here's an example from our Core Accounts plugin:

layout: [{  
  workflow: "coreAccountsWorkflow",
  layout: "coreLayout",
  collection: "Accounts",
  theme: "default",
  enabled: true,
  structure: {
    template: "accountsDashboard",
    layoutHeader: "layoutHeader",
    layoutFooter: "",
    notFound: "notFound",
    dashboardHeader: "dashboardHeader",
    dashboardControls: "",
    dashboardHeaderControls: "",
    adminControlsFooter: "adminControlsFooter"
  }
}]

Workflow

workflow: "coreAccountsWorkflow"  

The combination of workflow + layout determines what gets rendered to the client. By looking up a workflow + layout combination we can get the structure that's embedded here and render the correct templates to each section of the layout.
For examples of how the workflow and layout work together like this, dig into router.js

We also use workflows as a sort of state machine in the case of our checkout and order fulfillment processes. Checkout workflow.js to see how this works.

Layout

layout: "coreLayout"  

For any given workflow/layout combination, the layout property defines which macro-level layout should be used. Each layout can have a different structure. Our core layout component is defined in coreLayout.js and registered in index.js, along with our only other included layout, printLayout.

Collection

collection: "Accounts"  

The collection property declares which Mongo collection is associated with this workflow/layout combination. It shouldn't be necessary to define this for most plugins.

Theme

theme: "default"  

The theme property defines which CSS theme to use. Currently, "default" is the only included option.

Enabled

enabled: true  

The enabled property turns this workflow/layout combo on or off.

Structure

structure: {  
  template: "accountsDashboard",
  layoutHeader: "layoutHeader",
  layoutFooter: "",
  notFound: "notFound",
  dashboardHeader: "dashboardHeader",
  dashboardControls: "",
  dashboardHeaderControls: "",
  adminControlsFooter: "adminControlsFooter"
}

The structure property is where specific templates can be assigned to a layouts areas. Each layout may have it's own unique structure and so some render locations may render in one layout and not in another.

Conclusion

I hope that this post has given you a better understanding of how a register.js file for a Reaction plugin can be structured. There's a lot of information here, and while the initial read-through should do you some good, I'd recommend bookmarking this post for future reference.

If you have any implementation questions, or if you ever get stuck along the way, don't hesitate to join our Gitter chat. We have a general channel, as well as a Plugins channel. Our community is very active, and you'll find core Reaction developers to reach out to through these channels as well.

If this was helpful to you, or if you have any follow-up questions, let me know in the comments.

Thanks for reading!

comments powered by Disqus