Skip to content

Latest commit

 

History

History
347 lines (288 loc) · 12.2 KB

app-extensions.md

File metadata and controls

347 lines (288 loc) · 12.2 KB
Title Added
App extensions
3.0.0

App Extensions

ADF lets you simplify the app developer's task by providing an extensible app as a starting point.

An extensible app is designed with extension points, which are placeholders where components and other content can be "plugged in" to provide functionality. The app may be supplied with default content for the extension points but the idea is that a developer can easily replace this with custom content as necessary. An organization might find this useful, for example, if they want to create a family of apps with consistent appearance and behavior. One developer can produce an extensible app that can then be adapted by other developers to create the various apps in the family.

Contents

Extension points

A pluggable extension is implemented by a class or data object that provides its functionality. The class or object is then registered in the app with a key/ID string that is used to reference it. The general idea is that only the ID string is used directly in the main app code to designate the extension point, while the actual implementation is loaded and registered separately. In this respect, extension points work somewhat like translation keys - the key is used to mark a place in the app where the actual content will be supplied dynamically.

Extensibility features

ADF provides a number of features that offer extension points or help with extensibility in general:

  • Components: The Dynamic component has no content of its own but it has an id property that references a registered component extension ID. The referenced component will be added as a child of the Dynamic component at runtime.
  • Routes: These are registered as key/ID strings that resolve to standard Angular routes. This feature can be used, say, that a click on a list item should send the user somewhere but leave the actual destination up to the developer.
  • Auth guards: Routes can be protected by auth guards to prevent unauthorized users from accessing pages they shouldn't see.
  • Rules: These are tests that produce a boolean result depending on the app state. The extensible app can use them with features or ngIf directives, for example, to show or hide content in certain conditions. The exact conditions, however, are chosen by the developer who extends the app.
  • Actions: The extensible app can define a set of application actions that perform basic operations in the app. These are each referenced by a unique key string and can take a data value as a parameter. Items from this set can then be referenced by extension actions. These contain their own key/ID string along with the name of an application action to trigger and a "payload" value to pass as a parameter. The payload can either be a string (to represent a static message, say) or an expression that calculates a result from app state. The expression could, for example, return the current user's name, the currently selected list item or a string composed from several data items.
  • Features: What counts as a "feature" varies according to the application but it is intended to mean any salient piece of functionality that can be customized by extensions. For example, a toolbar, navigation bar, login page or tools menu might all be regarded as features. Any of these features could be extended in a variety of ways. A menu, say, might support custom commands that are implemented by actions with each command enabled or disabled depending on the value returned by a rule.

Setting up an app for extensibility

You can register component classes for use with the Dynamic component using the setComponents method of the Extension service (see the Dynamic component page for further details and code samples). The service also has setAuthGuards and setEvaluators methods that behave analogously.

The recommended way to provide the set of application actions (ie, the built-in actions that can be referenced by extension actions) is to use the scheme defined by @ngrx/store. Briefly, the idea is that all app state is stored centrally and can only be updated by functions triggered by named command strings (eg, "ADD_USER", "CLEAR_SELECTION", "NEW_DOCUMENT", etc). ADF's extensibility features are designed to fit in neatly with @ngrx/store but it has many other advantages, as described on the website.

Creating extensions

The set of basic classes, evaluators and actions provided by the app can be used to set up extensions. The easiest way to configure the extension functionality is with an extension config file. The structure of this file (in JSON format) follows the basic pattern shown below:

{
  "$id": "unique.id",
  "$name": "extension.name",
  "$version": "1.0.0",
  "$vendor": "author.name",
  "$license": "license",
  "$runtime": "1.5.0",
  "$description": "some description",

  "routes": [ ... ],
  "actions": [ ... ],
  "rules": [ ... ],
  "features": { ... }
}

You can use the load method of the Extension service to read the file into a convenient object that implements the ExtensionConfig and ExtensionRef interfaces. Note that the extension.schema.json file contains a JSON schema that allows for format checking and also text completion in some editors.

Replacing Values

By default, the data from the extensions gets merged with the existing one.

For example:

Application Data

{
    "languages": [
        { "key": "en", "title": "English" },
        { "key": "it", "title": "Italian" }
    ]
}

Extension Data

{
    "languages": [
        { "key": "fr", "title": "French" },
    ]
}

Expected Result

At runtime, the application is going to display three languages

