summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/FE_Style_Guide.md
blob: a636119c8e6a920e9901d2c5afc6e7af0023472d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# Gerrit JavaScript style guide

Gerrit frontend follows [recommended eslint rules](https://eslint.org/docs/rules/)
and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html).
Eslint is used to automate rules checking where possible. You can find exact eslint rules
[here](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/.eslintrc.js).

Gerrit JavaScript code uses ES6 modules and doesn't use goog.module files.

Additionally to the rules above, Gerrit frontend uses the following rules (some of them have automated checks,
some don't):

- [Prefer null over undefined](#prefer-null)
- [Use destructuring imports only](#destructuring-imports-only)
- [Use classes and services for storing and manipulating global state](#services-for-global-state)
- [Pass required services in the constructor for plain classes](#pass-dependencies-in-constructor)
- [Assign required services in a HTML/Polymer element constructor](#assign-dependencies-in-html-element-constructor)

## <a name="prefer-undefined"></a>Prefer `undefined` over `null`

It is more confusing than helpful to work with both `null` and `undefined`. We prefer to only use `undefined` in
our code base. Try to avoid `null`.

Some browser and library APIs are using `null`, so we cannot remove `null` completely from our code base. But even
then try to convert return values and leak as few `nulls` as possible.

## <a name="destructuring-imports-only"></a>Use destructuring imports only
Always use destructuring import statement and specify all required names explicitly (e.g. `import {a,b,c} from '...'`)
where possible.

**Note:** Destructuring imports are not always possible with 3rd-party libraries, because a 3rd-party library
can expose a class/function/const/etc... as a default export. In this situation you can use default import, but please
keep consistent naming across the whole gerrit project. The best way to keep consistency is to search across our
codebase for the same import. If you find an exact match - always use the same name for your import. If you can't
find exact matches - find a similar import and assign appropriate/similar name for your default import. Usually the
name should include a library name and part of the file path.

You can read more about different type of imports
[here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import).

**Good:**
```Javascript
// Import from the module in the same project.
import {getDisplayName, getAccount} from './user-utils.js'

// The following default import is allowed only for 3rd-party libraries.
// Please ensure, that all imports have the same name accross gerrit project (downloadImage in this example)
import downloadImage from 'third-party-library/images/download.js'
```

**Bad:**
```Javascript
import * as userUtils from './user-utils.js'
```

## <a name="services-for-global-state"></a>Use classes and services for storing and manipulating global state

You must use classes and services to share global state across the gerrit frontend code. Do not put a state at the
top level of a module.

It is not easy to define precise what can be a shared global state and what is not. Below are some
examples of what can treated as a shared global state:

* Information about enabled experiments
* Information about current user
* Information about current change

**Note:**

Service name must ends with a `Service` suffix.

To share global state across modules in the project, do the following:
- put the state in a class
- add a new service to the
[appContext](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/services/app-context.js)
- add a service initialization code to the
[services/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/services/app-context-init.js) file.
- add a service or service-mock initialization code to the
[embed/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/embed/app-context-init.js) file.
- recommended: add a separate service-mock for testing. Do not use the same mock for testing and for
the shared gr-diff (i.e. in the `services/app-context-init.js`). Even if the mocks are simple and looks
identically, keep them separate. It allows to change them independently in the future.

Also see the example below if a service depends on another services.

**Note 1:** Be carefull with the shared gr-diff element. If a service is not required for the shared gr-diff,
the safest option is to provide a mock for this service in the embed/app-context-init.js file. In exceptional
cases you can keep the service uninitialized in
[embed/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/embed/app-context-init.js) file
, but it is recommended to write a comment why mocking is not possible. In the future we can
review/update rules regarding the shared gr-diff element.

**Good:**
```Javascript
export class CounterService {
    constructor() {
        this._count = 0;
    }
    get count() {
        return this._count;
    }
    inc() {
        this._count++;
    }
}

// app-context.js
export const appContext = {
    //...
    mouseClickCounterService: null,
    keypressCounterService: null,
};

// services/app-context-init.js
export function initAppContext() {
    //...
    // Add the following line before the Object.defineProperties(appContext, registeredServices);
    addService('mouseClickCounterService', () => new CounterService());
    addService('keypressCounterService', () => new CounterService());
    // If a service depends on other services, pass dependencies as shown below
    // If circular dependencies exist, app-init-context tests fail with timeout or stack overflow
    // (we are  going to improve it in the future)
    addService('analyticService', () =>
        new CounterService(appContext.mouseClickCounterService, appContext.keypressCounterService));
    //...
    // This following line must remains the last one in the initAppContext
    Object.defineProperties(appContext, registeredServices);
}
```

**Bad:**
```Javascript
// module counter.js
// Incorrect: shared state declared at the top level of the counter.js module
let count = 0;
export function getCount() {
    return count;
}
export function incCount() {
    count++;
}
```

## <a name="pass-dependencies-in-constructor"></a>Pass required services in the constructor for plain classes

If a class/service depends on some other service (or multiple services), the class must accept all dependencies
as parameters in the constructor.

Do not use appContext anywhere else in a class.

**Note:** This rule doesn't apply for HTML/Polymer elements classes. A browser creates instances of such classes
implicitly and calls the constructor without parameters. See
[Assign required services in a HTML/Polymer element constructor](#assign-dependencies-in-html-element-constructor)

**Good:**
```Javascript
export class UserService {
    constructor(restApiService) {
        this._restApiService = restApiService;
    }
    getLoggedIn() {
        // Send request to server using this._restApiService
    }
}
```

**Bad:**
```Javascript
import {appContext} from "./app-context";

export class UserService {
    constructor() {
        // Incorrect: you must pass all dependencies to a constructor
        this._restApiService = appContext.restApiService;
    }
}

export class AdminService {
    isAdmin() {
        // Incorrect: you must pass all dependencies to a constructor
        return appContext.restApiService.sendRequest(...);
    }
}

```

## <a name="assign-dependencies-in-html-element-constructor"></a>Assign required services in a HTML/Polymer element constructor
If a class is a custom HTML/Polymer element, the class must assign all required services in the constructor.
A browser creates instances of such classes implicitly, so it is impossible to pass anything as a parameter to
the element's class constructor.

Do not use appContext anywhere except the constructor of the class.

**Note for legacy elements:** If a polymer element extends a LegacyElementMixin and overrides the `created()` method,
move all code from this method to a constructor right after the call to a `super()`
([example](#assign-dependencies-legacy-element-example)). The `created()`
method is [deprecated](https://polymer-library.polymer-project.org/2.0/docs/about_20#lifecycle-changes) and is called
when a super (i.e. base) class constructor is called. If you are unsure about moving the code from the `created` method
to the class constructor, consult with the source code:
[`LegacyElementMixin._initializeProperties`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/legacy/legacy-element-mixin.js#L318)
and
[`PropertiesChanged.constructor`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/mixins/properties-changed.js#L177)



**Good:**
```Javascript
import {appContext} from `.../services/app-context.js`;

export class MyCustomElement extends ...{
    constructor() {
        super(); //This is mandatory to call parent constructor
        this._userService = appContext.userService;
    }
    //...
    _getUserName() {
        return this._userService.activeUserName();
    }
}
```

**Bad:**
```Javascript
import {appContext} from `.../services/app-context.js`;

export class MyCustomElement extends ...{
    created() {
        // Incorrect: assign all dependencies in the constructor
        this._userService = appContext.userService;
    }
    //...
    _getUserName() {
        // Incorrect: use appContext outside of a constructor
        return appContext.userService.activeUserName();
    }
}
```

<a name="assign-dependencies-legacy-element-example"></a>
**Legacy element:**

Before:
```Javascript
export class MyCustomElement extends ...LegacyElementMixin(...) {
    constructor() {
        super();
        someAction();
    }
    created() {
        super();
        createdAction1();
        createdAction2();
    }
}
```

After:
```Javascript
export class MyCustomElement extends ...LegacyElementMixin(...) {
    constructor() {
        super();
        // Assign services here
        this._userService = appContext.userService;
        // Code from the created method - put it before existing actions in constructor
        createdAction1();
        createdAction2();
        // Original constructor code
        someAction();
    }
    // created method is removed
}
```