Skip to content
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

feat: Angular generator #386

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions src/generators.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenera
import VueGenerator from "./generators/VueGenerator.js";
import VuetifyGenerator from "./generators/VuetifyGenerator.js";
import QuasarGenerator from "./generators/QuasarGenerator.js";
import AngularGenerator from "./generators/AngularGenerator.js";

function wrap(cl) {
return ({ hydraPrefix, templateDirectory }) =>
Expand Down Expand Up @@ -36,5 +37,7 @@ export default async function generators(generator = "react") {
return wrap(VuetifyGenerator);
case "quasar":
return wrap(QuasarGenerator);
case "angular":
return wrap(AngularGenerator);
}
}
157 changes: 157 additions & 0 deletions src/generators/AngularGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import BaseGenerator from "./BaseGenerator.js";
import handlebars from "handlebars";
import hbhComparison from "handlebars-helpers/lib/comparison.js";
import hbhString from "handlebars-helpers/lib/string.js";

export default class extends BaseGenerator {
constructor(params) {
super(params);

this.registerTemplates("angular/", [
// COMMON COMPONENTS
"app/components/common/delete/delete.component.html",
"app/components/common/delete/delete.component.ts",
"app/components/common/form/form.component.html",
"app/components/common/form/form.component.ts",
"app/components/common/header/header.component.html",
"app/components/common/header/header.component.ts",
"app/components/common/sidebar/sidebar.component.html",
"app/components/common/sidebar/sidebar.component.ts",
"app/components/common/table/table.component.html",
"app/components/common/table/table.component.ts",

// COMPONENTS
"app/components/foo/create/create.component.html",
"app/components/foo/create/create.component.ts",
"app/components/foo/edit/edit.component.html",
"app/components/foo/edit/edit.component.ts",
"app/components/foo/list/list.component.html",
"app/components/foo/list/list.component.ts",
"app/components/foo/show/show.component.html",
"app/components/foo/show/show.component.ts",
"app/app.component.html",
"app/app.component.ts",

//SVG COMPONENT
"app/components/svg/list-svg/list-svg.component.svg",
"app/components/svg/list-svg/list-svg.component.ts",
"app/components/svg/menu/menu.component.svg",
"app/components/svg/menu/menu.component.ts",

//INTERFACE
"app/interface/api.ts",
"app/interface/foo.model.ts",
"app/interface/hero.model.ts",
"app/interface/list.model.ts",
"app/interface/show.model.ts",
"app/interface/update.model.ts",

// ROUTER
"app/router/foo.ts",
"app/app.routes.ts",

//SERVICE
"app/service/hero.service.ts",
]);

handlebars.registerHelper("compare", hbhComparison.compare);
handlebars.registerHelper("lowercase", hbhString.lowercase);
}

generate(api, resource, dir) {
const lc = resource.title.toLowerCase();
const titleUcFirst =
resource.title.charAt(0).toUpperCase() + resource.title.slice(1);
const fields = this.parseFields(resource);
const hasIsRelation = fields.some((field) => field.isRelation);
const hasIsRelations = fields.some((field) => field.isRelations);
const hasDateField = fields.some((field) => field.type === "dateTime");

const context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
fields,
formFields: this.buildFields(fields),
hydraPrefix: this.hydraPrefix,
titleUcFirst,
hasIsRelation,
hasIsRelations,
hasRelations: hasIsRelation || hasIsRelations,
hasDateField,
};

//CREATE DIRECTORIES - These directories may already exist
[
`${dir}/assets`,
`${dir}/utils`,
`${dir}/app/components/${lc}`,
`${dir}/app/components/common`,
`${dir}/app/components/svg`,
`${dir}/app/interface`,
`${dir}/app/router`,
`${dir}/app/service`,
].forEach((dir) => this.createDir(dir, false));

//CREATE FILE
[
`${dir}/app/components/svg/list-svg/list-svg.component.svg`,
`${dir}/app/components/svg/list-svg/list-svg.component.ts`,
`${dir}/app/components/svg/menu/menu.component.svg`,
`${dir}/app/components/svg/menu/menu.component.ts`,
`${dir}/app/components/common/delete/delete.component.html`,
`${dir}/app/components/common/delete/delete.component.ts`,
`${dir}/app/components/common/form/form.component.html`,
`${dir}/app/components/common/form/form.component.ts`,
`${dir}/app/components/common/header/header.component.html`,
`${dir}/app/components/common/header/header.component.ts`,
`${dir}/app/components/common/sidebar/sidebar.component.html`,
`${dir}/app/components/common/sidebar/sidebar.component.ts`,
`${dir}/app/components/common/table/table.component.html`,
`${dir}/app/app.component.html`,
`${dir}/app/app.component.ts`,
`${dir}/app/app.routes.ts`,
].forEach((file) => this.createFile(file, file, context, false));

[
`app/components/%s/create/create.component.html",
"app/components/%s/create/create.component.ts",
"app/components/%s/edit/edit.component.html",
"app/components/%s/edit/edit.component.ts",
"app/components/%s/list/list.component.html",
"app/components/%s/list/list.component.ts",
"app/components/%s/show/show.component.html",
"app/components/%s/show/show.component.ts",`,
].forEach((file) => this.createFileFromPattern(file, dir, [lc], context));
}

