Skip to content

Commit 685520d

Browse files
alOnehmysiar
authored andcommitted
Vuejs generator (#35)
* Added Vue.js generator * Added api templates for Vue.js generator * Added list module template for Vue.js * Fixed the modules path * Updated list store module for foo * Added create module store for foo * Added update module store for foo * Added show store module for foo * Added delete store module for foo * Improve the VueCrudGenerator * Fix store module * Added routes template for vue * Updated list store module for foo * Added foo List component * Namespaced foo store module * Fixed foo store delete module * Fixed foo store show module * Cleanup foo store module update * Added template for foo show component * Fixed List.vue * Fixed delete store module * Fixed show store module * Added Create.vue * Added Update.vue * Fix the icon of edit link on List.vue * Refactored mutations syntax * Added mutation-types * Use commit instead of dispatch * Fix loading on List.vue * Fix List.vue pagination * Fix VueCrudGenerator * Fix Update.vue FormComponent syntax * Fix missing SubmissionError * Fix * Move fetch to utils * Update the documentation for VueCrudGenerator * Added VueCrudGenerator.test * Updated test-gen command * Removed *Fetch.js * Cleanup store module update.js * Removed prefix of fetch() in compoments
1 parent 5ae3568 commit 685520d

19 files changed

+1152
-1
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"lint": "eslint src",
4444
"build": "babel src -d lib --ignore '*.test.js'",
4545
"watch": "babel --watch src -d lib --ignore '*.test.js'",
46-
"test-gen": "rm -rf ./tmp && npm run build && ./lib/index.js https://demo.api-platform.com ./tmp/react && ./lib/index.js https://demo.api-platform.com ./tmp/react-native -g react-native",
46+
"test-gen": "rm -rf ./tmp && npm run build && ./lib/index.js https://demo.api-platform.com ./tmp/react && ./lib/index.js https://demo.api-platform.com ./tmp/react-native -g react-native && ./lib/index.js https://demo.api-platform.com ./tmp/vue -g vue",
4747
"test-gen-env": "rm -rf ./tmp && npm run build && API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=https://demo.api-platform.com API_PLATFORM_CLIENT_GENERATOR_OUTPUT=./tmp ./lib/index.js"
4848
},
4949
"bin": {

src/generators.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ReactCrudGenerator from './generators/ReactCrudGenerator';
22
import ReactNativeCrudGenerator from './generators/ReactNativeCrudGenerator';
33
import TypescriptInterfaceGenerator from './generators/TypescriptInterfaceGenerator';
4+
import VueCrudGenerator from './generators/VueCrudGenerator';
45

56
function wrap (cl) {
67
return ({hydraPrefix, templateDirectory}) => new cl({hydraPrefix, templateDirectory})
@@ -14,6 +15,8 @@ function generators (generator = 'react') {
1415
return wrap(ReactNativeCrudGenerator);
1516
case 'typescript':
1617
return wrap(TypescriptInterfaceGenerator);
18+
case 'vue':
19+
return wrap(VueCrudGenerator)
1720
}
1821
}
1922

src/generators/VueCrudGenerator.js

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import mkdirp from 'mkdirp';
2+
import handlebars from 'handlebars';
3+
import fs from 'fs';
4+
import urlapi from 'url';
5+
import chalk from 'chalk';
6+
7+
export default class VueCrudGenerator {
8+
templates = {};
9+
10+
constructor({hydraPrefix, templateDirectory}) {
11+
const templatePath = `${templateDirectory}/vue/`;
12+
13+
this.hydraPrefix = hydraPrefix;
14+
15+
// modules
16+
this.registerTemplate(templatePath, 'store/modules/foo/index.js');
17+
this.registerTemplate(templatePath, 'store/modules/foo/create.js');
18+
this.registerTemplate(templatePath, 'store/modules/foo/delete.js');
19+
this.registerTemplate(templatePath, 'store/modules/foo/list.js');
20+
this.registerTemplate(templatePath, 'store/modules/foo/update.js');
21+
this.registerTemplate(templatePath, 'store/modules/foo/show.js');
22+
this.registerTemplate(templatePath, 'store/modules/foo/mutation-types.js');
23+
24+
// components
25+
this.registerTemplate(templatePath, 'components/foo/Create.vue');
26+
this.registerTemplate(templatePath, 'components/foo/Form.vue');
27+
this.registerTemplate(templatePath, 'components/foo/List.vue');
28+
this.registerTemplate(templatePath, 'components/foo/Update.vue');
29+
this.registerTemplate(templatePath, 'components/foo/Show.vue');
30+
31+
// routes
32+
this.registerTemplate(templatePath, 'routes/foo.js');
33+
34+
// entrypoint
35+
this.registerTemplate(templatePath, 'config/_entrypoint.js');
36+
37+
// utils
38+
this.registerTemplate(templatePath, 'utils/fetch.js');
39+
}
40+
41+
registerTemplate(templatePath, path) {
42+
this.templates[path] = handlebars.compile(fs.readFileSync(templatePath+path).toString());
43+
}
44+
45+
help(resource) {
46+
const titleLc = resource.title.toLowerCase()
47+
48+
console.log('Code for the "%s" resource type has been generated!', resource.title);
49+
console.log('Paste the following definitions in your application configuration:');
50+
console.log(chalk.green(`
51+
//import routes
52+
import ${titleLc}Routes from './routes/${titleLc}';
53+
54+
// Add routes to VueRouter
55+
const router = new VueRouter({
56+
// ...
57+
routes: [
58+
...{ ${titleLc}Routes },
59+
]
60+
});
61+
62+
// Add the modules in the store
63+
import { ${titleLc} from './store/modules/{ ${titleLc}/';
64+
65+
export const store = new Vuex.Store({
66+
// ...
67+
modules: {
68+
{ ${titleLc}
69+
}
70+
});
71+
`));
72+
}
73+
74+
generate(api, resource, dir) {
75+
const lc = resource.title.toLowerCase();
76+
const titleUcFirst = resource.title.charAt(0).toUpperCase() + resource.title.slice(1);
77+
78+
const context = {
79+
title: resource.title,
80+
name: resource.name,
81+
lc,
82+
uc: resource.title.toUpperCase(),
83+
fields: resource.readableFields,
84+
formFields: this.buildFields(resource.writableFields),
85+
hydraPrefix: this.hydraPrefix,
86+
titleUcFirst
87+
};
88+
89+
90+
// Create directories
91+
// These directories may already exist
92+
mkdirp.sync(`${dir}/config`);
93+
mkdirp.sync(`${dir}/routes`);
94+
mkdirp.sync(`${dir}/utils`);
95+
96+
this.createDir(`${dir}/store/modules/${lc}`);
97+
this.createDir(`${dir}/components/${lc}`);
98+
99+
// modules
100+
this.createFile('store/modules/foo/index.js', `${dir}/store/modules/${lc}/index.js`, context);
101+
this.createFile('store/modules/foo/create.js', `${dir}/store/modules/${lc}/create.js`, context);
102+
this.createFile('store/modules/foo/delete.js', `${dir}/store/modules/${lc}/delete.js`, context);
103+
this.createFile('store/modules/foo/list.js', `${dir}/store/modules/${lc}/list.js`, context);
104+
this.createFile('store/modules/foo/update.js', `${dir}/store/modules/${lc}/update.js`, context);
105+
this.createFile('store/modules/foo/show.js', `${dir}/store/modules/${lc}/show.js`, context);
106+
this.createFile('store/modules/foo/mutation-types.js', `${dir}/store/modules/${lc}/mutation-types.js`, context);
107+
108+
// components
109+
this.createFile('components/foo/Create.vue', `${dir}/components/${lc}/Create.vue`, context);
110+
this.createFile('components/foo/Form.vue', `${dir}/components/${lc}/Form.vue`, context);
111+
this.createFile('components/foo/List.vue', `${dir}/components/${lc}/List.vue`, context);
112+
this.createFile('components/foo/Update.vue', `${dir}/components/${lc}/Update.vue`, context);
113+
this.createFile('components/foo/Show.vue', `${dir}/components/${lc}/Show.vue`, context);
114+
115+
// config
116+
this.createFile('config/_entrypoint.js', `${dir}/config/_entrypoint.js`, context);
117+
118+
// routes
119+
this.createFile('routes/foo.js', `${dir}/routes/${lc}.js`, context);
120+
}
121+
122+
entrypoint(apiEntry, dir) {
123+
const url = urlapi.parse(apiEntry);
124+
const {protocol, host, port, pathname} = url;
125+
const hostUrl = `${protocol}//${host}${port ? `:${port}` : ''}`;
126+
127+
const context = {
128+
host: hostUrl,
129+
path: pathname
130+
}
131+
132+
this.createFile('config/_entrypoint.js', `${dir}/config/_entrypoint.js`, context);
133+
}
134+
135+
utils(dir) {
136+
const context = {
137+
hydraPrefix: this.hydraPrefix
138+
}
139+
140+
this.createFile('utils/fetch.js', `${dir}/utils/fetch.js`, context);
141+
}
142+
143+
getInputTypeFromField(field) {
144+
switch (field.id) {
145+
case 'http://schema.org/email':
146+
return {type: 'email'};
147+
148+
case 'http://schema.org/url':
149+
return {type: 'url'};
150+
}
151+
152+
switch (field.range) {
153+
case 'http://www.w3.org/2001/XMLSchema#integer':
154+
return {type: 'number'};
155+
156+
case 'http://www.w3.org/2001/XMLSchema#decimal':
157+
return {type: 'number', step: '0.1'};
158+
159+
case 'http://www.w3.org/2001/XMLSchema#boolean':
160+
return {type: 'checkbox'};
161+
162+
case 'http://www.w3.org/2001/XMLSchema#date':
163+
return {type: 'date'};
164+
165+
case 'http://www.w3.org/2001/XMLSchema#time':
166+
return {type: 'time'};
167+
168+
default:
169+
return {type: 'text'};
170+
}
171+
}
172+
173+
buildFields(apiFields) {
174+
let fields = [];
175+
for (let apiField of apiFields) {
176+
let field = this.getInputTypeFromField(apiField);
177+
field.required = apiField.required;
178+
field.name = apiField.name;
179+
field.description = apiField.description.replace(/"/g, "'"); // fix for Form placeholder description
180+
181+
fields.push(field)
182+
}
183+
184+
return fields;
185+
}
186+
187+
createDir(dir) {
188+
if (fs.existsSync(dir)) throw new Error(`The directory "${dir}" already exists`);
189+
mkdirp.sync(dir);
190+
}
191+
192+
createFile(template, dest, context) {
193+
fs.writeFileSync(dest, this.templates[template](context));
194+
}
195+
}
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Api from 'api-doc-parser/lib/Api';
2+
import Resource from 'api-doc-parser/lib/Resource';
3+
import Field from 'api-doc-parser/lib/Field';
4+
import fs from 'fs';
5+
import tmp from 'tmp';
6+
import VueCrudGenerator from './VueCrudGenerator';
7+
8+
9+
test('Generate a Vue app', () => {
10+
const generator = new VueCrudGenerator({hydraPrefix: 'hydra:', templateDirectory: `${__dirname}/../../templates`});
11+
const tmpobj = tmp.dirSync({unsafeCleanup: true});
12+
13+
const fields = [new Field('bar', {
14+
id: 'http://schema.org/url',
15+
range: 'http://www.w3.org/2001/XMLSchema#string',
16+
reference: null,
17+
required: true,
18+
description: 'An URL'
19+
})];
20+
const resource = new Resource('abc', 'http://example.com/foos', {
21+
id: 'foo',
22+
title: 'Foo',
23+
readableFields: fields,
24+
writableFields: fields
25+
});
26+
const api = new Api('http://example.com', {
27+
title: 'My API',
28+
resources: [resource]
29+
});
30+
generator.generate(api, resource, tmpobj.name);
31+
32+
expect(fs.existsSync(tmpobj.name+'/components/abc/Create.vue'), true);
33+
expect(fs.existsSync(tmpobj.name+'/components/abc/Form.vue'), true);
34+
expect(fs.existsSync(tmpobj.name+'/components/abc/List.vue'), true);
35+
expect(fs.existsSync(tmpobj.name+'/components/abc/Show.vue'), true);
36+
expect(fs.existsSync(tmpobj.name+'/components/abc/Update.vue'), true);
37+
38+
expect(fs.existsSync(tmpobj.name+'/config/_entrypoint.js'), true);
39+
40+
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/create.js'), true);
41+
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/delete.js'), true);
42+
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/index.js'), true);
43+
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/list.js'), true);
44+
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/mutation-types.js'), true);
45+
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/show.js'), true);
46+
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/update.js'), true);
47+
48+
expect(fs.existsSync(tmpobj.name+'/utils/fetch.js'), true);
49+
50+
tmpobj.removeCallback();
51+
});
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div>
3+
<h1>New {{{ title }}}</h1>
4+
5+
<div v-if="loading" class="alert alert-info" role="status">Loading...</div>
6+
<div v-if="error" class="alert alert-danger" role="alert"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> \{{ error }}</div>
7+
8+
<{{{titleUcFirst}}}Form :handle-submit="create" :values="item" :errors="violations"></{{{titleUcFirst}}}Form>
9+
<router-link :to="{ name: '{{{titleUcFirst}}}List' }" class="btn btn-default">Back to list</router-link>
10+
</div>
11+
</template>
12+
13+
<script>
14+
import {{{titleUcFirst}}}Form from './Form.vue';
15+
import { createNamespacedHelpers } from 'vuex';
16+
17+
const { mapActions, mapGetters } = createNamespacedHelpers('{{{lc}}}/create');
18+
19+
export default {
20+
components: {
21+
{{{titleUcFirst}}}Form
22+
},
23+
data: function() {
24+
return {
25+
item: {}
26+
}
27+
},
28+
computed: mapGetters([
29+
'error',
30+
'loading',
31+
'created',
32+
'violations'
33+
]),
34+
methods: {
35+
create: function(item) {
36+
this.$store.dispatch('{{{lc}}}/create/create', item);
37+
}
38+
},
39+
watch: {
40+
created: function (created) {
41+
if (created) {
42+
this.$router.push({ name: '{{{titleUcFirst}}}Update', params: { id: created['@id']} });
43+
}
44+
}
45+
}
46+
}
47+
</script>

templates/vue/components/foo/Form.vue

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<template>
2+
<form @submit.prevent="handleSubmit(item)">
3+
{{#each formFields}}
4+
<div :class="{ 'form-group': true, 'has-error': (errors && errors.{{{ name }}}) }">
5+
<label for="{{{ lc }}}_{{{ name }}}" class="control-label">{{{ name }}}</label>
6+
<input v-model="item.{{{ name }}}" type="{{{ type }}}" {{#if step}} step="{{{ step }}}"{{/if}} placeholder="{{{ description }}}" {{#if required}}required="true"{{/if}} id="{{{ lc }}}_{{{ name }}}" class="form-control" />
7+
<span v-if="errors && errors.{{{ name }}}" class="help-block" id="{{{ lc }}}_{{{ name }}}_helpBlock">\{{ errors.{{{ name }}} }}</span>
8+
</div>
9+
{{/each}}
10+
11+
<button type="submit" class="btn btn-primary">Submit</button>
12+
</form>
13+
</template>
14+
15+
<script>
16+
export default {
17+
props: {
18+
handleSubmit: {
19+
type: Function,
20+
required: true,
21+
},
22+
values: {
23+
type: Object,
24+
required: true
25+
},
26+
errors: {
27+
type: Object
28+
},
29+
initialValues: {
30+
type: Object
31+
}
32+
},
33+
computed: {
34+
item: function () {
35+
return this.initialValues ? this.initialValues : this.values;
36+
}
37+
}
38+
}
39+
</script>

0 commit comments

Comments
 (0)