Skip to content

Commit 6b976c4

Browse files
authored
Update documentation (#3013)
* Add a note about parent selector in css doc * add a new testing doc page * Update adding a page doc
1 parent 9fb119b commit 6b976c4

File tree

4 files changed

+94
-337
lines changed

4 files changed

+94
-337
lines changed

docs/adding-a-page.md

Lines changed: 5 additions & 333 deletions
Original file line numberDiff line numberDiff line change
@@ -1,337 +1,9 @@
1-
# addons-frontend
2-
3-
This will outline what is required to add a page to the project. A basic knowledge of
4-
[react](https://facebook.github.io/react/docs/getting-started.html) and
5-
[redux](http://redux.js.org/) is assumed.
6-
7-
**Note:** This page is very out-of-date and does not reflect our practices anymore. We now use [redux-saga](https://github.com/redux-saga/redux-saga) for API requests. See `amo/components/Categories.js` for a more modern example of a component that makes API/async requests for data.
8-
9-
## Structure
10-
11-
A basic app structure will look like this:
12-
13-
```
14-
src/
15-
<app-name>/
16-
components/
17-
MyComponent/
18-
index.js
19-
styles.scss
20-
containers/
21-
MyContainer/
22-
index.js
23-
styles.scss
24-
reducers/
25-
client.js
26-
routes.js
27-
store.js
28-
tests/
29-
client/
30-
<app-name>/
31-
components/
32-
TestMyComponent.js
33-
containers/
34-
TestMyContainer.js
35-
reducers/
36-
```
37-
38-
## Components vs Containers
39-
40-
A component should have no usage of redux in it. It only operates on the data passed into it
41-
through props. A container will use redux to connect data from the store to a component. A
42-
container may or may not wrap a component, whatever makes sense to you.
43-
441
# How to Add a Page
452

46-
We'll make a basic user profile component that hits the API. We'll start by creating a basic
47-
component with data set manually, then we'll hit the API to populate redux, then we'll update the
48-
component to pull its data from the redux store.
49-
50-
## Creating a Component
51-
52-
We'll create our component in the search app since it will use the currently logged in user. Our
53-
component is going to show the currently logged in user's email address and username. To start
54-
we'll create a component without any outside data.
55-
56-
### Basic component
57-
58-
```jsx
59-
// src/search/containers/UserPage/index.js
60-
import React from 'react';
61-
62-
export default class UserPage extends React.Component {
63-
render() {
64-
return (
65-
<div className="user-page">
66-
<h1>Hi there!</h1>
67-
<ul>
68-
<li>username: my-username</li>
69-
<li>email: me@example.com</li>
70-
</ul>
71-
</div>
72-
);
73-
}
74-
}
75-
```
76-
77-
### Adding a route
78-
79-
To render this component we'll tell [react-router](https://github.com/reactjs/react-router) to load
80-
it at the `/user` path.
81-
82-
```jsx
83-
// src/search/routes.js
84-
// ... omit imports
85-
86-
export default (
87-
<Route path="/" component={App}>
88-
<Route component={LoginRequired}>
89-
<Route path="search">
90-
<IndexRoute component={CurrentSearchPage} />
91-
<Route path="addons/:slug" component={AddonPage} />
92-
</Route>
93-
// Add this line to use the `UserPage` component at the `/user` path.
94-
<Route path="user" component={UserPage} />
95-
</Route>
96-
<Route path="fxa-authenticate" component={HandleLogin} />
97-
</Route>
98-
);
99-
```
100-
101-
### Starting the server
102-
103-
Now that the component is setup we can run `yarn dev:search` and navigate to
104-
[http://localhost:3000/user](http://localhost:3000/user) to see the page. Since our component is
105-
wrapped in the `LoginRequired` component you will need to be logged in to see the page.
106-
107-
### Using props
108-
109-
We want our component's data to be able to change based on the current user. For now we'll update
110-
the component so that it uses props but we won't yet connect it to redux. We will however use
111-
react-redux's `connect()` to set the props for us as if it were pulling the data from redux.
112-
113-
```jsx
114-
// src/search/containers/UserPage/index.js
115-
import React from 'react';
116-
import PropTypes from 'prop-types';
117-
import { connect } from 'react-redux';
118-
import { compose } from 'redux';
119-
120-
class UserPage extends React.Component {
121-
static propTypes = {
122-
email: PropTypes.string.isRequired,
123-
username: PropTypes.string.isRequired,
124-
}
125-
126-
render() {
127-
const { email, username } = this.props;
128-
return (
129-
<div className="user-page">
130-
<h1>Hi there!</h1>
131-
<ul>
132-
<li>username: {username}</li>
133-
<li>email: {email}</li>
134-
</ul>
135-
</div>
136-
);
137-
}
138-
}
139-
140-
function mapStateToProps() {
141-
return {
142-
email: 'me@example.com',
143-
username: 'my-username',
144-
};
145-
}
146-
147-
export default compose(
148-
connect(mapStateToProps),
149-
)(UserPage);
150-
```
151-
152-
Changing the values in `mapStateToProps` will now update the values shown on the page.
153-
154-
NOTE: You may need to restart your server to see any changes as there is currently a bug that
155-
does not update the server rendered code on the dev server.
156-
157-
### Making API Requests
158-
159-
To access the user's data we'll add a new API function and a
160-
[`normalizr`](https://github.com/paularmstrong/normalizr) schema for user objects. Our API function
161-
will be pretty basic since it will use our internal `callApi()` function to handle accessing the
162-
API along with the `normalizr` schema to format the response data.
163-
164-
```js
165-
// src/core/api/index.js
166-
// ... omit imports
167-
168-
const addon = new schema.Entity('addons', {}, { idAttribute: 'slug' });
169-
// Tell normalizr we have "users" and they use the `username` property for
170-
// their primary key.
171-
const user = new schema.Entity('users', {}, { idAttribute: 'username' });
172-
173-
// Add this function.
174-
export function fetchProfile({ api }) {
175-
return callApi({
176-
endpoint: 'accounts/profile',
177-
schema: user,
178-
params: { lang: 'en-US' },
179-
auth: true,
180-
state: api,
181-
});
182-
}
183-
```
184-
185-
Calling the `fetchProfile()` function will hit the API, now we need to get the data into redux. We
186-
can use the `loadEntities()` action creator to dispatch a generic action for loading data from our
187-
API but we'll need to add a users reducer to store the data.
188-
189-
```js
190-
// src/core/reducers/users.js
191-
export default function users(state = {}, { payload = {} }) {
192-
if (payload.entities && payload.entities.users) {
193-
return {...state, ...payload.entities.users};
194-
}
195-
return state;
196-
}
197-
```
198-
199-
No we need to tell the app to use the reducer by adding it to our store.
200-
201-
```js
202-
// src/search/store.js
203-
import users from 'core/reducers/users';
204-
205-
export default function createStore(initialState = {}) {
206-
return _createStore(
207-
// Add the `users` reducer here.
208-
combineReducers({addons, api, auth, search, reduxAsyncConnect, users}),
209-
initialState,
210-
middleware(),
211-
);
212-
}
213-
```
214-
215-
We also don't have any record of the user's username. We'll need that to pull the right user. We
216-
can update the `auth` reducer to store it.
217-
218-
```js
219-
// src/core/reducers/authentication.js
220-
export default function authentication(state = {}, action) {
221-
const { payload, type } = action;
222-
if (type === 'SET_JWT') {
223-
return {token: payload.token};
224-
} else if (type === 'SET_CURRENT_USER') {
225-
return {...state, username: payload.username};
226-
}
227-
return state;
228-
}
229-
```
230-
231-
We'll also want to add an action creator to set the current user. The action creator just
232-
simplifies interacting with redux to keep the code clean.
233-
234-
```js
235-
// src/search/actions/index.js
236-
237-
// Add this at the bottom of the file.
238-
export function setCurrentUser(username) {
239-
return {
240-
type: 'SET_CURRENT_USER',
241-
payload: {
242-
username,
243-
},
244-
};
245-
}
246-
```
247-
248-
### Combining redux and the API
249-
250-
Now that we can hit the API and we can store that data in the redux store we need to update our
251-
component to hit the API, update the store, and pull the data from the store. To do this we will
252-
use [redux-connect](https://github.com/makeomatic/redux-connect)'s `asyncConnect()`.
253-
254-
```jsx
255-
// src/search/containers/AddonPage/index.js
256-
import React from 'react';
257-
import PropTypes from 'prop-types';
258-
import { connect } from 'react-redux';
259-
import { compose } from 'redux';
260-
import { asyncConnect } from 'redux-connect';
261-
262-
import { loadEntities, setCurrentUser } from 'core/actions';
263-
import { fetchProfile } from 'core/api';
264-
265-
class UserPage extends React.Component {
266-
static propTypes = {
267-
email: PropTypes.string.isRequired,
268-
username: PropTypes.string.isRequired,
269-
}
270-
271-
render() {
272-
const { email, username } = this.props;
273-
return (
274-
<div className="user-page">
275-
<h1>Hi there!</h1>
276-
<ul>
277-
<li>username: {username}</li>
278-
<li>email: {email}</li>
279-
</ul>
280-
</div>
281-
);
282-
}
283-
}
284-
285-
function getUser({ auth, users }) {
286-
return users[auth.username];
287-
}
288-
289-
function mapStateToProps(state) {
290-
return getUser(state);
291-
}
292-
293-
function loadProfileIfNeeded({ store: { getState, dispatch } }) {
294-
const state = getState();
295-
const user = getUser(state);
296-
if (!user) {
297-
return fetchProfile({api: state.api})
298-
.then(({entities, result}) => {
299-
dispatch(loadEntities(entities));
300-
dispatch(setCurrentUser(result));
301-
});
302-
}
303-
return Promise.resolve();
304-
}
305-
306-
export default compose(
307-
asyncConnect([{
308-
deferred: true,
309-
promise: loadProfileIfNeeded,
310-
}]),
311-
connect(mapStateToProps),
312-
)(UserPage);
313-
```
314-
315-
### Styling the page
316-
317-
To style your page you just need to import your SCSS file in your component. All of the CSS will
318-
be transpiled and minified into a single bundle in production so you will still need to namespace
319-
your styles.
3+
**Note:** this page needs to be expanded.
3204

321-
```js
322-
// src/search/containers/AddonPage/index.js
323-
// Add this line with the other imports.
324-
import './styles.scss';
325-
```
5+
A basic knowledge of [react](https://facebook.github.io/react/docs/getting-started.html) and [redux](http://redux.js.org/) is assumed.
3266

327-
```scss
328-
// src/search/containers/AddonPage/styles.scss
329-
.user-page {
330-
h1 {
331-
text-decoration: underline;
332-
}
333-
li {
334-
text-transform: uppercase;
335-
}
336-
}
337-
```
7+
We follow the [Ducks proposal](https://github.com/erikras/ducks-modular-redux) to create isolated and self contained modules including reducers, action types and action creators. See `src/core/reducers/autocomplete.js` for an example of a Ducks module.
8+
We use [redux-saga](https://github.com/redux-saga/redux-saga) for API requests. See `src/amo/components/Categories/index.js` for an example of a component that makes API/async requests for data and `src/core/sagas/autocomplete.js` for an example of a saga.
9+
Each reducer, saga, component has a corresponding test file. Please refer to it to know how to properly test your code and read [our dedicated page to testing](./testing.md).

docs/css.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Our Approach to Writing CSS
22

3-
We use [SCSS](http://sass-lang.com/documentation/file.SCSS_FOR_SASS_USERS.html) to write our CSS. All components should keep their CSS local to their own component (eg. use `Button/style.scss` next to `Button/index.js`). Here are a few guidelines for writing scalable CSS:
3+
We use [SCSS](http://sass-lang.com/documentation/file.SCSS_FOR_SASS_USERS.html) to write our CSS. All components should keep their CSS local to their own component (e.g. use `Button/styles.scss` next to `Button/index.js`). Here are a few guidelines for writing scalable CSS:
44

5-
* **Use component names for class names:** if your component is `SearchResult`, its class name should be `.SearchResult`. Any children component should start with the component name eg `.SearchResult-listItem`, etc. Any utility classes like `enabled` or `disabled` should be written with two dashes eg: `.SearchResult-textInput--enabled`.
5+
* **Use component names for class names:** if your component is `SearchResult`, its class name should be `.SearchResult`. Any children component should start with the component name, e.g. `.SearchResult-listItem`, etc. Any utility classes like `enabled` or `disabled` should be written with two dashes, e.g.: `.SearchResult-textInput--enabled`.
66
* **Always use variables for colors, margins, and sizes**: these should be app-wide variables whenever possible to keep them shared between components; when we change the link color it should change everywhere.
77
* **Do not import directly from `~core/`**: each app has its own `mixins.scss`, `vars.scss`, etc. Use `~amo/css/mixins.scss` instead of `~core/css/mixins.scss`. Each app extends core but be careful of changing things in core; it's best to put overrides in an app's mixin.
88
* **Use `px` (pixels) for size instead of `em`, `rem`, etc.**: pixel sizes work fine and keeps things consistent. If you want to scale something with screen size `%` (percent) values are okay.
9-
* **Keep CSS local to the component:** don't use `App.scss` or `SearchPage/style.scss` to style a button component on a `SearchPage` component. Instead create options that can be passed to differently style the button from the `SearchPage` component.
9+
* **Keep CSS local to the component:** don't use `App.scss` or `SearchPage/styles.scss` to style a button component on a `SearchPage` component. Instead create options that can be passed to differently style the button from the `SearchPage` component.
1010
* **Don't nest selectors; use more specific class names instead**: prefer `<ul className="List"><li className="List-item"></li></ul>` and the selector `.List-item` over `.List li`. This seems verbose but it prevents nesting bugs and confusion as the codebase grows. It also makes it easier to scan CSS rules at a glance.
11-
* **When overrides make sense, keep them in the parent component and use CSS nesting:** this is the only time we use nesting instead of specifically-named class selectors. There are a few cases where tiny adjustments may be made to `margin`, etc. for a child component in only one context. If modifying the `margin` of `Result` inside `SearchResultList`, it is appropriate to declare `.SearchResultList .Result` in `SearchResultList/style.scss`.
11+
* **Don't use the [parent selector](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#Referencing_Parent_Selectors_____parent-selector), except for pseudo-classes**: prefer full class names, it is easier to find. Using `&` for pseudo-class such as `:hover`, `:active`, etc. is allowed though.
12+
* **When overrides make sense, keep them in the parent component and use CSS nesting:** this is the only time we use nesting instead of specifically-named class selectors. There are a few cases where tiny adjustments may be made to `margin`, etc. for a child component in only one context. If modifying the `margin` of `Result` inside `SearchResultList`, it is appropriate to declare `.SearchResultList .Result` in `SearchResultList/styles.scss`.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ addons-frontend
66
* [How to develop features for mozAddonManager](./moz-addon-manager.md)
77
* [Internationalization (i18n)](./i18n.md)
88
* [Our Approach to Writing CSS](./css.md)
9+
* [Testing](./testing.md)

0 commit comments

Comments
 (0)