Documentation

3 Frontend framework

An Oskari-based frontend application includes the Oskari framework code and a selection of bundles that implement functionalities for the application. Bundles have a lifecycle and are started in sequence. Bundles can communicate with each other using events, requests and services. The framework code of Oskari provides the messaging system for events, requests and service registry but also an API which bundles need to implement so they can be included in an Oskari based application like having lifecycle functions/a starting point that can be called when the functionality is started.

The sequence diagram below explains what happens in the frontend initialization process (try reloading the page if the diagram is not rendered properly).

sequenceDiagram
  box rgb(245, 242, 222) Oskari-frontend
  participant Oskari as Oskari
  participant Bundle as Bundle
  end
  box rgb(163, 196, 188) Oskari-server
  participant Server as Server
  end

  Oskari ->> Oskari: Oskari.app.startApplication()
  Oskari ->> Server: GetAppSetup (UUID)
  Server -->> Oskari: app definition
  loop For each bundle
    Oskari ->> Bundle: inject configuration
    Oskari ->> Bundle: start
    Bundle -> Bundle: init functionality
    Bundle ->> Oskari: bundle.started
  end
  Oskari ->> Oskari: app.started

The frontend in started by the application code calling Oskari.app.startApplication() (in applications index.js for example) which triggers a call for the server action route called GetAppSetup. The GetAppSetup response lists all the bundles that should be started for that specific application and includes the configuration and state of those bundles (like which layers are on the map and what are the coordinates for the center of the map etc). The application to start is noted by an UUID when the page is opened (usually a parameter on the page URL) but the server has several options to default an appsetup based on user role etc. The frontend then proceeds with starting the requested bundles with the included configuration in sequence until the whole application has been started. An event is triggered after each started bundle and another once the whole application has been started that enables programmatically react to such lifecycle events.

The bundle called mapfull is usually a starting point for the bundle sequence as it creates the map implementation that most functionalities expect to be present when started. This documentation section contains documentation for the frontend framework that Oskari frontend provides and what it means for applications that are built using oskari-frontend.

3.1 oskari-frontend directory structure

Oskari frontend source code has the following folder structure:

  • /api - The documentation for bundles and API they provide with a change log of changes to the API
  • /bundles - Implementation files for built-in bundles
  • /resources - Common CSS styles/images
  • /src - Code for Oskari framework
    • src/react - UI-components library
  • /tools - Random templates and scripts for generating CSV-files based on localization
  • /webpack - Helpers and configurations for current build tools
  • /libraries - Older jQuery plugins and other dependencies/libraries that are not reasonably available through npm
  • (/packages) - Legacy-definition files for bundles (content is being migrated to bundles with new syntax).

The main folders are:

  • bundles (functionality implementations you can use in applications)
  • src (for framework code)
  • webpack (for build scripts)

Note! We have started migrating the contents under packages folder to bundles with a newer format. These two formats require different Webpack-loaders when referenced from the application main.js file.

The folder structure for bundles follows a pattern where the first folder under the base folder is a namespace folder. Oskari uses framework and mapping namespaces for most of the bundles and admin for admin tools. The namespace is purely cosmetic and is for grouping the bundles/organizational purposes. When creating app-specific bundles you don't have to use a namespace but you can if you wish. The next folder after the namespace is named after the {bundle-identifier}. Note that under the packages folder there can be a folder with the name bundle in between.

3.1.1 Frontend libraries and technologies

Oskari frontend uses the following libraries and technologies (for details see package.json on the oskari-frontend repository):

  • OpenLayers (map implementation)
  • jQuery (older UI implementations, migrating towards React)
  • React (current UI implementations)
  • Ant Design (UI components and icons)
  • CesiumJS (3d map implementation)
  • Lo-Dash
  • geostats.js
  • D3.js

You can get a list of licenses for all the libraries with npm, for example by running:

npx license-checker

Just to get summary of licenses you can add --summary after the command:

npx license-checker --summary

3.2 Oskari global and sandbox

The Oskari frontend framework functionality is located in the src folder in oskari-frontend repository. The folder also includes React.js based UI-component library (based on AntD-components).

3.2.1 Oskari global

A global Oskari variable is introduced for Oskari frontend applications to access the framework functionalities.