parseFields(resource) {
const fields = [
...resource.writableFields,
...resource.readableFields,
].reduce((list, field) => {
if (list[field.name]) {
return list;
}

const isReferences = Boolean(
field.reference && field.maxCardinality !== 1
);
const isEmbeddeds = Boolean(field.embedded && field.maxCardinality !== 1);

return {
...list,
[field.name]: {
...field,
isReferences,
isEmbeddeds,
isRelation: field.reference || field.embedded,
isRelations: isEmbeddeds || isReferences,
},
};
}, {});

return Object.values(fields);
}
}
75 changes: 75 additions & 0 deletions src/generators/AngularGenerator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Api, Resource, Field } from "@api-platform/api-doc-parser";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import tmp from "tmp";
import AngularGenerator from "./AngularGenerator.js";

const dirname = path.dirname(fileURLToPath(import.meta.url));

test("Generate a React app", () => {
const generator = new AngularGenerator({
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: "abc",
title: "abc",
readableFields: fields,
writableFields: fields,
});
const api = new Api("http://example.com", {
entrypoint: "http://example.com:8080",
title: "My API",
resources: [resource],
});
generator.generate(api, resource, tmpobj.name);

[
"app/components/common/delete/delete.component.html",
"app/components/common/delete/delete.component.ts",
"app/components/common/form/form.component.html",
"app/components/common/form/form.component.ts",
"app/components/common/header/header.component.html",
"app/components/common/header/header.component.ts",
"app/components/common/sidebar/sidebar.component.html",
"app/components/common/sidebar/sidebar.component.ts",
"app/components/common/table /table.component.html",
"app/components/common/table/table.component.ts",
"app/components/svg/list-svg/list-svg.component.svg",
"app/components/svg/list-svg/list-svg.component.ts",
"app/components/svg/menu/menu.component.svg",
"app/components/svg/menu/menu.component.ts",
"app/interface/api.ts",
"app/interface/foo.model.ts",
"app/interface/hero.model.ts",
"app/interface/list.model.ts",
"app/interface/show.model.ts",
"app/interface/update.model.ts",
"app/router/foo.ts",
"app/service/hero.service.ts",
].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true));

[
"/components/abc/Form.tsx",
"/components/abc/List.tsx",
"/components/abc/Show.tsx",
"/interfaces/Abc.ts",
].forEach((file) => {
expect(fs.existsSync(tmpobj.name + file)).toBe(true);
expect(fs.readFileSync(tmpobj.name + file, "utf8")).toMatch(/bar/);
});

tmpobj.removeCallback();
});
1 change: 1 addition & 0 deletions src/generators/BaseGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default class {
context,
templateValues = ["foo", "Foo"]
) {
console.log(dir);
this.createFile(
vsprintf(pattern, templateValues),
vsprintf(`${dir}/${pattern}`, values),
Expand Down
9 changes: 9 additions & 0 deletions templates/angular/app/app.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<app-header></app-header>
<div class="flex">
<div class="w-1/6 h-100">
<app-sidebar></app-sidebar>
</div>
<div class="w-5/6 ">
<router-outlet></router-outlet>
</div>
</div>
17 changes: 17 additions & 0 deletions templates/angular/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Component} from '@angular/core';
import {RouterOutlet} from '@angular/router';
import {AsyncPipe} from "@angular/common";
import {HttpClientModule} from "@angular/common/http";
import {SidebarComponent} from "./components/common/sidebar/sidebar.component";
import {HeaderComponent} from "./components/common/header/header.component";

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, AsyncPipe, HttpClientModule, SidebarComponent, HeaderComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'Api Platform Angular Admin';
}
24 changes: 24 additions & 0 deletions templates/angular/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Routes} from '@angular/router';
import {ListComponent} from "./components/foo/list/list.component";
import {ShowComponent} from "./components/foo/show/show.component";
import {EditComponent} from "./components/foo/edit/edit.component";
import {CreateComponent} from "./components/foo/create/create.component";

export const routes: Routes = [
{
path: 'heroes',
component: ListComponent
},
{
path: 'heroes/add',
component: CreateComponent
},
{
path: 'heroes/:id',
component: ShowComponent,
},
{
path: 'heroes/:id/edit',
component: EditComponent
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<button
(click)="deleteAction()"
[className]="!disabled ?
'py-2 px-4 bg-red-600 text-white text-sm rounded shadow-md hover:bg-red-700' :
'py-2 px-4 bg-red-600 text-white text-sm rounded shadow-md opacity-25'"
[disabled]="disabled">
Delete
</button>
23 changes: 23 additions & 0 deletions templates/angular/app/components/common/delete/delete.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {HeroService} from "../../../service/hero.service";
import {Location} from "@angular/common";

@Component({
selector: 'app-delete',
standalone: true,
imports: [],
templateUrl: './delete.component.html',
})
export class DeleteComponent {
@Input() disabled!: boolean
@Output() delete: EventEmitter<Function> = new EventEmitter<Function>()
constructor(
private heroService: HeroService,
private location: Location
) {
}

deleteAction () {
return this.delete.emit()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>form works!</p>
11 changes: 11 additions & 0 deletions templates/angular/app/components/common/form/form.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';

@Component({
selector: 'app-form',
standalone: true,
imports: [],
templateUrl: './form.component.html',
})
export class FormComponent {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.header {
background-color: #288690;
width: 100%;
padding: 0 8px;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2),
0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12);
}

.menu {
padding: 8px;
cursor: pointer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<header class="header">
<div class="menu">
<app-menu />
</div>
</header>
Loading
Loading