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.
- Main principle
- Google style guide
- Property renaming
- Property renaming and directives
- Directive name or attributes names in the DOM
- API documentation
- Custom properties
- Service typing
- Limit the use of ng-controller
- Use the controller-as syntax
- Use @export for controllers
- Templating
- Directive scoping
- Avoid two-way bindings when not needed
- Authoring examples
- Usage of the closure-library
- Watch your watchers!
- Styling with less
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.
We more or less follow the AngularJS Style Guide for Closure Users at Google.
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';
// …
};
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.
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 bengeo
or a contribs prefix.platform
is optional and can be for instancemobile
ordesktop
.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'
},
// …
};
};
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);
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');
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');
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.
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.
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!
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
, 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
.
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
forngeo
directives.contribs/gmf/src/directives/partials
forgmf
directives.
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.
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 ofngeo
directives. - one for
gmf
that will contain both all template files fromngeo
andgmf
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.
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
, 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
.
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.
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.
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.
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);
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.
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, in both ngeo and gmf, follow a set of rules that determines their value. A CSS class name:
- always begins with the
ngeo-
orgmf-
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
, likegmf-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.