It provides some functionalities that applictions can use (bundles usually) like:

  • application environment like:
    • current language Oskari.getLang()
    • supported languages Oskari.getSupportedLanguages() and Oskari.getDefaultLanguage()
    • setup configuration Oskari.app.getApplicationSetup()
    • type Oskari.app.getType() and uuid Oskari.app.getUuid()
    • theme Oskari.app.getTheming().getTheme() and setTheme()
  • instance customization:
    • marker selection Oskari.getMarkers() and Oskari.getDefaultMarker()
    • default app setups Oskari.app.getSystemDefaultViews()
    • urls Oskari.urls.getRoute('route name')
  • bundle registry Oskari.bundle() and Oskari.lazyBundle()
  • sandbox registry (see below) Oskari.getSandbox()
  • app/bundle lifecycle handling Oskari.on('bundle.start') and Oskari.on('app.start')
  • tracking of current user Oskari.user()
  • access to localizations Oskari.getMsg(locId, locKey, params)
  • class system (being migrated away in favor of ES-classes) Oskari.clazz.define() and .create()
  • helper functions:
    • for colors etc Oskari.util.hexToRgb('#FFAA33')
    • number formatting Oskari.getNumberFormatter()
    • coordinate formatting Oskari.util.coordinateMetricToDegrees()
    • sequence tracking Oskari.getSeq('sequenceId').curVal() and .nextVal()
    • url building Oskari.urls.buildUrl('https://mydomain/path', { paramName: 'value' })
  • DOM element helpers like Oskari.dom.getNavigationEl()
  • logging functionality Oskari.log('loggername').warn('some warning')

3.2.2 Sandbox

Sandbox is:

  • event/message bus for frontend bundles to communicate through requests and events etc
    • sandbox.postRequestByName('AddMapLayerRequest', [...params]);
    • sandbox.notifyAll(someEvent)
    • sandbox.addRequestHandler(someReqName, someHandler)
    • sandbox.registerForEventByName('AfterMapLayerAddEvent')
  • registry for modules sandbox.register(this) (required for listening to events)
    • sandbox.findRegisteredModuleInstance()
  • stateful bundle registry registerAsStateful(bundleId, bundleInstance)
  • registry for services sandbox.registerService(someService) and sandbox.getService(serviceName)
  • conveniency getters for map state:
    • sandbox.getMap()
    • sandbox.findAllSelectedMapLayers()
  • functionality context for scoping events/requests

There can be multiple sandboxes in an Oskari frontend application to scope messaging, but most commonly there is just one that can be accesssed with Oskari.getSandbox(). The function takes a string-parameter enabling multiple sandbox instances to be used but it's very uncommon to use other than the default sandbox.

Bundle instances get a sandbox reference as parameter on their start(sandbox) function and they should save and use that reference. This is handled by BasicBundleInstance base class that also provides getSandbox() function as a way of using the given sandbox. This enables scoping the bundle interactions that could be used for having two instances of for example the mapmodule on the same page/Oskari application.

Note that some bundles might use the Oskari global to get a sandbox reference (effectively defeating the purpose), but this is not intended use.

See Bundle API for details about messaging between bundles.

3.3 Bundles

A bundle can be considered as a building block in an Oskari application. A bundle provides some documented functionality and optionally an API that it offers to other bundles for interaction purposes. A bundle has a bundle id that is used as a name for the documention of the functionality and the API that the bundle provides, but can have multiple parallel implementations with the idea that switching between implementations is a drop-in replacement. An example of this could be a 2D (OpenLayers) and a 3D (Cesium) implementation of the map functionality (mapmodule). They both integrate with the rest of the application by providing and using the same documented API, but offer a different experience for the end-user.

The implementation details of a bundle should not really matter, but the API and the functionality they provide should be documented and either be backwards compatible or any changes need to be documented in the changelog. Notice that changes to the API can be very hard to manage for developers that use it to control embedded maps that are published from Oskari-based services that they themselves don't control. The API changes for the embedded maps at that exact time when the Oskari instance is updated with new version. It is good to keep in mind when designing API changes.

A comprehensive versioned documentation of all available bundles in oskari-frontend can be found here (generated from the api-folder).