{
    "languages": [
        { "key": "en", "title": "English" },
        { "key": "it", "title": "Italian" },
        { "key": "fr", "title": "French" },
    ]
}

You can replace the value by using the special key syntax:

{
    "<name>.$replace": "<value>"
}

Example:

{
    "languages.$replace": [
        { "key": "fr", "title": "French" }
    ]
}

Expected Result

At runtime, the application is going to display languages provided by the extension (given that no other extension file replaces the values, otherwise it is going to be a "last wins" scenario)

{
    "languages": [
        { key: "fr", "title": "French" }
    ]
}

Routes

The routes array in the config contains objects like those shown in the following example:

"routes": [
    {
      "id": "plugin1.routes.customTrash",
      "path": "ext/customtrash",
      "component": "yourCustomTrash.component.id",
      "layout": "app.layout.main",
      "auth": ["app.auth"],
      "data": {
        "title": "Custom Trashcan"
      }
    },
    ...
  ]

You can access routes from the config using the getRouteById method of the Extension service, which returns a RouteRef object. Note that the references to the component and auth guards are extension IDs, as described above.

Actions

The actions array has the following structure:

 "actions": [
    {
      "id": "plugin1.actions.settings",
      "type": "NAVIGATE_URL",
      "payload": "/settings"
    },
    {
      "id": "plugin1.actions.info",
      "type": "SNACKBAR_INFO",
      "payload": "I'm a nice little popup raised by extension."
    },
    {
      "id": "plugin1.actions.node-name",
      "type": "SNACKBAR_INFO",
      "payload": "$('Action for ' + context.selection.first.entry.name)"
    },
    ...
  ]

The Extension service defines a getActionById method that returns an ActionRef object corresponding to an item from this array.

The type property refers to an action type that must be provided by the app (eg, the "SNACKBAR_INFO" in the example presumably just shows a standard snackbar message).

By default, the payload is just an ordinary string that can be used for a message, URL or other static text data. However, you can also define a JavaScript expression here by surrounding it with $( ... ). The expression has access to an object named context which typically contains information about the app state. You can supply the object that contains this data via the runExpression method of the Extension service, which actually evaluates the expression. Note that the result of the expression doesn't necessarily have to be a string.

Rules

The simplest type of rule is configured as shown below:

"rules": [
    {
      "id": "app.trashcan",
      "type": "app.navigation.isTrashcan"
    },
    ...
  ]

The type is the ID of a RuleEvaluator function that has been registered using the setEvaluators method of the Extension service. The evaluator is a boolean function that represents whether a certain condition is true or false (eg, whether an item is selected, whether the user has certain options enabled, etc). The evaluator has access to a context object that is supplied from the app during the call to evaluateRule (defined in the Extension service).

A more complex rule can take other rules as parameters:

"rules": [
    {
      "id": "app.toolbar.favorite.canAdd",
      "type": "core.every",
      "parameters": [
        { "type": "rule", "value": "app.selection.canAddFavorite" },
        { "type": "rule", "value": "app.navigation.isNotRecentFiles" },
        { "type": "rule", "value": "app.navigation.isNotSharedFiles" },
        { "type": "rule", "value": "app.navigation.isNotSearchResults" }
      ]
    }
  ]

This is mainly useful for creating "metarules" that require certain relationships to hold among the parameter rules. A few useful metarules are defined in the core.evaluators.ts file:

  • every: Returns true only if all the parameter rules return true
  • some: Returns true if one or more of the parameter rules return true
  • not: Returns true only if all the parameter rules return false

Note that parameter rules can also recursively invoke their own rules, etc.

Features

The features object does not have any defined structure but the intention is that each key in the object corresponds to the name of a salient feature of the app that can be extended. The object or array that matches the key name contains parameters that modify the behavior of the feature, possibly using actions, rules, etc, defined elsewhere in the config. Suppose, for example, the app has a tools menu that can be extended with extra commands. The properties for a new command might include:

  • The title shown in the menu
  • An icon shown next to the title
  • The action that is activated when the command is selected
  • A rule that determines whether or not the command is enabled

A features object to add an extra item to this menu might look like the following:

"features": {
  "toolmenu": [
    {
      "id": "app.toolmenu.givebiscuit",
      "title": "Give a biscuit to the selected user",
      "icon": "icons/GiveBiscuit.svg",
      "actions": {
        "click": "GIVE_BISCUIT"
      },
      "rules": {
        "visible": "app.biscuits.notempty"
      }
    }
  ]
}