Skip to content

Latest commit

 

History

History
875 lines (691 loc) · 26.1 KB

guidelines.md

File metadata and controls

875 lines (691 loc) · 26.1 KB

Development guidelines

The purpose of this guideline is to help the developper to contribute in the best way to ngeo and gmf cores. It will describe the golbal philosophy of ngeo design, and set base rules to apply when you want to add a new feature.

Table of content

Main principle

Before starting to code a new feature in gmf, you must determine if this feature is 100% gmf specific, or if the feature is generic and could be added to ngeo core. You also need to check in ngeo core and examples if you don't find anything that could fit with your needs.

The main principle is to put everything you can in ngeo, and put in gmf only what is specific. When you develop into gmf contribs, you must consider that you are developing a client application, and try your best to extract from your code all things that could go into ngeo, and be shared with other projects. You must not consider that gmf is a real part of ngeo, and that there is no real importance to put your stuff into ngeo or gmf cores, it does. This point is essential to be sure ngeo is going in the good direction: maintainable, reusable, evolving.

In ngeo, we want to have very generic code that will be shared between gmf and other web map applications. When you add some code in ngeo, you need to follow some rules that helps the code to be easly usable and customisable.

Google style guide

We more or less follow the AngularJS Style Guide for Closure Users at Google.

Property renaming

The ngeo code is compiled with Closure Compiler in advanced mode. This means we should conform to the restrictions imposed by the compiler.

In particular, Angular controllers and directives typically set properties on the controller instance (this) or on the $scope. These properties are then referenced by their names in HTML pages and templates. So it is required to prevent the compiler from renaming these properties.

The way to do that is to add the @export tag when declaring a variable; this will tell the compiler to not rename the variable. For example if you need to set a property foo on the controller instance you should do as follows:

/**
 * @constructor
 * @ngInject
 */
app.MainController = function() {

  /**
   * @type {string}
   * @export
   */
  this.foo = 'bar';
  // …
};

Property renaming and directives

In the definition of a directive, if an object is used for the scope property (isolate scope) then quotes must be used for the keys in that object. And in the link function, the [] notation, instead of the . notation, must be used when accessing these properties. See the example below.

ngeo.exampleDirective = function() {
  return {
    restrict: 'A',
    scope: {
      map: '=ngeoExampleMap'
    }
    controller: function() {
      let m = this['map'];
      // Then, for Closure-Compiler, assert and type this value.
      // …
    },
    controllerAs: 'ctrl',
    // …
  });

But if you bind your directive to the controller, you must use the . notation. See the example below :

ngeo.exampleDirective = function() {
  return {
    restrict: 'A',
    scope: {
      map: '=ngeoExampleMap'
    }
    bindToController: true,
    controller: function() {
      /**
       * This value will always be the value of 'ngeoExampleMap' and vice versa.
       * The '@type …' and the '@export' below are only for Closure-Compiler.
       * @type {ol.Map}
       * @export
       */
      this.map;
      // …
    },
    controllerAs: 'ctrl',
    // …
  });

Note also that if you use the & binding, you will still get a function instead of your ol.Map but directly in the scope of your controller.

Directive name or attributes names in the DOM

The naming of a directive or the name of a directive attribute as represented in the DOM must follow this rule: base-(platform-)directive(-attribute)

Where:

  • Base can be ngeo or a contribs prefix.
  • platform is optional and can be for instance mobile or desktop.
  • directive is the name of the directive.
  • attribute is the name of your attribute.

So, for a ngeo my example directive, exclusif for mobile and with, for example, the existing attribute title, you will have a DOM like this:

<ngeo-mobile-myexample
  ngeo-mobile-myexample-title="hello">
<ngeo-mobile-myexample>

And in the directive, a scope like this:

ngeo.mobileMyexampleDirective = function() {
  return {
    scope: {
      t: '=ngeoMobileMyexampleTitle'
    },
    // …
  };
};

API documentation

ngeo uses the Angular-JSDoc plugin in addition to JSDoc to create the API documentation.

This plugin provides the @ngdoc <type> and @ngname <name> tags. @ngdoc is used to define the Angular type (directive, service, controller or filter) and @ngname defines the name used to register this component.