3.3.1 Bundle implementation

Minimal implementation of a bundle is just having a factory function for the bundle that returns an instance of the bundle implementation:

import { BasicBundleInstance } from 'oskari-ui/BasicBundleInstance';

class MyBundleInstance extends BasicBundleInstance {
    start (sandbox) {
        super.start(sandbox);
        console.log('Hello world')
    }
}

Oskari.bundle('hello-world', () => new MyBundleInstance());

Here the Oskari.bundle() function is used to register the factory function for bundle id hello-world. You could save the code to oskari-frontend/bundles/sample/hello-world/index.js to have it accessible for any Oskari-based applications. In the path:

  • bundles is the folder that holds all the bundle implementations
  • sample would be a namespace for organizing/grouping bundles (you can skip this on application specific bundles)
  • hello-world should match the bundle id (just to make things easier to find, but is not a requirement)

To use this in an application you would import it to your applications main.js with:

import 'oskari-bundle!oskari-frontend/bundles/sample/hello-world';

Or you could save it application specific bundle as your-frontend-repo/anywhere_really/whatever.js and just update the reference for the import on your-frontend-repo/applications/myapp/main.js:

import 'oskari-bundle!../../anywhere_really/whatever.js';

After that the bundle with id hello-world can be started as part of your application. A good practice is to separate the bundle instance to its own file and import it on the index.js file so the factory file stays as clean and simple as it can.

Note! Making the bundle accessible in your application with import on main.js only means that the code is packaged as part of the javascript file that is loaded by the end-users browser. It doesn't mean it gets run/started on your application.

3.3.2 Starting a bundle as part of an application

To start a bundle as part of the application it needs to be included in the javascript file that is loaded by the end-users browser. This is done by importing it on the main.js file of the application.

To actually start an included bundle, there are two choices:

  1. have the bundle be referenced in the GetAppSetup response from the server (recommended)
  2. for testing you can start it in the developer console with Oskari.app.playBundle({ bundlename: 'hello-world' })

Applications usually have an index.js file that fetches the GetAppSetup response from the server with Oskari.app.loadAppSetup().

If you need to give the bundle a config and know when it has been started you can give the config as second parameter and a callback function as third:

Oskari.app.playBundle({
    "bundlename": "coordinatetool"
}, {
    conf: {
        someVariable: 'some value'
    }
},
() => {
    console.log('Bundle started');
});

You can also give the callback function as the second parameter to playBundle() function.

3.3.3 Bundle lifecycle

When a bundle is started by the framework:

1) the factory function is called to create an instance for the bundle.

2) after creating an instance, variables are injected into the instance object:

instance.mediator = {
    bundleId,
    instanceId
}

Where:

  • bundleId is the id you expect (hello-world in the example)
  • instanceId is something that can be used to differentiate multiple instances of the same bundle in a more complex application (defaults to bundle id).

Oskari.app.getConfiguration() has configuration for all of the bundles in the application and is usually populated from the GetAppSetup response from the server with Oskari.app.loadAppSetup(). This configuration has keys matching bundle instanceIds (defaults to bundle ids) with an object as value.

The configuration object can have conf and/or state keys and these are injected as-is to the instance object as variables. The values under conf usually have configurations that don't change at runtime (for map this could be for example the zoom levels or projection). The values under state have settings that can change at runtime (for map this could be the center coordinate and layers that are on the map). Bundles that use these have code that refer to this.conf or this.state for handling these and they are also documented in the bundle documentation.

3) The start() function is called by the framework when the bundle is started as a part of an application.

The start-function receives a reference to the Oskari sandbox as parameter and if you pass that to BasicBundleInstance base class with super.start(sandbox) it is accessible with this.getSandbox() in the other functions you might want to implement on your bundle.

4) The stop() function can be called by for example the publisher functionality

The bundle should do any cleanup in the stop-function like stop listening to events, unregister itself and any request handlers it has added etc.

3.3.4 Bundle architecture

There's a couple pf concepts that are used with bundles implemented in oskari-frontend that you can use when implementing any application specific bundles as well.

bundle.png

Handler

