Skip to content

Commit df9ce0f

Browse files
committed
docs: add docs for custom generator
no new argument
1 parent 1df6938 commit df9ce0f

File tree

2 files changed

+303
-21
lines changed

2 files changed

+303
-21
lines changed

client-generator/custom.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Custom Generator
2+
3+
You will probably want to extend or, at least, take a look at [BaseGenerator.js](https://github.com/api-platform/client-generator/blob/main/src/generators/BaseGenerator.js), since the library expects some methods to be available, as well as one of the included generator to make your own.
4+
5+
## Usage
6+
7+
```shell
8+
generate-api-platform-client -g "$(pwd)/path/to/custom/generator.js" -t "$(pwd)/path/to/templates"
9+
```
10+
11+
The `-g` argument can point to any resolvable node module which means it can be a package dependency of the current project as well as any js file.
12+
13+
## Example
14+
15+
Let's create a basic react generator with Create form as an example:
16+
17+
### Generator
18+
19+
```js
20+
// ./Generator.js
21+
import BaseGenerator from "@api-platform/client-generator/lib/generators/BaseGenerator";
22+
23+
export default class extends BaseGenerator {
24+
constructor(params) {
25+
super(params);
26+
27+
this.registerTemplates("", [
28+
"utils/dataAccess.js",
29+
"components/foo/Create.js",
30+
"routes/foo.js",
31+
]);
32+
}
33+
34+
help(resource) {
35+
const titleLc = resource.title.toLowerCase();
36+
37+
console.log(
38+
'Code for the "%s" resource type has been generated!',
39+
resource.title
40+
);
41+
console.log(`
42+
//import routes
43+
import ${titleLc}Routes from './routes/${titleLc}';
44+
45+
// Add routes to <Switch>
46+
{ ${titleLc}Routes }
47+
`);
48+
}
49+
50+
generate(api, resource, dir) {
51+
const lc = resource.title.toLowerCase();
52+
const titleUcFirst =
53+
resource.title.charAt(0).toUpperCase() + resource.title.slice(1);
54+
55+
const context = {
56+
title: resource.title,
57+
name: resource.name,
58+
lc,
59+
uc: resource.title.toUpperCase(),
60+
fields: resource.readableFields,
61+
formFields: this.buildFields(resource.writableFields),
62+
hydraPrefix: this.hydraPrefix,
63+
titleUcFirst,
64+
};
65+
66+
// Create directories
67+
// These directories may already exist
68+
[`${dir}/utils`, `${dir}/config`, `${dir}/routes`].forEach((dir) =>
69+
this.createDir(dir, false)
70+
);
71+
72+
[`${dir}/components/${lc}`].forEach((dir) => this.createDir(dir));
73+
74+
["components/%s/Create.js", "routes/%s.js"].forEach((pattern) =>
75+
this.createFileFromPattern(pattern, dir, lc, context)
76+
);
77+
78+
// utils
79+
this.createFile(
80+
"utils/dataAccess.js",
81+
`${dir}/utils/dataAccess.js`,
82+
context,
83+
false
84+
);
85+
86+
this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`);
87+
}
88+
}
89+
```
90+
91+
### `Create` component
92+
93+
```js
94+
// template/components/Create.js
95+
import React from 'react';
96+
import { Redirect } from 'react-router-dom';
97+
import fetch from '../utils/dataAccess';
98+
99+
export default function Create() {
100+
const [isLoading, setLoading] = useState(false);
101+
const [error, setError] = useState(null);
102+
const [created, setCreated] = useState(null);
103+
104+
const create = useCallback(async (e) => {
105+
setLoading(true)
106+
try {
107+
const values = Array.from(e.target.elements).reduce((vals, e) => {
108+
vals[e.id] = e.value;
109+
return vals
110+
}, {})
111+
const response = await fetch('{{{name}}}', { method: 'POST', body: JSON.stringify(values) });
112+
const retrieved = await response.json();
113+
setCreated(retrieved);
114+
} catch (err) {
115+
setError(err);
116+
} finally {
117+
setLoading(false);
118+
}
119+
}, [setLoading, setError])
120+
121+
if (created) {
122+
return <Redirect to={`edit/${encodeURIComponent(created['@id'])}`} />;
123+
}
124+
125+
return (
126+
<div>
127+
<h1>New {{{title}}}</h1>
128+
129+
{isLoading && (
130+
<div className="alert alert-info" role="status">
131+
Loading...
132+
</div>
133+
)}
134+
{error && (
135+
<div className="alert alert-danger" role="alert">
136+
<span className="fa fa-exclamation-triangle" aria-hidden="true" />{' '}
137+
{error}
138+
</div>
139+
)}
140+
141+
<form onSubmit={create}>
142+
{{#each formFields}}
143+
<div className={`form-group`}>
144+
<label
145+
htmlFor={`{{{lc}}}_{{{name}}}`}
146+
className="form-control-label"
147+
>
148+
{data.input.name}
149+
</label>
150+
<input
151+
name="{{{name}}}"
152+
type="{{{type}}}"{{#if step}}
153+
step="{{{step}}}"{{/if}}
154+
placeholder="{{{description}}}"{{#if required}}
155+
required={true}{{/if}}
156+
id={`{{{lc}}}_{{{name}}}`}
157+
/>
158+
</div>
159+
160+
<button type="submit" className="btn btn-success">
161+
Submit
162+
</button>
163+
</form>
164+
</div>
165+
);
166+
}
167+
```
168+
169+
### Utilities
170+
171+
```js
172+
// template/entrypoint.js
173+
export const ENTRYPOINT = "{{{entrypoint}}}";
174+
```
175+
176+
```js
177+
// template/routes/foo.js
178+
import React from "react";
179+
import { Route } from "react-router-dom";
180+
import { Create } from "../components/{{{lc}}}/";
181+
182+
export default [
183+
<Route path="/{{{name}}}/create" component={Create} exact key="create" />,
184+
];
185+
```
186+
187+
```js
188+
// template/utils/dataAccess.js
189+
import { ENTRYPOINT } from "../config/entrypoint";
190+
import { SubmissionError } from "redux-form";
191+
import get from "lodash/get";
192+
import has from "lodash/has";
193+
import mapValues from "lodash/mapValues";
194+
195+
const MIME_TYPE = "application/ld+json";
196+
197+
export function fetch(id, options = {}) {
198+
if ("undefined" === typeof options.headers) options.headers = new Headers();
199+
if (null === options.headers.get("Accept"))
200+
options.headers.set("Accept", MIME_TYPE);
201+
202+
if (
203+
"undefined" !== options.body &&
204+
!(options.body instanceof FormData) &&
205+
null === options.headers.get("Content-Type")
206+
)
207+
options.headers.set("Content-Type", MIME_TYPE);
208+
209+
return global.fetch(new URL(id, ENTRYPOINT), options).then((response) => {
210+
if (response.ok) return response;
211+
212+
return response.json().then(
213+
(json) => {
214+
const error =
215+
json["hydra:description"] ||
216+
json["hydra:title"] ||
217+
"An error occurred.";
218+
if (!json.violations) throw Error(error);
219+
220+
let errors = { _error: error };
221+
json.violations.forEach((violation) =>
222+
errors[violation.propertyPath]
223+
? (errors[violation.propertyPath] +=
224+
"\n" + errors[violation.propertyPath])
225+
: (errors[violation.propertyPath] = violation.message)
226+
);
227+
228+
throw new SubmissionError(errors);
229+
},
230+
() => {
231+
throw new Error(response.statusText || "An error occurred.");
232+
}
233+
);
234+
});
235+
}
236+
237+
export function mercureSubscribe(url, topics) {
238+
topics.forEach((topic) =>
239+
url.searchParams.append("topic", new URL(topic, ENTRYPOINT))
240+
);
241+
242+
return new EventSource(url.toString());
243+
}
244+
245+
export function normalize(data) {
246+
if (has(data, "hydra:member")) {
247+
// Normalize items in collections
248+
data["hydra:member"] = data["hydra:member"].map((item) =>
249+
normalize(item)
250+
);
251+
252+
return data;
253+
}
254+
255+
// Flatten nested documents
256+
return mapValues(data, (value) =>
257+
Array.isArray(value)
258+
? value.map((v) => normalize(v))
259+
: value instanceof Object
260+
? normalize(value)
261+
: get(value, "@id", value)
262+
);
263+
}
264+
265+
export function extractHubURL(response) {
266+
const linkHeader = response.headers.get("Link");
267+
if (!linkHeader) return null;
268+
269+
const matches = linkHeader.match(
270+
/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/
271+
);
272+
273+
return matches && matches[1] ? new URL(matches[1], ENTRYPOINT) : null;
274+
}
275+
```
276+
277+
Then we can use our generator:
278+
279+
```shell
280+
generate-api-platform-client https://demo.api-platform.com out/ -g "$(pwd)/Generator.js" -t "$(pwd)/template"
281+
```

client-generator/index.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,30 @@ Client Generator is the fastest way to scaffold fully featured webapps and nativ
88

99
It is able to generate apps using the following frontend stacks:
1010

11-
* [Next.js](nextjs.md)
12-
* [Nuxt.js](nuxtjs.md)
13-
* [Quasar Framework](quasar.md)
14-
* [Vuetify](vuetify.md)
15-
* [React with Redux](react.md)
16-
* [React Native](react-native.md)
17-
* [Vue.js](vuejs.md)
11+
- [Next.js](nextjs.md)
12+
- [Nuxt.js](nuxtjs.md)
13+
- [Quasar Framework](quasar.md)
14+
- [Vuetify](vuetify.md)
15+
- [React with Redux](react.md)
16+
- [React Native](react-native.md)
17+
- [Vue.js](vuejs.md)
18+
- [Or bring your custom generator](custom.md)
1819

1920
Client Generator works especially well with APIs built with the [API Platform](https://api-platform.com) framework.
2021

2122
## Features
2223

23-
* Generates high-quality ES6:
24-
* list view (with pagination)
25-
* detail view
26-
* creation form
27-
* update form
28-
* delete button
29-
* Supports to-one and to-many relations
30-
* Uses the appropriate input type (`number`, `date`...)
31-
* Client-side validation
32-
* Subscribes to data updates pushed by servers supporting [the Mercure protocol](https://mercure.rocks)
33-
* Displays server-side validation errors under the related input (if using API Platform Core)
34-
* Integration with [Bootstrap](https://getbootstrap.com/) and [FontAwesome](https://fontawesome.com/) (Progressive Web Apps)
35-
* Integration with [React Native Elements](https://react-native-training.github.io/react-native-elements/)
36-
* Accessible to people with disabilities ([ARIA](https://www.w3.org/WAI/intro/aria) support in webapps)
24+
- Generates high-quality ES6:
25+
- list view (with pagination)
26+
- detail view
27+
- creation form
28+
- update form
29+
- delete button
30+
- Supports to-one and to-many relations
31+
- Uses the appropriate input type (`number`, `date`...)
32+
- Client-side validation
33+
- Subscribes to data updates pushed by servers supporting [the Mercure protocol](https://mercure.rocks)
34+
- Displays server-side validation errors under the related input (if using API Platform Core)
35+
- Integration with [Bootstrap](https://getbootstrap.com/) and [FontAwesome](https://fontawesome.com/) (Progressive Web Apps)
36+
- Integration with [React Native Elements](https://react-native-training.github.io/react-native-elements/)
37+
- Accessible to people with disabilities ([ARIA](https://www.w3.org/WAI/intro/aria) support in webapps)

0 commit comments

Comments
 (0)