For directives the used HTML attributes are declared with @htmlAttribute {<type>} <name> <description>..

The used metadata should be documented.

The usage of a directive should be shown with an example.

For example:

/**
 * Description.
 *
 * Example:
 *
 *      <example />
 *
 * Used UI metadata:
 *
 *  * theMetadata: Description.
 *
 * @return {angular.Directive} The directive specs.
 * @htmlAttribute {ol.Map} ngeo-control-map The map.
 * @ngInject
 * @ngdoc directive
 * @ngname ngeoControl
 */
ngeo.controlDirective = function() {
  // …
};
ngeo.module.directive('ngeoControl', ngeo.controlDirective);

Custom ol.Object properties

OpenLayers 3 allows passing custom properties to classes inheriting from ol.Object. For example:

let layer = new ol.layer.Tile({
  maxResolution: 5000,
  title: 'A title',
  source: new ol.source.OSM()
});

title is the custom property in this example. (While maxResolution is an ol3 built-in layer property.)

You can then use the get methods to get that property's value:

let layerTitle = layer.get('title');

But this won't work in the case of the ngeo, or any code compiled in with Closure Compiler in ADVANCED mode. The compiler is indeed going to rename the key title in the options object passed to the ol.layer.Tile constructor.

One option to work-around the issue involves using the set method after the construction of the layer:

let layer = new ol.layer.Tile({
  maxResolution: 5000,
  source: new ol.source.OSM()
});
// use `set` to set custom layer properties
layer.set('title', 'A title');

Service typing

ngeo defines Angular services. They're located in the src/services directory. Angular services may be of any type : objects, functions, etc.

For each type we define in ngeo we must provide a type. This allows having a type for @param when using (injecting) an ngeo service in a function.

If the service is a function the type will be defined using a @typedef. For example:

/**
 * @typedef {function(ol.layer.Layer)}
 */
ngeo.DecorateLayer;

If the service is an object a @constructor must be defined. For example:

/**
 * The ngeo Permalink type.
 * @constructor
 * @param {!ngeo.Location} ngeoLocation Location.
 * @param {!History} history History.
 */
ngeo.Permalink = function(ngeoLocation, history) {
  /**
   * @type {!History}
   * @private
   */
  this.history_ = history;

  /**
   * @type {!ngeo.Location}
   * @private
   */
  this.uri_ = ngeoLocation;
};

And in both cases a goog.provide must be added for the type. For example:

goog.provide('ngeo.DecorateLayer');
goog.provide('ngeo.Location');

Limit the use of ng-controller

Use one ng-controller only, the "main controller", somewhere at the root of the DOM tree, at the <html> element for example.

And instead of ng-controller instances use application-specific directives, and store the directive-specific data in the directive itself. For that, use a directive controller, with controller and bindToController.

The permalink example shows how to create an application-specific map directive wrapping the ngeo-map directive.

The "main controller" is where we create the application's map instance, which we store in the controller itself (this) or in the controller's scope ($scope).

See this blog post for explanations on why using many ng-controller instances may cause trouble.

Use the controller as syntax

When ng-controller is used in the HTML code it is recommended to use the controller as syntax. For example:

<body ng-controller="MainController as ctrl">
  …
  …
  <button ngeo-btn class="btn btn-success" ng-model="ctrl.drawPoint.active">Point</button>

In this way it is clear by reading the HTML that the drawPoint interaction is defined in the MainController.

See this blog post for more details.

Use @export for controllers

So following the angularjs-google-style controllers are written as classes, and controller functions should be defined on the constructor prototype.

Here is an example:

/**
 * Application main controller.
 *
 * @param {!angular.Scope} $scope Scope.
 * @constructor
 * @ngInject
 * @export
 */
app.MainController = function($scope) {

  /**
   * @type {string}
   * @export
   */
  this.title = 'Addition';

  // …
};


/**
 * @param {number} a
 * @param {number} b
 * @return {number} Result.
 * @export
 */
app.MainController.prototype.add = function(a, b) {
   return a + b;
};


angular.module('MainController', app.MainController);