"Service" in the image above. It's the Handler's responsibility keep the state related to the bundle business logic consistent and updated. Possibly saving this state to the backend via action routes when needed. The Handler exposes public methods as Controllers to mutate the state and allows other components within the bundle to register for notifications about state changes.

Controller

Controller is created as a subset of functions from Handler and only has implicitly exposed functions from Handler that allows manipulate the state with new values.

View

It's the View's responsibility to update the user-interface DOM accordingly when it receives notification from the Handler that state has changed. View is passed the current state with a Controller and it's the View's responsibility to use the Controller to mutate state as a reaction to user input. Flyouts, Tiles, Popups etc. are parts of the View.

Map Plugin

It's the Map Plugin's responsibility to update the map related presentation when it receives notification from the Handler that state has changed. If the bundle does not have map related functionality, it doesn't need to implement a Map Plugin. Map layers, map controls, map interactions are implemented by Map Plugins.

Data flow

User input -> View/Plugin mutates state by calling Controller functions -> Controller as part of Handler updates internal state (possibly saving to backend) -> Handler notifies all interested components by triggering an event -> Listening components (Views & Map Plugins) update their presentation.

stateDiagram-v2
    direction LR
    [*] --> View: Initial state
    View --> Controller: User interaction
    Controller --> Handler: Manipulate state
    Handler --> View: State changed

Resources

Any additional CSS definitions or images the bundle needs are located under the bundle implementation resources folder. Any image links should be relative paths.

External dependencies

If your bundle depends on external library code, the libary must be imported to be included into the build as usual. You can use NPM modules with npm install --save and import as usual. But before adding dependencies, check that the library isn't already imported through oskari-frontend like OpenLayers, React, etc. to avoid duplication of library code and/or having multiple versions of the same library.

3.3.5 Bundle configuration

When bundles are started the configuration variables are injected on them if available before starting (see Bundle lifecycle above). Configurations are matched using the <bundle-identifier> and any properties defined are set as properties on the instances. Like the example below, mapfull instance will have conf and state properties available when it fires up:

{
   "mapfull": {
      "state": {
         "selectedLayers": [{
            "id": "base_35"
         }],
         "zoom": 1,
         "north": "6874042",
         "east": "517620"
      },
      "conf": {
         "globalMapAjaxUrl": "/ajax?",
         "plugins": [
            {
               "id": "Oskari.mapframework.bundle.mapmodule.plugin.LayersPlugin"
            },
            {
               "id": "Oskari.mapframework.mapmodule.WmsLayerPlugin"
            },
            {
               "id": "Oskari.mapframework.mapmodule.ControlsPlugin"
            },
            {
               "id": "Oskari.mapframework.bundle.mapmodule.plugin.ScaleBarPlugin"
            },
            {
               "id": "Oskari.mapframework.bundle.mapmodule.plugin.Portti2Zoombar"
            }
         ],
         "layers": [
            "...layers as JSON objects..."
         ],
         "imageLocation": "/Oskari/resources"
      }
   }
}

The conf and state properties are used throughout Oskari bundles to configuring and initializing the application to a specific. Their contents can be anything that the bundle requires. The conf property should be used to relay information about the runtime environment and state is used to set the bundles initial values for things that are likely to change at runtime.

3.4 Bundle implementation conventions

The folder structure follows a pattern where the first folder under the base bundles folder is a namespace folder. Application specific bundles can choose to skip the namespace especially if all bundles of the application would be under the same namespace. Oskari uses framework, mapping and admin namespace for most bundles. The next folder is named after the <bundle-identifier>.

Any events, requests (and request handlers) and services a bundle implements should be separated into subfolders under the bundles implementation. However this is a convention, not a requirement for application specific bundles. In addition if you have divided the code into views or components that are shown on the flyout, you can create subfolders for them as well. Having an index.js file as the starting point for bundle definition is a nice way of shortening the reference to the bundle in applications main.js file.

There is a functional requirement when using the oskari-bundle loader to load your bundle that localization files should be under resources/locale folder relative to the index.js (bundle definition/instance factory file). Usually the files are named after the language code for the localization, but this is not a functional requirement. The contents of the file declares the locale for localization.

