ui-core

1.5.0 • Public • Published

ui-core

Ui-core is a framework for building single page applications using angular. The module provides shared code that runs on the server and client.

Ui-core is currently using Angular v1.5.3

server

The server component of ui-core provides several key features:

  • Basic routing capabilities for loading static files
  • Advanced routing for dynamically loading modules
  • An plugin model for adding components shared across products

usage

Using ui-core requires adding the dependency in your package.json file:

"dependencies": {
    ...
    "ui-core": "git+ssh://git@github.com:medseek-engineering/ui-core.git"

And starting the application (generally in app.js):

var ui = require('ui-core')();
ui.start();

structure

The framework expects a specific application structure. All existing apps follow a common structure which minimally includes 'app' and 'config' directories at the root. The 'app' directory contains the client code, and should contain the following subdirectories:

  • app/directives
  • app/filters
  • app/layouts
  • app/modules
  • app/services
  • app/templates
  • app/themes

Javascript files in the 'directives', 'filters', and 'services' directories are automatically loaded on startup. These scripts get injected into the document head of the initial response from the server.

Please note: These directories are meant to contain application level code. They should be restricted to files which contain code that needs to be shared by multiple modules. If your code is module specific, it should be placed in the appropriate subdirectory under app/modules.

layouts and modules

The framework provides a lightweight module loading system. A module is an independent set of code, which may optionally contain an entry point, and view. Both layouts and modules are loaded through the same mechanism, but are intended to be used for different purposes.

Layouts should be placed under the 'app/layouts' directory. Layouts should be used to provide the visual structure of the page and should not contain business logic. Layouts are generally loaded directly into the body of the page.

Modules should be placed under 'app/modules' directory. Modules are the main visual components of the application.

The module loading system produces a JSON manifest based on this convention. If it exists, the index.html file is specified as the visual entry point. All javascript fields included within a module or layout are executed on loading of the module or layout.

configuration

The framework uses the medseek-config module (see: https://github.com/medseek-engineering/medseek-config) to apply various settings which affect the application. The notable parts of this are:

  • client.debug: Specifies the application should run in debug mode
  • client.head.stylesheets: Application level CSS files which are placed in the head
  • client.head.scripts: Application level JS files which are placed in the head
  • client.head.addlPathedScripts: include scripts that are already pathed in the compress array
  • client.app: An object which gets pushed to the client (as window.app)
  • server.hosts: A comma separated list of hostnames to bind to.
  • server.useSsl: boolean on whether or not to use an ssl certificate.
  • server.cert.key: path to ssl cert.key
  • server.cert.crt: path to ssl cert.crt

plugins

Plugins are supported where code needs to be shared across multiple products. Plugins should be separate git repositories and included in the application as NPM dependencies. This allows us to leverage the power of git for versioning, as well as continue using NPM as the package manager.

In order to use an existing plugin, you need to add a dependency (in package.json):

"dependencies": {
    ...
    "ui-your-extension": "git+ssh://git@github.com:medseek-engineering/ui-your-extension.git"

And, apply the extension (in app.js):

var ui = require('ui-core')();

// apply ui extensions
ui.use(require('ui-your-extension'));

// start the UI server
ui.start();

writing plugins

Plugins should exist as independent repositories, and be valid NPM packages. The framework applies plugins in two stages:

  1. Configuration extension - Modify the app config object
  2. Application extension - Modify the express app object

An example of a basic plugin which provides CSS and JS files (index.js):

var rootDir = __dirname + '/';

module.exports = {
  
  config: function(conf) {
    console.log('Using ui-your-extension');
    if (conf.client.head.settings &&
        conf.client.head.settings.combine &&
        conf.client.head.addlPathedScripts) {
      // addlPathedScripts are scripts that have been pathed that need to be included in the compressed files.
      // by adding to the compressed files, we minimize the number of requests for additional plugins
      conf.client.head.addlPathedScripts.push(rootDir + 'your-extension.js');

    } else {
      // add a script to the head
      conf.client.head.scripts.push(conf.client.app.root + '$ui-your-extension/your-extension.js');
    }
    
  },

  app: function(app, conf) {
    app.get('/\\$ui-your-extension/*', function(req, res) {
      res.sendfile(rootDir + req.params[0]);
    });
  }
};

This would map a request for the route /$ui-your-extension/* to the directory of your extension and return a matching static file. In the example above, you should have a file named 'your-extension.js' available at the root. Depending on the complexity of the plugin, you may want to choose a more suitable directory structure. For an example of an existing plugin, see: https://github.com/medseek-engineering/ui-patterns

Plugins also support providing custom module resolvers. By default the ui-core provides a single module resolver that builds module manifest files based on the app structure conventions. This feature is not currently used in any existing repository, but may be useful in the future if the need arises to provide shared layouts of modules between the products. Your extension would need to provide a 'resolve' function which accepts a module path and callback function. For an example, see the core module resolver: https://github.com/medseek-engineering/ui-core/blob/master/lib/fsResolver.js

server summary

The server componet of ui-core provides us with a lightweight abstraction layer over express. It automatically loads required core artifacts such as angular and jQuery. It provides automatic loading of files that adhere to conventions useful for angular development. It provides an on-demand module loading system. It provides a plugin model that allows us to share UI code between products.

client

The client component of ui-core provides several key features:

  • Setup of the angular app (angular module)
  • A lightweight abstraction over angular's provider recipes
  • Integration with ui-core module loading system
  • Basic client route management

the app object

The app object (window.app) is a global object which contains application config settings, and easy access to angular provider functions. The app API is as follows:

  • app.ng: direct access to the angular 'app' module
  • app.controller: shorthand access to angular.module('app').controller
  • app.directive: shorthand access to angular.module('app').directive
  • app.factory: shorthand access to angular.module('app').factory
  • app.filter: shorthand access to angular.module('app').filter
  • app.value: shorthand access to angular.module('app').value
  • app.constant: shorthand access to angular.module('app').constant
  • app.config: shorthand access to angular.module('app').config
  • app.run: provides the ability to run code with injectables
  • app.require: provides the ability to load modules as dependencies
  • app.locate: provides the ability to lookup services by name (useful for debugging)
  • app.softRefresh: forces an angular digest loop (useful for debugging)

In addition to this, the app also contains any data set on the client.app section of the configuration (generally in config/app.config.js). By convention, at a minimum this configuration contains:

  • app.root: the root of the application (typically just '/')
  • app.api.root: the root of the medseek API
  • app.routes: provides routes used by the core for page level route management

Note: window.app should be the only global object defined by our applications and you should not add additional data onto the app object at runtime. All other app level functionality and data (aside from 3rd party code which we cannot control) should be wrapped in an IIFE (see: http://en.wikipedia.org/wiki/Immediately-invoked_function_expression), and accessed as injectables, using angular's dependency injection system.

page routing

The core provides route management which integrates with the module system to easily specify page level routes which load layouts, and modules. Routes may come from any endpoint (such as the medseek API, in the future), but are generally defined in a local file (app/routes.json) for ease of development. Each route should provide an object which represents the page to be displayed. The format of the page object is:

"/my-route": {
    "title": "My Page",
    "layout": "layouts/default",
    "theme": "themes/default",
    "sections": {
        "main": "modules/my-module"
    }
}

The routing system matches routes based on specificity (not order of declaration). In the case above, any route starting with '/my-route' would match ('/my-route/a', '/my-route/a/b', etc). However, if you added a second route with greater specificity, such as:

"/my-route/a": { ... }

The routing system would then match '/my-route/a' to the second object.

Page routes also support route parameters of the form '/my-route/:id'. In this example, the ':id' portion of the route can be any value, and will be available in the route data. Note that parameters are less specific that a static value, so a URL of '/my-route/a' would still match the fixed route.

Additional route sections beyond the matching portion are available as view parameters in the route data.

Route data is accessible via the app object. The following API is provided:

  • app.routing.data: the actual page object, resolved from the route
  • app.routing.route: the matching route (for example, /my-route/:id)
  • app.routing.routeParams: the route params (for example, /my-route/5 would set routeParams.id equal to 5)
  • app.routing.routeUrl: The actual URL of the matching route (for example, /my-route/5)
  • app.routing.viewParams: any additional params beyond the matching route portion (for example, /my-route/5/foo would result in viewParams being equal to ['foo']

core directives

The core provides two angular directives of interest:

  • module: integrates with the module system to easily load modules
  • viewSelector: integrates with routing to provide simple MVC routing within modules

Example usage of module directive (in a layout, index.html file):

<module path="page.sections.main" />

This would load a module using the value of scope.page.sections.main as the key. Note that the page object associated with the matching route generally provides this information. For example, the 'my-route' page above provided a sections.main value of 'modules/my-module'. Modules can also be hardcoded to a specific key by using single quotes:

<module path="'modules/my-module'" />

Example usage of viewSelector directive (in a module, index.html file):

<view-selector params="model.routeParams">
    <view route="" path="views/default.html"></view>
    <view route="list" path="views/list.html"></view>
    <view route="detail/:id" path="views/detail.html"></view>
</view-selector>

View routing works largely the same as page level routing, and allows us to manage routes independent of the modules page route (routes match relative to the module root). This works by integrating with the route system, and using the app.routing.viewParams array. The params attribute allows mapping of route parameters to an object (in this case, scope.model.routeParams). For example, building on the above page route, the following routes produce:

  • '/my-route': loads my-module, and displays the file views/default.html
  • '/my-route/list': loads my-module, and displays the file views/list.html
  • '/my-route/detail/5': loads my-module, and displays the file views/detail.html (with scope.model.routeParams.id set equal to 5)

writing components

Client side components follow the available recipes defined by angular, they are:

  • controllers
  • services
  • directives
  • filters

Components that provide application level functionality should be placed under the appropriate subdirectory under app (app/services, app/directives, etc). Components that provide functionality specific to a module should be placed under the module (app/modules/my-module/services, app/modules/my-module/controllers, etc). Components should typically have their own file, and follow a naming convention where the filename matches the component name. Components should always be wrapped in an IIFE (see: http://en.wikipedia.org/wiki/Immediately-invoked_function_expression).

Example of a service (services/foo.js):

(function (app) {
  'use strict';
  app.factory('foo', [function () {
    return {
      // service methods
    };
  }]);
})(window.app);

Example of a controller (controllers/ListCtrl.js):

(function (app) {
  'use strict';
  app.controller('MyModuleListCtrl', ['$scope', function (scope) {
    
  }]);
})(window.app);

Note: The controller name is prefixed with the module name to help avoid naming clashes.

build

If you make changes to the client code, you will need to run gulp to rebuild the combined/minified main.js file.

client summary

The client componet of ui-core provides us with the basic functionality required for building single-page apps on top of angular. It provides route management for pages and support for MVC design patterns within modules. It provides integration with the server components to share UI config settings and automatically load and display layouts and modules.

Dependents (1)

Package Sidebar

Install

npm i ui-core

Weekly Downloads

66

Version

1.5.0

License

none

Last publish

Collaborators

  • acsearles
  • mcastre
  • mstone6769