And this is the template:

<div ng-controller="MainController as ctrl">
  <h2>{{ctrl.title}}</h2>
  <span>{{ctrl.add(2, 3)}}</span>
</div>

For this to work the title and add properties must exist on the controller object. This is why the app.MainController constructor, the add method, and the title property, are annotated with @export. The @export annotation tells Closure Compiler to generate exports in the build for the annotated functions, and to not rename the annotated properties.

Note that the compiler flags

"--generate_exports"
"--export_local_property_definitions"

are required for the Compiler to actually take the @export annotations into account.

And remember to @export the constructor as well! If you just export the method (add here) this is the code the compiler will generate for the export:

t("app.MainController.prototype.add",cw.prototype.d)

t is actually goog.exportSymbol here (renamed). What this function does is basically the following:

app = {};
app.MainController = {};
app.MainController.prototype = {};
app.MainController.prototype.add = cw.prototype.d;

which does not help. If the constructor is @export'ed as well this is what the compiler will generate:

t("app.MainController",cw);
cw.prototype.add=cw.prototype.d;

which looks much better!

Templating

In ngeo avoid template when not needed.

For example, if you want to create a new directive for a button that will have an action on click. The bad way to write it in ngeo:

/**
 * @return {angular.Directive} Directive Definition Object.
 */
ngeo.foobarDirective = function() {
  return {
    restricted: 'E',
    template: '<button ng-click="ctrl.doAction()" />',
    controllerAs: 'ctrl',
    ....
  }
};

/**
 * @constructor
 * @ngInject
 * @export
 */
ngeo.NgeoFoobarController = function() {
  this.doAction = function() {console.log('Action');}
};

Now the right approach:

/**
 * @return {angular.Directive} Directive Definition Object.
 */
ngeo.foobarDirective = function() {
  return {
    restricted: 'A',
    ....
  }
};

You can then bind the click in from the controller of your directive :

/**
 * @constructor
 * @param {angular.JQLite} $element Element.
 * @ngInject
 * @export
 */
ngeo.NgeoFoobarController = function($element) {
  $element.on('click', function() {console.log('Action');});
};

Or from the link function of your directive:

link:
  /**
   * @param {!angular.Scope} scope Scope.
   * @param {angular.JQLite} element Element.
   * @param {angular.Attributes} attrs Attributes.
   * @param {!Array.<!Object>} ctrls Controllers.
   */
  function(scope, element, attrs, ctrls) {
    element.on('click', function() {console.log('Action');});
};

In the second case, the directive has no template and is restricted to the attribute declaration. It will just add custom behavior to the HTML element it's attached to. Try to create directive in this perspective the more you can when you are in ngeo.

This example of the <button> tag could be extended to the use of <select> <options> <a href=""> or any other HTML tags.

In gmf you can use specific templates

In gmf, if you are sure that all the UIs will use the exact same HTML view, you can add templates to your directives, even small templates that just define a button.

Generally, if your widget could be in ngeo, you have to create a new directive with no template in ngeo, then to avoid to have too much HTML in the main gmf view, you can create a new directive in gmf on top of the ngeo one, that will just define a template including the ngeo directive.

For example, the gmf directive gmf-layertree will declare a template that will include the ngeo-layertree directive.

/**
 * @return {angular.Directive} The directive Definition Object.
 * @ngInject
 */