${your root dir}
   |--bundles
      |--${namespace}
         |--${bundle-identifier}
            |--component
            |  |--MyComponent.js
            |--event
            |  |--MyEvent.js
            |--request
            |  |--MyRequest.js
            |  |--MyRequestHandler.js
            |--resources
            |  |--locale
            |    |--en.js
            |    |--fi.js
            |    |--sv.js
            |--service
            |  |--MyService.js
            |--view
            |  |--MyLoggedInView.js
            |  |--MyGuestView.js
            |--index.js
            |--instance.js
            |--Flyout.js

3.5 Bundle API

Bundles can interact with each other through the API that bundles themselves provide. These are separated into three categories:

  • request for requesting something to be done (serializable to JSON)
  • event for notifying that something happened (serializable to JSON)
  • service when direct function calls are required between bundles

The Oskari sandbox is the message bus for all of these categories:

sequenceDiagram
    participant mapmodule as mapmodule
bundle participant layerlist as layerlist
bundle mapmodule->>Sandbox: Add handler for AddMapLayerRequest layerlist->>Sandbox: Add listener for AfterMapLayerAddEvent Note over layerlist: User adds a layer to map using
UI provided by layerlist layerlist->>layerlist: Click toggle layerlist->>Sandbox: Send AddMapLayerRequest Sandbox->>mapmodule: handle AddMapLayerRequest mapmodule->>mapmodule: Download layer metadata
and add to map mapmodule->>Sandbox: Trigger AfterMapLayerAddEvent Sandbox->>layerlist: call listener for AfterMapLayerAddEvent layerlist->>layerlist: update state/UI to show layer on map

3.5.1 Bundle requests

Requests can only have one handler/bundle that handles them. Other bundles request something to be done by the bundle that offers a request API.

A comprehensive versioned documentation of all available bundle requests can be found here

Providing request API

A bundle can define a request that it wants to provide as its API with a javascript file like this:

Oskari.clazz.define('Oskari.sample.HelloRequest',
    function (target = 'World') {
        this.target = target;
    }, {
        __name: 'HelloRequest',
        getName: function () {
            return this.__name;
        },
        getTarget: function () {
            return this.target;
        }
    }, {
        protocol: ['Oskari.mapframework.request.Request']
    });

Where:

  • name is the request name and references to the request are done using the name
  • the constructor parameters are values that the handling code needs to do what the request is supposed to do. They can be partially or even fully optional.
  • the protocol at the end is required for Oskari to find any of the request files by just using the request name

By convention the request file should be placed in a request-folder under the bundles implementation folder. The bundle should then also register a handler for the request in the sandbox. If the request handler is complex you should have the code in a separate file in the request-folder, but simple ones can be done in the bundle instance file for example.

Note! The request file needs to be imported as part of the bundle so they are included in the build. Even as they usually are not directly referenced by the importing code:

import './request/HelloRequest';

Registering the handler is done like this when extending the BasicBundleInstance class:

this.addRequestHandler('{the request name}', (req) => /* use request methods to get parameters and do what was requested */);

You can also use sandbox functions directly:

sandbox.requestHandler('{the request name}', (req) => handlerFn(req));

Remember that you should cleanup the handler in stop() and you need the request name to do that. So you may want to store which handlers are registered so you can restart after stopping etc. The BasicBundleInstance class handles this automatically.

Removing a handler is done like this when extending the BasicBundleInstance class:

this.removeRequestHandler('{the request name}');

You can also use sandbox functions directly:

sandbox.requestHandler('{the request name}', null);

Using the request API

When you need to do something that another bundle already provides an API for, you can use the request API to execute that functionality through the Oskari sandbox:

sandbox.postRequestByName('{the request name}', ['{request contructor params}', '{as an array}']);

If you want to check if anything in the current application is listening to the request you can do this:

if (!sandbox.hasHandler('{the request name}')) {
    // nothing is listening to the request I want to use
    // do error handling or try again later
} else {
    const theRequestingComponent = this;
    const requestBuilder = Oskari.requestBuilder('{the request name}');
    sandbox.request(theRequestingComponent, requestBuilder('{request contructor params}', '{as values}'));
}

The request handler might be registered later on at application startup or missing completely from the application and you might need to deal with this as error handling. The example also shows another way of sending requests, but you can use the postRequestByName() instead of requestBuilder as its easier to work with.

