Repository: styleguidist/react-styleguidist
Pull request #784 (2018-01-22 09:37:02)

Sort properties by 'required' and 'name' attributes
I've noticed that properties are not sorted. I think it would make sense to first show the required properties and then the optional ones. Each of both groups should be grouped by name.

This PR solves the issue by adjusting how `propsToArray` works.

codecov-io: # [Codecov](https://codecov.io/gh/styleguidist/react-styleguidist/pull/784?src=pr&el=h1) Report
> Merging [#784](https://codecov.io/gh/styleguidist/react-styleguidist/pull/784?src=pr&el=desc) into [master](https://codecov.io/gh/styleguidist/react-styleguidist/commit/b8e2ceb83fae59ea06a9af27a645b2670d715892?src=pr&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `100%`.

| [Impacted Files](https://codecov.io/gh/styleguidist/react-styleguidist/pull/784?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/rsg-components/Props/PropsRenderer.js](https://codecov.io/gh/styleguidist/react-styleguidist/pull/784/diff?src=pr&el=tree#diff-c3JjL3JzZy1jb21wb25lbnRzL1Byb3BzL1Byb3BzUmVuZGVyZXIuanM=) | `96.07% <100%> (+0.16%)` | :arrow_up: |

dotcs: @sapegin I'm not completely sure, but I guess it's not possible since `props` is an object all the time until it's converted to an array. IMHO this is the first (and only?) position where we can sort values.
sapegin: I don't think there's a strict requirement to keep it as an object, but I'm afraid it may be too difficult to change now.
dotcs: @sapegin Changing how the props-loader works was a bit harder, but it's possible I guess. Please have another look. Since I don't know the code by heart I hope I have found every occurrence of props object vs. array, so please consider having another look into other components that are involved which I could have forgotten. 
dotcs: @sapegin What do you think? Are there still changes necessary or are you fine with it? Current review status says that there are still requested changes.
dotcs: @sapegin Alright, I've fixed all findings 👍 
n1313: Can this be made optional? Props have different significance, and I personally sort/group them manually in the order of significance. For example, I would dislike having something like `onTransactionSigned` being placed above `transactions` because you wouldn't understand what `onTransactionSigned` does without reading about `transactions` first. 
dotcs: @n1313 I guess it could be made optional. In the props-loader we have access to the `config` object, which could be used to add additional config to it.
Here we could enable or disable the sorting, e.g. by having a property called `sortComponentProperties: boolean`. The default value could be `false` to keep the default as it is right now. 
@sapegin What do you think?
sapegin: I'd suggest to make a more generic option to transform props with the default implementation that will sort them like in this PR. To disable sorting you could pass a function that does nothing. I don't really like the idea or an option that control sorting of props, that feels too specific.
dotcs: I've added my idea of how a generic function could look like. What are your opinions on that approach?
n1313: The option of sorting props is valuable and useful, but I would personally prefer the default prop display behaviour to not change (especially if it is coming in with only a minor version bump). 
sapegin: I'd like to collect more opinions, so far we're two against one ;-)

@okonet @kof @SaraVieira @stepancar What do you think?
dotcs: @n1313 I understand your point, but I do not agree. The current implementation does not make any assumptions about the order of the properties. Eventually it uses the sorting of keys in an object, which is not standardised (see [here](https://stackoverflow.com/a/5525820)). So this should change anyways to make it reliable - even if this had not been an issue so far.
kof: this sort by default makes sense, evtl optionally could be disabled, but I don't care right now about this, I hate manually decide what is more important and most of the time just put  require stuff first. Btw this should be an eslint feature which also works with --fix
n1313: Well, truth be told, I'm still sitting on v5.3.2 (because I couldn't get over the previous UI change that came in with a minor version bump and undoing that change is too much effort), so this change won't affect me at all. But if I was on v6, I would be pissed. 
kof: One suggestion to validate users reaction could be to release a beta first and see if any one else has  hard feelings about this change. In general prettier showed that good automated defaults are well accepted by the community.
n1313: Hey, nice, I dislike prettier too! :) 
kof: Btw. having this in eslint would completely solve the problem as you would shift this decision to the user.
n1313: https://eslint.org/docs/rules/sort-keys exists, but it doesn't work with --fix :|
dotcs: eslint does not solve this problem, because eslint is a tool for structuring code. But it does not change the way how the underlying JavaScript engine interprets objects and how it sorts the keys. `react-docgen` (or `react-docgen-typescript`) returns the property as an object in which the property names are the keys. This is what we get and what we have to work with! 
As mentioned sorting of keys in an dictionary is [not standardised](https://stackoverflow.com/a/5525820). This means that how the properties are sorted right now is not guaranteed in any way. eslint does not change anything - regardless of how the user sort the properties and how they get parsed by `react-docgen`.

The only way to solve this problem is to use a data-structure with a guaranteed order, such as arrays. This PR tries to archive an order of the properties by transforming the object to an array of objects where a strict order is defined and uses this array for property rendering.

So please keep this discussion focused. eslint does something completely different. The `sort-keys` rule is meant for readability not for data integrity. It does not help in this discussion in any way.
sapegin: > Well, truth be told, I'm still sitting on v5.3.2 (because I couldn't get over the previous UI change that came in with a minor version bump and undoing that change is too much effort), so this change won't affect me at all. But if I was on v6, I would be pissed.

The let’s just do this and see if anyone else will complain. I believe this would be more useful for majority of the users and we have a way to disable this. We can’t make all users happy anyway.
dotcs: > [...] we have a way to disable this

That's true. We could disable it by using `propsTransform: props => props`. This passes the array without any transformation (also no sorting) which would replicate the way it has been done before.

To completely solve the problem and not rely on the pseudo-sorting of keys in an object we would have to change the way how `react-docgen` and `react-docgen-typescript` save properties. It should be an array already at this point such that the order is guaranteed, but this is not the scope of this PR.
n1313: > let’s just do this and see if anyone else will complain. 

What's the plan if someone else does complain? Roll back the release?

> We can’t make all users happy anyway.

In my opinion it is important to note that current situation with props being unsorted is *not* a known source of unhappiness for any noticeable fraction of users of RSG (at the very least I am not aware of any issues or complaints about this in the repo). Many users are relying on current behaviour of RSG even though it is non-standard, and changing it will no doubt be a surprise to them. 

Please consider keeping current behaviour as default by shipping with `propsTransform: props => props` and instead adding the sorting function to documentation as a useful example of using `propsTransform`. 


sapegin: > What's the plan if someone else does complain? Roll back the release?

We can always change the default value.

> Please consider keeping current behaviour as default by shipping with propsTransform: props => props and instead adding the sorting function to documentation as a useful example of using propsTransform.

I *did* consider that and have made a decision, thanks for comments.
okonet: I think it’s a great thing to put required props on top and option below. I can imagine some push back from very pedantic users who carefully organize props in the source code so an opt-out should be provided but I believe most of users will appreciate this. 

Patch:
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 0ddc62e0..3673738d 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -37,6 +37,7 @@ By default, Styleguidist will look for `styleguide.config.js` file in your proje
 * [`template`](#template)
 * [`theme`](#theme)
 * [`title`](#title)
+* [`transformProps`](#transformprops)
 * [`updateExample`](#updateexample)
 * [`verbose`](#verbose)
 * [`webpackConfig`](#webpackconfig)
@@ -430,6 +431,20 @@ Type: `String`, default: `<app name from package.json> Style Guide`
 
 Style guide title.
 
+#### `transformProps`
+
+Type: `Function`, optional
+
+Function that transforms component properties. By default properties are sorted such that required properties come first, optional prameters come second. Properties in both groups are sorted by their property names.
+
+To disable sorting the identity function can be used:
+
+```javascript
+module.exports = {
+  transformProps: props => props
+}
+```
+
 #### `updateExample`
 
 Type: `Function`, optional
diff --git a/docs/Documenting.md b/docs/Documenting.md
index 14ecc63a..d243ae3d 100644
--- a/docs/Documenting.md
+++ b/docs/Documenting.md
@@ -51,7 +51,7 @@ export default class Button extends React.Component {
 
 > **Note:** You can change its behavior using [propsParser](Configuration.md#propsparser) and [resolver](Configuration.md#resolver) options.
 
-> **Note:** Component’s `PropTypes` and documentation comments are parsed by the [react-docgen](https://github.com/reactjs/react-docgen) library.
+> **Note:** Component’s `PropTypes` and documentation comments are parsed by the [react-docgen](https://github.com/reactjs/react-docgen) library. They can be modified using the [transformProps](Configuration.md#transformprops) function.
 
 ## Usage examples and Readme files
 
diff --git a/loaders/props-loader.js b/loaders/props-loader.js
index 65cfb7ec..0857658e 100644
--- a/loaders/props-loader.js
+++ b/loaders/props-loader.js
@@ -8,6 +8,7 @@ const toAst = require('to-ast');
 const logger = require('glogg')('rsg');
 const getExamples = require('./utils/getExamples');
 const getProps = require('./utils/getProps');
+const sortProps = require('./utils/sortProps');
 const consts = require('../scripts/consts');
 
 const ERROR_MISSING_DEFINITION = 'No suitable component definition found.';
@@ -24,6 +25,7 @@ module.exports = function(source) {
 	const defaultParser = (filePath, source, resolver, handlers) =>
 		reactDocs.parse(source, resolver, handlers);
 	const propsParser = config.propsParser || defaultParser;
+	const propsTransform = config.propsTransform || sortProps;
 
 	let props = {};
 	try {
@@ -53,6 +55,19 @@ module.exports = function(source) {
 
 	props = getProps(props, file);
 
+	const componentProps = props.props;
+	if (componentProps) {
+		// Transform the properties to an array. This will allow for sorting.
+		const propsAsArray = Object.keys(componentProps).reduce((acc, name) => {
+			componentProps[name].name = name;
+			acc.push(componentProps[name]);
+			return acc;
+		}, []);
+
+		// Pipe through transform method and override component properties.
+		props.props = propsTransform(propsAsArray);
+	}
+
 	// Examples from Markdown file
 	const examplesFile = config.getExampleFilename(file);
 	props.examples = getExamples(examplesFile, props.displayName, config.defaultExample);
diff --git a/loaders/utils/sortProps.js b/loaders/utils/sortProps.js
new file mode 100644
index 00000000..934263db
--- /dev/null
+++ b/loaders/utils/sortProps.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const sortBy = require('lodash/sortBy');
+
+/**
+ * Sorts an array of properties by their 'required' property first and 'name'
+ * property second.
+ *
+ * @param {array} props
+ * @return {array} Sorted properties
+ */
+function sortProps(props) {
+	const requiredPropNames = sortBy(props.filter(prop => prop.required), 'name');
+	const optionalPropNames = sortBy(props.filter(prop => !prop.required), 'name');
+	const sortedProps = requiredPropNames.concat(optionalPropNames);
+	return sortedProps;
+}
+
+module.exports = sortProps;
diff --git a/scripts/schemas/config.js b/scripts/schemas/config.js
index 453b0cda..aa85da6d 100644
--- a/scripts/schemas/config.js
+++ b/scripts/schemas/config.js
@@ -235,6 +235,9 @@ module.exports = {
 		},
 		example: 'My Style Guide',
 	},
+	transformProps: {
+		type: 'function',
+	},
 	updateExample: {
 		type: 'function',
 		default: props => {
diff --git a/src/rsg-components/Props/Props.spec.js b/src/rsg-components/Props/Props.spec.js
index 475e3b05..cb769cfb 100644
--- a/src/rsg-components/Props/Props.spec.js
+++ b/src/rsg-components/Props/Props.spec.js
@@ -1,14 +1,16 @@
+/* eslint-disable react/prop-types */
 import React from 'react';
 import { parse } from 'react-docgen';
-import PropsRenderer, { propsToArray, columns, getRowKey } from './PropsRenderer';
+import PropsRenderer, { columns, getRowKey } from './PropsRenderer';
 import { unquote, getType, showSpaces } from './util';
 
+const propsToArray = props => Object.keys(props).map(name => ({ ...props[name], name }));
+
 // Test renderers with clean readable snapshot diffs
-// eslint-disable-next-line react/prop-types
 export default function ColumnsRenderer({ props }) {
 	return (
 		<ul>
-			{propsToArray(props).map((row, rowIdx) => (
+			{props.map((row, rowIdx) => (
 				<li key={rowIdx}>
 					{columns.map(({ render }, colIdx) => <div key={colIdx}>{render(row)}</div>)}
 				</li>
@@ -32,7 +34,7 @@ function render(propTypes, defaultProps = []) {
 			}
 		}
 	`);
-	return shallow(<ColumnsRenderer props={props.props} />);
+	return shallow(<ColumnsRenderer props={propsToArray(props.props)} />);
 }
 
 function renderFlow(propsType, defaultProps = []) {
@@ -50,14 +52,14 @@ function renderFlow(propsType, defaultProps = []) {
 			}
 		}
 	`);
-	return shallow(<ColumnsRenderer props={props.props} />);
+	return shallow(<ColumnsRenderer props={propsToArray(props.props)} />);
 }
 
 describe('PropsRenderer', () => {
 	it('should render a table', () => {
 		const actual = shallow(
 			<PropsRenderer
-				props={{ color: { type: { name: 'string' }, required: false, description: '' } }}
+				props={[{ type: { name: 'string' }, required: false, description: '', name: 'color' }]}
 			/>
 		);
 
@@ -238,8 +240,9 @@ describe('props columns', () => {
 	});
 
 	it('should render arguments from JsDoc tags', () => {
-		const props = {
-			size: {
+		const props = [
+			{
+				name: 'size',
 				type: {
 					name: 'number',
 				},
@@ -260,15 +263,16 @@ describe('props columns', () => {
 					],
 				},
 			},
-		};
+		];
 		const actual = shallow(<ColumnsRenderer props={props} />);
 
 		expect(actual).toMatchSnapshot();
 	});
 
 	it('should render return from JsDoc tags', () => {
-		const getProps = tag => ({
-			size: {
+		const getProps = tag => [
+			{
+				name: 'size',
 				type: {
 					name: 'number',
 				},
@@ -284,7 +288,7 @@ describe('props columns', () => {
 					],
 				},
 			},
-		});
+		];
 
 		const actualForReturn = shallow(<ColumnsRenderer props={getProps('return')} />);
 
@@ -296,8 +300,9 @@ describe('props columns', () => {
 	});
 
 	it('should render name as deprecated when tag deprecated is present', () => {
-		const props = {
-			size: {
+		const props = [
+			{
+				name: 'size',
 				type: {
 					name: 'number',
 				},
@@ -312,7 +317,7 @@ describe('props columns', () => {
 					],
 				},
 			},
-		};
+		];
 		const actual = shallow(<ColumnsRenderer props={props} />);
 
 		expect(actual).toMatchSnapshot();
diff --git a/src/rsg-components/Props/PropsRenderer.js b/src/rsg-components/Props/PropsRenderer.js
index cc0c8805..b32b0632 100644
--- a/src/rsg-components/Props/PropsRenderer.js
+++ b/src/rsg-components/Props/PropsRenderer.js
@@ -12,7 +12,6 @@ import Type from 'rsg-components/Type';
 import Text from 'rsg-components/Text';
 import Para from 'rsg-components/Para';
 import Table from 'rsg-components/Table';
-import map from 'lodash/map';
 import { unquote, getType, showSpaces } from './util';
 
 function renderType(type) {
@@ -238,10 +237,6 @@ export function getRowKey(row) {
 	return row.name;
 }
 
-export function propsToArray(props) {
-	return map(props, (prop, name) => ({ ...prop, name }));
-}
-
 export const columns = [
 	{
 		caption: 'Prop name',
@@ -262,9 +257,9 @@ export const columns = [
 ];
 
 export default function PropsRenderer({ props }) {
-	return <Table columns={columns} rows={propsToArray(props)} getRowKey={getRowKey} />;
+	return <Table columns={columns} rows={props} getRowKey={getRowKey} />;
 }
 
 PropsRenderer.propTypes = {
-	props: PropTypes.object.isRequired,
+	props: PropTypes.array.isRequired,
 };
diff --git a/src/rsg-components/Usage/Usage.js b/src/rsg-components/Usage/Usage.js
index c5500de6..02641c92 100644
--- a/src/rsg-components/Usage/Usage.js
+++ b/src/rsg-components/Usage/Usage.js
@@ -21,7 +21,7 @@ export default function Usage({ props: { props, methods } }) {
 
 Usage.propTypes = {
 	props: PropTypes.shape({
-		props: PropTypes.object,
+		props: PropTypes.array,
 		methods: PropTypes.array,
 	}).isRequired,
 };
diff --git a/src/rsg-components/Usage/Usage.spec.js b/src/rsg-components/Usage/Usage.spec.js
index 5a0c5b29..ee987ea5 100644
--- a/src/rsg-components/Usage/Usage.spec.js
+++ b/src/rsg-components/Usage/Usage.spec.js
@@ -1,13 +1,14 @@
 import React from 'react';
 import Usage from './Usage';
 
-const props = {
-	children: {
+const props = [
+	{
+		name: 'children',
 		type: { name: 'string' },
 		required: true,
 		description: 'Button label.',
 	},
-};
+];
 const methods = [
 	{
 		name: 'set',
diff --git a/src/rsg-components/Usage/__snapshots__/Usage.spec.js.snap b/src/rsg-components/Usage/__snapshots__/Usage.spec.js.snap
index 55603d93..d9435adb 100644
--- a/src/rsg-components/Usage/__snapshots__/Usage.spec.js.snap
+++ b/src/rsg-components/Usage/__snapshots__/Usage.spec.js.snap
@@ -29,15 +29,16 @@ exports[`Usage should render props table 1`] = `
 <div>
   <PropsRenderer
     props={
-      Object {
-        "children": Object {
+      Array [
+        Object {
           "description": "Button label.",
+          "name": "children",
           "required": true,
           "type": Object {
             "name": "string",
           },
         },
-      }
+      ]
     }
   />
 </div>


Test patch:
diff --git a/loaders/__tests__/props-loader.spec.js b/loaders/__tests__/props-loader.spec.js
index 3232e05e..41a22497 100644
--- a/loaders/__tests__/props-loader.spec.js
+++ b/loaders/__tests__/props-loader.spec.js
@@ -1,6 +1,8 @@
 import vm from 'vm';
 import { readFileSync } from 'fs';
 import glogg from 'glogg';
+
+import sortBy from 'lodash/sortBy';
 import config from '../../scripts/schemas/config';
 import propsLoader from '../props-loader';
 
@@ -44,6 +46,67 @@ it('should extract doclets', () => {
 	expect(result).toMatch(/require\('!!.*?\/loaders\/examples-loader\.js!\.\/examples.md'\)/);
 });
 
+describe('property sorting', () => {
+	it('should sort properties by default', () => {
+		const file = './test/components/Price/Price.js';
+		const result = propsLoader.call(
+			{
+				request: file,
+				_styleguidist,
+			},
+			readFileSync(file, 'utf8')
+		);
+		expect(result).toBeTruthy();
+
+		expect(() => new vm.Script(result)).not.toThrow();
+		expect(result.includes('makeABarrelRoll')).toBe(false);
+		expect(result).toMatch(
+			/props[\s\S]*?name': 'symbol'[\s\S]*?name': 'value'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'unit'/m
+		);
+	});
+
+	it('should be possible to disable sorting', () => {
+		const file = './test/components/Price/Price.js';
+		const result = propsLoader.call(
+			{
+				request: file,
+				_styleguidist: { ..._styleguidist, propsTransform: props => props },
+			},
+			readFileSync(file, 'utf8')
+		);
+		expect(result).toBeTruthy();
+
+		expect(() => new vm.Script(result)).not.toThrow();
+		expect(result.includes('makeABarrelRoll')).toBe(false);
+		expect(result).toMatch(
+			/props[\s\S]*?name': 'value'[\s\S]*?name': 'unit'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'symbol'/m
+		);
+	});
+
+	it('should be possible to write custom sort function', () => {
+		const sortFn = props => {
+			const requiredProps = sortBy(props.filter(prop => prop.required), 'name').reverse();
+			const optionalProps = sortBy(props.filter(prop => !prop.required), 'name').reverse();
+			return optionalProps.concat(requiredProps);
+		};
+		const file = './test/components/Price/Price.js';
+		const result = propsLoader.call(
+			{
+				request: file,
+				_styleguidist: { ..._styleguidist, propsTransform: sortFn },
+			},
+			readFileSync(file, 'utf8')
+		);
+		expect(result).toBeTruthy();
+
+		expect(() => new vm.Script(result)).not.toThrow();
+		expect(result.includes('makeABarrelRoll')).toBe(false);
+		expect(result).toMatch(
+			/props[\s\S]*?name': 'unit'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'value'[\s\S]*?name': 'symbol'/m
+		);
+	});
+});
+
 it('should work with JSDoc annnotated components', () => {
 	const file = './test/components/Annotation/Annotation.js';
 	const result = propsLoader.call(
diff --git a/loaders/utils/__tests__/__snapshots__/getSections.spec.js.snap b/loaders/utils/__tests__/__snapshots__/getSections.spec.js.snap
index 57e8da85..6af4869a 100644
--- a/loaders/utils/__tests__/__snapshots__/getSections.spec.js.snap
+++ b/loaders/utils/__tests__/__snapshots__/getSections.spec.js.snap
@@ -56,6 +56,19 @@ Array [
         },
         "slug": "placeholder-2",
       },
+      Object {
+        "filepath": "components/Price/Price.js",
+        "hasExamples": true,
+        "metadata": Object {},
+        "module": Object {
+          "require": "~/test/components/Price/Price.js",
+        },
+        "pathLine": "components/Price/Price.js",
+        "props": Object {
+          "require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
+        },
+        "slug": "price-2",
+      },
       Object {
         "filepath": "components/RandomButton/RandomButton.js",
         "hasExamples": true,
@@ -107,6 +120,19 @@ Array [
         },
         "slug": "placeholder-3",
       },
+      Object {
+        "filepath": "components/Price/Price.js",
+        "hasExamples": true,
+        "metadata": Object {},
+        "module": Object {
+          "require": "~/test/components/Price/Price.js",
+        },
+        "pathLine": "components/Price/Price.js",
+        "props": Object {
+          "require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
+        },
+        "slug": "price-3",
+      },
       Object {
         "filepath": "components/RandomButton/RandomButton.js",
         "hasExamples": true,
@@ -175,6 +201,19 @@ Object {
       },
       "slug": "placeholder",
     },
+    Object {
+      "filepath": "components/Price/Price.js",
+      "hasExamples": true,
+      "metadata": Object {},
+      "module": Object {
+        "require": "~/test/components/Price/Price.js",
+      },
+      "pathLine": "components/Price/Price.js",
+      "props": Object {
+        "require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
+      },
+      "slug": "price",
+    },
     Object {
       "filepath": "components/RandomButton/RandomButton.js",
       "hasExamples": true,
@@ -243,6 +282,19 @@ Object {
       },
       "slug": "placeholder-1",
     },
+    Object {
+      "filepath": "components/Price/Price.js",
+      "hasExamples": true,
+      "metadata": Object {},
+      "module": Object {
+        "require": "~/test/components/Price/Price.js",
+      },
+      "pathLine": "components/Price/Price.js",
+      "props": Object {
+        "require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
+      },
+      "slug": "price-1",
+    },
     Object {
       "filepath": "components/RandomButton/RandomButton.js",
       "hasExamples": true,
diff --git a/loaders/utils/__tests__/getComponentFiles.spec.js b/loaders/utils/__tests__/getComponentFiles.spec.js
index 97b0ed1f..71b50c20 100644
--- a/loaders/utils/__tests__/getComponentFiles.spec.js
+++ b/loaders/utils/__tests__/getComponentFiles.spec.js
@@ -30,6 +30,7 @@ it('getComponentFiles() should accept components as a glob', () => {
 		'~/components/Annotation/Annotation.js',
 		'~/components/Button/Button.js',
 		'~/components/Placeholder/Placeholder.js',
+		'~/components/Price/Price.js',
 		'~/components/RandomButton/RandomButton.js',
 	]);
 });
@@ -39,6 +40,7 @@ it('getComponentFiles() should ignore specified patterns', () => {
 	expect(deabs(result)).toEqual([
 		'~/components/Annotation/Annotation.js',
 		'~/components/Placeholder/Placeholder.js',
+		'~/components/Price/Price.js',
 	]);
 });
 
diff --git a/loaders/utils/__tests__/getComponentFilesFromSections.spec.js b/loaders/utils/__tests__/getComponentFilesFromSections.spec.js
index 54da82b0..9127315d 100644
--- a/loaders/utils/__tests__/getComponentFilesFromSections.spec.js
+++ b/loaders/utils/__tests__/getComponentFilesFromSections.spec.js
@@ -30,10 +30,14 @@ it('getComponentFilesFromSections() should return a list of files', () => {
 	expect(deabs(result)).toEqual([
 		'~/components/Button/Button.js',
 		'~/components/Placeholder/Placeholder.js',
+		'~/components/Price/Price.js',
 	]);
 });
 
 it('getComponentFilesFromSections() should ignore specified patterns', () => {
 	const result = getComponentFilesFromSections(sections, configDir, ['**/*Button*']);
-	expect(deabs(result)).toEqual(['~/components/Placeholder/Placeholder.js']);
+	expect(deabs(result)).toEqual([
+		'~/components/Placeholder/Placeholder.js',
+		'~/components/Price/Price.js',
+	]);
 });
diff --git a/loaders/utils/__tests__/sortProps.spec.js b/loaders/utils/__tests__/sortProps.spec.js
new file mode 100644
index 00000000..a3d99662
--- /dev/null
+++ b/loaders/utils/__tests__/sortProps.spec.js
@@ -0,0 +1,37 @@
+import sortProps from '../sortProps';
+
+function makeProp(name, required, defaultValue, type) {
+	// Default values.
+	required = required === undefined ? false : required;
+	type = type === undefined ? { name: 'string' } : type;
+
+	return {
+		name,
+		required,
+		defaultValue,
+		type,
+	};
+}
+
+it('should sort required props', () => {
+	const props = [makeProp('prop2', true), makeProp('prop1', true)];
+	const result = sortProps(props);
+	expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop2']);
+});
+
+it('should sort optional props', () => {
+	const props = [makeProp('prop2', false), makeProp('prop1', false)];
+	const result = sortProps(props);
+	expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop2']);
+});
+
+it('should sort mixed props (required props should come first)', () => {
+	const props = [
+		makeProp('prop2', false),
+		makeProp('prop1', true),
+		makeProp('prop3', true),
+		makeProp('prop4', false),
+	];
+	const result = sortProps(props);
+	expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop3', 'prop2', 'prop4']);
+});
diff --git a/test/components/Price/Price.js b/test/components/Price/Price.js
new file mode 100644
index 00000000..aa9abb9b
--- /dev/null
+++ b/test/components/Price/Price.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const unitSymbols = {
+	USD: '$',
+	EUR: '€',
+};
+
+/**
+ * Price component that renders a price and a unit.
+ */
+export default function Price(props) {
+	let Host = 'span';
+	if (props.emphasize) {
+		Host = 'em';
+	}
+	return (
+		<Host>
+			{props.value}
+			{!props.symbol ? props.unit : unitSymbols[props.unit]}
+		</Host>
+	);
+}
+
+Price.propTypes = {
+	/** Price value. */
+	value: PropTypes.number.isRequired,
+	/** Price unit */
+	unit: PropTypes.oneOf(['EUR', 'USD']),
+	/** Flag that determines if the price should be emphasized or not. */
+	emphasize: PropTypes.bool,
+	/** Defines if the unit should be shown as a symbol or not. */
+	symbol: PropTypes.bool.isRequired,
+};