Skip to content

Vuejs generator #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Sep 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e9a6180
Added Vue.js generator
alOneh Sep 15, 2017
827e01b
Added api templates for Vue.js generator
alOneh Sep 15, 2017
66d496a
Added list module template for Vue.js
alOneh Sep 15, 2017
c8c8f92
Fixed the modules path
alOneh Sep 15, 2017
74b1a8f
Updated list store module for foo
alOneh Sep 15, 2017
b89c181
Added create module store for foo
alOneh Sep 15, 2017
2f22dc3
Added update module store for foo
alOneh Sep 15, 2017
36dbcf2
Added show store module for foo
alOneh Sep 15, 2017
cec2438
Added delete store module for foo
alOneh Sep 15, 2017
78bf55c
Improve the VueCrudGenerator
alOneh Sep 15, 2017
ec0c66d
Fix store module
alOneh Sep 15, 2017
726ea4a
Added routes template for vue
alOneh Sep 15, 2017
adaa3b3
Updated list store module for foo
alOneh Sep 16, 2017
90ed6f6
Added foo List component
alOneh Sep 16, 2017
f7aa44b
Namespaced foo store module
alOneh Sep 17, 2017
6f73ef7
Fixed foo store delete module
alOneh Sep 17, 2017
131204f
Fixed foo store show module
alOneh Sep 17, 2017
a46e06c
Cleanup foo store module update
alOneh Sep 17, 2017
2d92c52
Added template for foo show component
alOneh Sep 17, 2017
729e6c7
Fixed List.vue
alOneh Sep 17, 2017
e14cc71
Fixed delete store module
alOneh Sep 17, 2017
aac5f04
Fixed show store module
alOneh Sep 17, 2017
a1b37c1
Added Create.vue
alOneh Sep 19, 2017
3a4d928
Added Update.vue
alOneh Sep 20, 2017
a028642
Fix the icon of edit link on List.vue
alOneh Sep 20, 2017
cf27239
Refactored mutations syntax
alOneh Sep 20, 2017
338d2dc
Added mutation-types
alOneh Sep 20, 2017
c1149ef
Use commit instead of dispatch
alOneh Sep 21, 2017
1fb9fe7
Fix loading on List.vue
alOneh Sep 21, 2017
bde1276
Fix List.vue pagination
alOneh Sep 21, 2017
55369b5
Fix VueCrudGenerator
alOneh Sep 21, 2017
3b0db53
Fix Update.vue FormComponent syntax
alOneh Sep 21, 2017
9cb4a73
Fix missing SubmissionError
alOneh Sep 21, 2017
81419f7
Fix
alOneh Sep 21, 2017
091776e
Move fetch to utils
alOneh Sep 22, 2017
3ae4c9f
Update the documentation for VueCrudGenerator
alOneh Sep 22, 2017
bfb71fb
Added VueCrudGenerator.test
alOneh Sep 22, 2017
5985ec3
Updated test-gen command
alOneh Sep 22, 2017
c03c91f
Removed *Fetch.js
alOneh Sep 22, 2017
bdfea11
Cleanup store module update.js
alOneh Sep 22, 2017
2ba8e03
Removed prefix of fetch() in compoments
alOneh Sep 23, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"lint": "eslint src",
"build": "babel src -d lib --ignore '*.test.js'",
"watch": "babel --watch src -d lib --ignore '*.test.js'",
"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",
"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",
"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"
},
"bin": {
Expand Down
3 changes: 3 additions & 0 deletions src/generators.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ReactCrudGenerator from './generators/ReactCrudGenerator';
import ReactNativeCrudGenerator from './generators/ReactNativeCrudGenerator';
import TypescriptInterfaceGenerator from './generators/TypescriptInterfaceGenerator';
import VueCrudGenerator from './generators/VueCrudGenerator';

function wrap (cl) {
return ({hydraPrefix, templateDirectory}) => new cl({hydraPrefix, templateDirectory})
Expand All @@ -14,6 +15,8 @@ function generators (generator = 'react') {
return wrap(ReactNativeCrudGenerator);
case 'typescript':
return wrap(TypescriptInterfaceGenerator);
case 'vue':
return wrap(VueCrudGenerator)
}
}

Expand Down
195 changes: 195 additions & 0 deletions src/generators/VueCrudGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import mkdirp from 'mkdirp';
import handlebars from 'handlebars';
import fs from 'fs';
import urlapi from 'url';
import chalk from 'chalk';

export default class VueCrudGenerator {
templates = {};

constructor({hydraPrefix, templateDirectory}) {
const templatePath = `${templateDirectory}/vue/`;

this.hydraPrefix = hydraPrefix;

// modules
this.registerTemplate(templatePath, 'store/modules/foo/index.js');
this.registerTemplate(templatePath, 'store/modules/foo/create.js');
this.registerTemplate(templatePath, 'store/modules/foo/delete.js');
this.registerTemplate(templatePath, 'store/modules/foo/list.js');
this.registerTemplate(templatePath, 'store/modules/foo/update.js');
this.registerTemplate(templatePath, 'store/modules/foo/show.js');
this.registerTemplate(templatePath, 'store/modules/foo/mutation-types.js');

// components
this.registerTemplate(templatePath, 'components/foo/Create.vue');
this.registerTemplate(templatePath, 'components/foo/Form.vue');
this.registerTemplate(templatePath, 'components/foo/List.vue');
this.registerTemplate(templatePath, 'components/foo/Update.vue');
this.registerTemplate(templatePath, 'components/foo/Show.vue');

// routes
this.registerTemplate(templatePath, 'routes/foo.js');

// entrypoint
this.registerTemplate(templatePath, 'config/_entrypoint.js');

// utils
this.registerTemplate(templatePath, 'utils/fetch.js');
}

registerTemplate(templatePath, path) {
this.templates[path] = handlebars.compile(fs.readFileSync(templatePath+path).toString());
}

help(resource) {
const titleLc = resource.title.toLowerCase()

console.log('Code for the "%s" resource type has been generated!', resource.title);
console.log('Paste the following definitions in your application configuration:');
console.log(chalk.green(`
//import routes
import ${titleLc}Routes from './routes/${titleLc}';

// Add routes to VueRouter
const router = new VueRouter({
// ...
routes: [
...{ ${titleLc}Routes },
]
});

// Add the modules in the store
import { ${titleLc} from './store/modules/{ ${titleLc}/';

export const store = new Vuex.Store({
// ...
modules: {
{ ${titleLc}
}
});
`));
}

generate(api, resource, dir) {
const lc = resource.title.toLowerCase();
const titleUcFirst = resource.title.charAt(0).toUpperCase() + resource.title.slice(1);

const context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
fields: resource.readableFields,
formFields: this.buildFields(resource.writableFields),
hydraPrefix: this.hydraPrefix,
titleUcFirst
};


// Create directories
// These directories may already exist
mkdirp.sync(`${dir}/config`);
mkdirp.sync(`${dir}/routes`);
mkdirp.sync(`${dir}/utils`);

this.createDir(`${dir}/store/modules/${lc}`);
this.createDir(`${dir}/components/${lc}`);

// modules
this.createFile('store/modules/foo/index.js', `${dir}/store/modules/${lc}/index.js`, context);
this.createFile('store/modules/foo/create.js', `${dir}/store/modules/${lc}/create.js`, context);
this.createFile('store/modules/foo/delete.js', `${dir}/store/modules/${lc}/delete.js`, context);
this.createFile('store/modules/foo/list.js', `${dir}/store/modules/${lc}/list.js`, context);
this.createFile('store/modules/foo/update.js', `${dir}/store/modules/${lc}/update.js`, context);
this.createFile('store/modules/foo/show.js', `${dir}/store/modules/${lc}/show.js`, context);
this.createFile('store/modules/foo/mutation-types.js', `${dir}/store/modules/${lc}/mutation-types.js`, context);

// components
this.createFile('components/foo/Create.vue', `${dir}/components/${lc}/Create.vue`, context);
this.createFile('components/foo/Form.vue', `${dir}/components/${lc}/Form.vue`, context);
this.createFile('components/foo/List.vue', `${dir}/components/${lc}/List.vue`, context);
this.createFile('components/foo/Update.vue', `${dir}/components/${lc}/Update.vue`, context);
this.createFile('components/foo/Show.vue', `${dir}/components/${lc}/Show.vue`, context);

// config
this.createFile('config/_entrypoint.js', `${dir}/config/_entrypoint.js`, context);

// routes
this.createFile('routes/foo.js', `${dir}/routes/${lc}.js`, context);
}

entrypoint(apiEntry, dir) {
const url = urlapi.parse(apiEntry);
const {protocol, host, port, pathname} = url;
const hostUrl = `${protocol}//${host}${port ? `:${port}` : ''}`;

const context = {
host: hostUrl,
path: pathname
}

this.createFile('config/_entrypoint.js', `${dir}/config/_entrypoint.js`, context);
}

utils(dir) {
const context = {
hydraPrefix: this.hydraPrefix
}

this.createFile('utils/fetch.js', `${dir}/utils/fetch.js`, context);
}

getInputTypeFromField(field) {
switch (field.id) {
case 'http://schema.org/email':
return {type: 'email'};

case 'http://schema.org/url':
return {type: 'url'};
}

switch (field.range) {
case 'http://www.w3.org/2001/XMLSchema#integer':
return {type: 'number'};

case 'http://www.w3.org/2001/XMLSchema#decimal':
return {type: 'number', step: '0.1'};

case 'http://www.w3.org/2001/XMLSchema#boolean':
return {type: 'checkbox'};

case 'http://www.w3.org/2001/XMLSchema#date':
return {type: 'date'};

case 'http://www.w3.org/2001/XMLSchema#time':
return {type: 'time'};

default:
return {type: 'text'};
}
}

buildFields(apiFields) {
let fields = [];
for (let apiField of apiFields) {
let field = this.getInputTypeFromField(apiField);
field.required = apiField.required;
field.name = apiField.name;
field.description = apiField.description.replace(/"/g, "'"); // fix for Form placeholder description

fields.push(field)
}

return fields;
}

createDir(dir) {
if (fs.existsSync(dir)) throw new Error(`The directory "${dir}" already exists`);
mkdirp.sync(dir);
}

createFile(template, dest, context) {
fs.writeFileSync(dest, this.templates[template](context));
}
}
51 changes: 51 additions & 0 deletions src/generators/VueCrudGenerator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Api from 'api-doc-parser/lib/Api';
import Resource from 'api-doc-parser/lib/Resource';
import Field from 'api-doc-parser/lib/Field';
import fs from 'fs';
import tmp from 'tmp';
import VueCrudGenerator from './VueCrudGenerator';


test('Generate a Vue app', () => {
const generator = new VueCrudGenerator({hydraPrefix: 'hydra:', templateDirectory: `${__dirname}/../../templates`});
const tmpobj = tmp.dirSync({unsafeCleanup: true});

const fields = [new Field('bar', {
id: 'http://schema.org/url',
range: 'http://www.w3.org/2001/XMLSchema#string',
reference: null,
required: true,
description: 'An URL'
})];
const resource = new Resource('abc', 'http://example.com/foos', {
id: 'foo',
title: 'Foo',
readableFields: fields,
writableFields: fields
});
const api = new Api('http://example.com', {
title: 'My API',
resources: [resource]
});
generator.generate(api, resource, tmpobj.name);

expect(fs.existsSync(tmpobj.name+'/components/abc/Create.vue'), true);
expect(fs.existsSync(tmpobj.name+'/components/abc/Form.vue'), true);
expect(fs.existsSync(tmpobj.name+'/components/abc/List.vue'), true);
expect(fs.existsSync(tmpobj.name+'/components/abc/Show.vue'), true);
expect(fs.existsSync(tmpobj.name+'/components/abc/Update.vue'), true);

expect(fs.existsSync(tmpobj.name+'/config/_entrypoint.js'), true);

expect(fs.existsSync(tmpobj.name+'/store/modules/abc/create.js'), true);
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/delete.js'), true);
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/index.js'), true);
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/list.js'), true);
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/mutation-types.js'), true);
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/show.js'), true);
expect(fs.existsSync(tmpobj.name+'/store/modules/abc/update.js'), true);

expect(fs.existsSync(tmpobj.name+'/utils/fetch.js'), true);

tmpobj.removeCallback();
});
47 changes: 47 additions & 0 deletions templates/vue/components/foo/Create.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<div>
<h1>New {{{ title }}}</h1>

<div v-if="loading" class="alert alert-info" role="status">Loading...</div>
<div v-if="error" class="alert alert-danger" role="alert"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> \{{ error }}</div>

<{{{titleUcFirst}}}Form :handle-submit="create" :values="item" :errors="violations"></{{{titleUcFirst}}}Form>
<router-link :to="{ name: '{{{titleUcFirst}}}List' }" class="btn btn-default">Back to list</router-link>
</div>
</template>

<script>
import {{{titleUcFirst}}}Form from './Form.vue';
import { createNamespacedHelpers } from 'vuex';

const { mapActions, mapGetters } = createNamespacedHelpers('{{{lc}}}/create');

export default {
components: {
{{{titleUcFirst}}}Form
},
data: function() {
return {
item: {}
}
},
computed: mapGetters([
'error',
'loading',
'created',
'violations'
]),
methods: {
create: function(item) {
this.$store.dispatch('{{{lc}}}/create/create', item);
}
},
watch: {
created: function (created) {
if (created) {
this.$router.push({ name: '{{{titleUcFirst}}}Update', params: { id: created['@id']} });
}
}
}
}
</script>
39 changes: 39 additions & 0 deletions templates/vue/components/foo/Form.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<form @submit.prevent="handleSubmit(item)">
{{#each formFields}}
<div :class="{ 'form-group': true, 'has-error': (errors && errors.{{{ name }}}) }">
<label for="{{{ lc }}}_{{{ name }}}" class="control-label">{{{ name }}}</label>
<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" />
<span v-if="errors && errors.{{{ name }}}" class="help-block" id="{{{ lc }}}_{{{ name }}}_helpBlock">\{{ errors.{{{ name }}} }}</span>
</div>
{{/each}}

<button type="submit" class="btn btn-primary">Submit</button>
</form>
</template>

<script>
export default {
props: {
handleSubmit: {
type: Function,
required: true,
},
values: {
type: Object,
required: true
},
errors: {
type: Object
},
initialValues: {
type: Object
}
},
computed: {
item: function () {
return this.initialValues ? this.initialValues : this.values;
}
}
}
</script>
Loading