3.5.2 Bundle events

Events can have multiple listeners like you would expect. Bundles that offer an event API expect other bundles to be interested about the things they do and can notify other bundles when that something has happend.

A comprehensive versioned documentation of all available bundle events can be found here

Providing event API

A bundle can define an event that it wants to provide as its API with a javascript file like this:

Oskari.clazz.define('Oskari.sample.HelloEvent',
    function (target = 'World') {
        this.target = target;
    }, {
        __name: 'HelloEvent',
        getName: function () {
            return this.__name;
        },
        getTarget: function () {
            return this.target;
        }
    }, {
        protocol: ['Oskari.mapframework.event.Event']
    });

Where:

  • name is the event name and references to the event are done using the name
  • the constructor parameters are values that the listening components might want to know about the change. They can be partially or even fully optional.
  • the protocol at the end is required for Oskari to find any of the event files by just using the event name

By convention the event file should be placed in a event-folder under the bundles implementation folder.

Note! The event file needs to be imported as part of the bundle so they are included in the build. Even as they usually are not directly referenced by the importing code:

import './event/HelloEvent';

Sending an event is done like this:

const eventTemplate = Oskari.eventBuilder('HelloEvent');
sandbox.notifyAll(eventTemplate('{event contructor params}', '{as values}')

Listening to events

Other bundles can listen and react to things happening on the application. When extending the BasicBundleInstance class listening to events can be done like this:

this.on('HelloEvent', (evt) => {
    console.log(`Hello ${evt.getTarget()}`);
});

Note! Adding a event listener for the same event name in BasicBundleInstance class currently overwrites the previous listener.

On many cases in oskari-frontend the event listening is much more complex and BasicBundleInstance class tries to hide this complexity, but it's good to know how it works behind the scenes. Listening to events requires a bundle to register itself into the sandbox with:

sandbox.register(this);

The this in the example is an object with functions init() and getName(). In addition for listening events a onEvent(event) function is required. Usually this is the bundle instance itself. The init() function is called by the sandboxes register() function itself.

After registering to sandbox, the object/instance can start listening to events with:

sandbox.registerForEventByName(this, '{event name to listen}');

After this when an event is triggered the objects/instances onEvent() function is called with the event as parameter.

onEvent (event) {
    const handler = this._eventListeners?.listener(event.getName());
    if (!handler) {
        return;
    }
    return handler(event);
}

It's common in oskari-frontend that references to eventListeners are stored in an object like _eventListeners as instance variable so it's easy to loop through when we need to cleanup any listeners and have a boilerplaty onEvent() implementation like above.

Remember that you should cleanup the event listeners in stop() and you need the event name to do that. So you may want to store which events are being listened to so you can restart after stopping etc. The BasicBundleInstance class handles this automatically.

sandbox.unregisterFromEventByName(this, '{event name to stop listening}');

3.5.3 Bundle services

Most communication between bundles should happen through the request/event API, but it's not always possible. When you can't serialize the messages to JSON or just need direct function API you should use services for that functionality for the bundle. Bundles can expose a service that is registered to the sandbox to let other bundles get a reference to the service and call the service functions. This is the recommended way of coupling bundles when a direct function call is required.

Another way of doing this is getting a reference to a bundle instance through sandbox.findRegisteredModuleInstance(instanceName) and that is fairly common in oskari-frontend as well, but a better approach would be to use the services. With services we can expose a reasonable set of functions required for things to work without exposing the whole bundle implementations. This leads to better decoupling and less accidental bugs as the bundle internals can be updated and changed without worrying if other bundles call their functions directly.

Services should have similar API documentation as requests and events. Unforturnately they don't at the moment, but services is the one API we could document and try to have backwards-compatible through versions and/or document any changes to the API changelog. Doing the same for any internals of all the bundles is basically impossible as it would grind every update to a halt.

So whenever you would want to call a function in a bundle directly, you should seriously consider making a pull request instead to add/modify a service to expose that function instead. Otherwise direct function calls (or even worse, referencing an internal variable from another bundle) are very fragile and fiddly to work with in regards of maintaining an application. It probably works now, but is very easily broken in a version update and the bug could be hard to find at that time.

Providing a service API

Providing a service through sandbox for other bundles to use is a simple registration of the service to sandbox like this:

const myService = {
    functionForOtherBundle: () => /* do stuff */,
    getQName: () => '{service name}'
};
sandbox.registerService(myService);

Usually the service is some kind of class instance instead of a simple object, but the only real requirement that it has to implement a getQName() function that returns a string as an identifier for the service.

As with other things, bundles should clean up on stop() and that can be done with:

sandbox.unregisterService('{service name}');

Using service API

Using a service provided by a bundle is done by fetching a reference to the service through sandbox and calling functions provided by the service:

const serviceRef = sandbox.getService('{service name}');
serviceRef.functionForOtherBundle();

3.5.4 How to use Oskari classes (deprecated)

A lot of older code in Oskari is still defined as custom classes. Newer code uses ES classes instead. Defining a class is a simple call to Oskari.clazz.define() method:

Oskari.clazz.define('Oskari.mynamespace.MyClass',
function(pState) {
    this._state = this.states[pState || 'open'];
}, {
    /**
     * @property states
     */
    states : {
        'open' : 1,
        'close' : 2
    },
    getState : function() {
        return this._state;
    }
},
{
    // a list of protocols this class implements
    "protocol" : []
});

The define method takes parameters that it uses to produce a class prototype:

/**
 * Defines a class implementation in Oskari framework
 *
 * @method define
 * @param {String} name fully qualified class name
 * @param {Function} contructor the method that will be called on create that should initialize any class properties.
 *        It can also have parameters that can be passed when creating an instance of the class
 * @param {Object} implementation JavaScript object defining the methods and 'static' properties of the class
 * @param {Object} metadata (optional) class metadata which can for example declare which protocols/interfaces the class implements
 */

Once a class has been defined, it can be created by calling Oskari.clazz.create() method and it's methods can be called:

var openState = Oskari.clazz.create('Oskari.mynamespace.MyClass'),
    closeState = Oskari.clazz.create('Oskari.mynamespace.MyClass', 'close');

alert('MyClass open state is ' + openState.getState());
alert('MyClass close state is ' + closeState.getState());

The create method returns an instance of the class which you specify as parameter:

/**
 * Creates an instance of Oskari framework class
 *
 * @method create
 * @param {String} name fully qualified class name
 * @param {Object} params (optional 0-n) parameters that are passed to the constructor. All the parameters after the class name will be passed.
 * @return {Object} instantiated class prototype
 */

Protocol

Classes can declare implementing a protocol (none to many) or classes can also be used to define a protocol. A protocol can be thought of as an interface declaration and contract for a set of functions that a class must provide/implement. For example for a class to register to Oskari sandbox it needs to implement the Oskari.mapframework.module.Module protocol which defines that the class must have for example a getName() and onEvent() methods. This way we can depend that any registered component can handle operations that will be expected from registered components.

Oskari.clazz.define('Oskari.mynamespace.MyProtocol', function() {}, {
  "myProtocolMethod": function(arg) {}
});

Defining a protocol class is not really used for anything other than explaining what methods an implementation should offer. Oskari class system provides functions to query classes that implement a protocol:

var classesImplementingProtocol = Oskari.clazz.protocol("Oskari.mynamespace.MyProtocol");

Category

Oskari Clazz system supports splitting Class implementations in multiple compilation units (files) by a Category concept. This means you can add functions to an existing class to "extend" it. When loading asynchronously a stub class is defined if class definition is not yet loaded.

Oskari.clazz.category('Oskari.mynamespace.MyClass', 'my-set-of-methods', {
  "myOtherMethod" : function() {
    return "one";
  },
  "myAnotherMethod" : function() {
    return "two";
  }
});

When the category() method call has completed, your class will have the new methods declared in the category call and they can be called as any original method in the class prototype:

var openState = Oskari.clazz.create('Oskari.mynamespace.MyClass');
openState.myOtherMethod();

The category method takes parameters that it uses to extend a class prototype:

/**
 * Appends a class implementation in Oskari framework
 *
 * @method category
 * @param {String} name fully qualified class name
 * @param {String} categoryName identifier for the set of methods.
 * @param {Object} implementation JavaScript object defining new methods and 'static' properties for the class
 */