gmf.layertreeComponent = function() {
  return {
    ...
    template:
        '<div ngeo-layertree="gmfLayertreeCtrl.tree" ' +
        'ngeo-layertree-map="gmfLayertreeCtrl.map" ' +
        'ngeo-layertree-nodelayer="gmfLayertreeCtrl.getLayer(node)" ' +
        'ngeo-layertree-templateurl="' + gmf.layertreeTemplateUrl + '"' +
        '<div>'
  };

In general, when creating a new directive in gmf, you must rely as much as possible on ngeo core directives. For example in the layertree, it would make no sense to create a new directive from scratch, you must rely on ngeo layer tree.

Taking care of this point will help you to create more generic directives in ngeo.

template vs templateUrl

The only technical reason to use templateUrl instead of template is that the template doesn't support i18n.

It's up to you to determine if the template is simple enough to be written inline in the directive code with the template attribute.

If the template looks too complex, please put it in an extern file and use the templateUrl attribute to point on it. In that case, the path of the template file should follow the following rule:

All directives templates must be stored in the sub partials/ folder of the directive folder:

  • src/directives/partials for ngeo directives.
  • contribs/gmf/src/directives/partials for gmf directives.

Template URL

First of all the partials should be in the folder src/directives/partials.

When we use a template URL it should be overwritten by an attribute.

For that we should use this kind of code:

ngeo.module.value('ngeo<Name>TemplateUrl',
    /**
     * @param {angular.JQLite} element Element.
     * @param {angular.Attributes} attrs Attributes.
     */
    function(element, attrs) {
      let templateUrl = attrs['ngeo<Name>Templateurl'];
      return templateUrl !== undefined ? templateUrl :
          ngeo.baseTemplateUrl + '/<name>.html';
    });

ngeo.<name>Directive = function(ngeo<Name>TemplateUrl) {
    return {
        templateUrl: ngeo<Name>TemplateUrl,
        ...

Where <Name> is the directive name in title case and <name> in lower case.

It can be adapted for contrib/gmf by replacing ngeo by gmf.

Note that the default template of a ngeo directive can be overloaded in 2 ways:

  • Send the template url via dom attributes. This will overload the template for the instance of the directive that is defined in the HTML.
  • Overload the angular value ngeo<Name>TemplateUrl. This will have effect on all instances of the directive.

Template caching

External directive template files are resolved by angular with a http request to the path defined by the templateUrl attribute. In avdanced build mode, we don't want the application to call http requests to resolve templates, so we store them in the templateCache. Watch AngularJs doc.

Two template caches are generated during build through two different targets:

  • one for ngeo that will contain all template files of ngeo directives.
  • one for gmf that will contain both all template files from ngeo and gmf directives.

Those generated files are attached to the build of ngeo and gmf distribs.

For this to work in any case (examples, applications, built or not), just refer the templateUrl as a relative path to the directive. The definition of the variable ngeo.baseTemplateUrl and gmf.baseTemplateUrl will resolve, depending on the case, the correct paths.

Directive scoping

In ngeo, prefer non-isolate scopes

When creating a "widget" directive (i.e. directive with templates/partials) it is usually recommended to use an isolate scope for the directive.

In the case of ngeo we want to be able to override directive templates at the application level. And when overriding a directive's template one expects to be able to use properties of an application-defined scope. This is not possible if the template is processed in the context of an isolate scope.

So this is what ngeo "widget" directives should look like:

/**
 * @return {angular.Directive} Directive Definition Object.
 */
ngeo.foobarDirective = function() {
  return {
    restrict: 'A',
    scope: true,
    templateUrl: 
    // …
  };
};

We still use scope: true so that a new, non-isolate, scope is created for the directive. In this way the directive will write to its own scope when adding new properties to the scope passed to link or to the directive's controller.

Note that even with a scope: true definition, a directive can take benefit of the bindToController and controllerAs declarations. Even with a non-isolate scope, you can bind attributes variable to the directive controller (as the isolate scope does). For this, you need to use the bindToController as an object for mapping definitions:

/**
 * @return {angular.Directive} Directive Definition Object.
 */
ngeo.foobarDirective = function() {
  return {
    restrict: 'A',
    scope: true,
    bindToController: {
     prop1: '='
    },
    controllerAs: 'myCtrl',
    templateUrl: 
    // …
  };
};

Here the prop1 property of the parent scope will be bound to the prop1 property of the directive controller.

In gmf isolate scopes are more appropriated

In gmf, you are pretty sure of what template you want to bind to your directive. Regarding this point, you are not under the constraint not to use an isolate scope.

Avoid two-way bindings when not needed

In templates

In angularJs, $scope values are mapped to HTML view through expressions. Add :: at the beginning of the expression to mark it as a single evaluated expression. Once the expression is evaluated and resolved, the watchers are removed and the expression won't be evaluated again.

See AnguarJs doc.

Through directive scopes

Be careful when you use isolate scope and bindToController objects to pass variable through scope inheritance.

/**
 * @return {angular.Directive} Directive Definition Object.
 */
ngeo.foobarDirective = function() {
  return {
    scope: {
      foo: '='
    }

A declaration like the one above with the symbol '=' will create an isolate scope for the directive and will create a two-way data bindings between the isolate scope foo property and the $parent scope property whose name is given in foo HTML attribute.

It's important to note that they don't share the same reference, but both are watched and updated concurrently. AngularJs adds $watchers each time you have a two-way bindings pattern in your application. As mentionned before, this should be avoided when not needed.

Here the way to get a one time binding when using scope or bindToController as an object:

/**
 * @return {angular.Directive} Directive Definition Object.
 */
ngeo.foobarDirective = function() {
  return {
    scope: {
      fooFn: '&'
    }
    // …
};

/**
 * @constructor
 * @param {angular.Scope} $scope The directive's scope.
 * @ngInject
 * @export
 */
ngeo.NgeoFoobarController = function($scope) {
  let foo = $scope['fooFn']();
};

In this example we tell Angular to create a function fooFn that evaluates the expression in the context of the parent/user scope. There is no binding, just an expression, and we get the foo variable only once.

Note:

  • if you need consistency, of course use the '=' symbol.
  • if you need a one time binding to a string, use the '&' symbol.

Authoring examples

A number of constraints must be respected when developing examples for ngeo.

As described above the gh-pages make target can be used to publish the examples to github.io. The examples published on github.io are not compiled, they rely on the standalone ngeo.js build.

Because of that the examples cannot rely on non-exported symbols or properties. For example they cannot use goog objects and they cannot use ol objects that are not part of the OpenLayers API (that is marked with @api in the OpenLayers source code).

There's one exception to the rule: the examples (must) use goog.provide and goog.require. This is necessary for running the examples in development mode and for compiling them (see below). The goog.provide and goog.require statements are removed before publication of the examples on github.io.

Even though the examples are not compiled on github.io the check target does compile them, as a verification step. This means that the examples must use compiler annotations and respect the constraints imposed by the compiler.

Declaring an event

When you declare an event on ol3 object, please use

  • the ol.events.listen function
  • the ol3 constant to identify the event

This is wrong:

this.geolocation_.on('change:accuracyGeometry', function() {
  ...
});

This is the correct syntax:

ol.events.listen(this.geolocation_,
  ol.Object.getChangeEventType(ol.GeolocationProperty.ACCURACY_GEOMETRY),
  function() {
    ...
  }, this);

Watch your watchers!

Angular runs something called a digest cycle. This digest cycle is a loop through the application's bindings (or watchers) that checks if values have changed. The more watchers the slower the digest cycle!

Angular 1.3 has introduced the concept of one-time binding. With one-time binding the data is rendered once and then persisted and not affected by future updates to the model.

This is the one-binding syntax:

<h1>{{ ::title }}</h1>

It is important to use one-time binding when possible!

There are other techniques to reduce the number of watchers in Angular applications. This blog post provides a very good overview.

ngeo includes a JavaScript script that can be used to watch the number of watchers in an Angular application. Look at this file to know how to use it.

Styling with less

To be able to do calculations directly with less we encourage to use a subset of the CSS units. We choose units that don't depend on parent tags and are relative.

  • rem: 1 rem is the font size of the root element ().
  • vw: 1 vw is 1/100th of the width of the viewport.
  • vh: 1 vh is 1/100th of the height of the viewport.

CSS class names convention

CSS class names, in both ngeo and gmf, follow a set of rules that determines their value. A CSS class name:

  • always begins with the ngeo- or gmf- prefix depending on its origin
  • always begins with the name of the component in which it is defined, for example in a layer tree directive in gmf, a name starts with gmf-layertree, like gmf-layertree-name, gmf-layertree-node, gmf-layertree (for the main <div>), etc.

In the gmf applications, CSS class names should begins with gmf-app.

In directive html templates, you should avoid using ìd with a combination of document.getElementById. Instead, define a unique CSS class name, inject the $element service in your controller and use Angular jqLite selector to get the element needed.