├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── __tests__
└── angora-test1.ts
├── jest.config.js
├── package.json
├── tsconfig.json
└── webpackLoader.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | .DS_Store
133 | package-lock.json
134 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # AngoraForms
6 | AngoraForms is a custom form component abstraction library designed for Angular that streamlines and simplifies the process of creating reactive custom form components in Angular.
7 |
8 | Custom form components in Angular come with a bit of boilerplate code; AngoraForms' components abstract away ~90% of that boilerplate code.
9 |
10 | See https://www.angoraforms.com/ or below for example code comparison.
11 |
12 | ## Version 1.0.6
13 | Minor readme, branding update.
14 |
15 | ## Documentation
16 | The official documentation website is https://www.angoraforms.com/docs.
17 |
18 | AngoraForms version 1.0.6 was released on 7/02/23. You can find more details on https://www.npmjs.com/package/@angoraforms/angora-loader/v/1.0.3?activeTab=versions
19 |
20 | ## Key Features
21 | - **Ease of Use:** Getting started with AngoraForms is simple. Visit our documentation for a quick start up guide.
22 | - **Static Typing:** Custom components created with our abstraction library accomodate for TypeScript to adhere to the philosophy of the Angular framework and improve overall deveoper experience.
23 | - **Customizable Components:** AngoraForms does not come with component styling. Developers are free to style any components to their own tastes.
24 | - **Maintainability:** Custom components are aggregated in a single location with minimal boilerplate code normally required by the Angular framework, aiding maintainability and improving reviewability.
25 |
26 | ## Getting Started
27 | 1. Install [Node.js](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
28 |
29 | 2. Install AngoraForms [npm library link](https://www.npmjs.com/package/@angoraforms/angora-loader).
30 | ```
31 | npm i @angoraforms/angora-loader
32 | ```
33 |
34 | 3. Configure webpack.config.ts:
35 | ```typescript
36 | const path = require("path");
37 | const customComponents = require('./src/app/customComponents.ts')
38 |
39 | module.exports = {
40 | mode: "development",
41 | entry: ["./src/app/app.module.ts"],
42 | output: {},
43 | devtool: false,
44 | module: {
45 | rules: [
46 | {
47 | test: /\.ts$/,
48 | use: [
49 | {
50 | loader: "@angoraforms/angora-loader",
51 |
52 | options: {
53 | customComponents: customComponents
54 | }
55 | },
56 | ],
57 | },
58 | ],
59 | },
60 | resolve: {
61 | extensions: ['.tsx', '.ts', '.js'],
62 | },
63 | };
64 | ```
65 |
66 | 4. Create Custom Component File:
67 | ```TypeScript
68 | class customComponent1 {
69 |
70 | template = '/* html template */'
71 |
72 | onChange = (value: any) => {};
73 |
74 | onTouched = () => {};
75 |
76 | value: any = 0;
77 |
78 | disabled = false
79 | }
80 |
81 | ```
82 | Each custom component class will require a template, onChange, onTouched, value and disabled property.
83 |
84 | 5. Insert html into value of template within backticks.
85 |
86 | Example:
87 | ```TypeScript
88 | template = `
89 | {{value}}
90 |
91 |
92 | `;
93 | ```
94 |
95 | 6. Customise value and/or disabled if/as required.
96 |
97 | 7. Add custom methods to component if/as required.
98 | Example:
99 | ```TypeScript
100 | increment() {
101 | this.value++;
102 | this.onChange(this.value);
103 | this.onTouched();
104 | }
105 |
106 | decrement() {
107 | this.value--;
108 | this.onChange(this.value);
109 | this.onTouched();
110 | }
111 | ```
112 |
113 | 8. Additional custom components are added following the first custom component class.
114 |
115 | Example:
116 | ```TypeScript
117 | class customComponent1 {
118 | template = `
119 | {{value}}
120 |
121 |
122 | `;
123 |
124 | onChange = (value: any) => {};
125 |
126 | onTouched = () => {};
127 |
128 | value = 0;
129 |
130 | disabled = false
131 |
132 | increment() {
133 | this.value++;
134 | this.onChange(this.value);
135 | this.onTouched();
136 | }
137 |
138 | decrement() {
139 | this.value--;
140 | this.onChange(this.value);
141 | this.onTouched();
142 | }
143 | }
144 |
145 | class customComponent2 {
146 | template = `
147 |
148 |
149 |
150 |
151 |
152 |
153 | `;
154 |
155 | onChange = (value: any) => {};
156 |
157 | onTouched = () => {};
158 |
159 | value = '';
160 |
161 | disabled = false;
162 |
163 | onFileSelected(event: any) {
164 | const file = event.target.files[0];
165 | if (file) {
166 | this.value = file.name;
167 | console.log(this.value);
168 | this.onChange(this.value);
169 | }
170 | }
171 |
172 | onClick(fileUpload: any) {
173 | this.onTouched();
174 | fileUpload.click();
175 | }
176 | }
177 |
178 | ```
179 | 9. Export custom component classes within an array.
180 |
181 | Example:
182 | ```TypeScript
183 | module.exports = [customComponent1, customComponent2]
184 | ```
185 |
186 | 10. Run `npx webpack` in terminal before running `ng serve`.
187 |
188 | Custom component files will be generated and required modifications to the app.modules file will be made.
189 |
190 | ## Example Comparison
191 |
192 | ### Without AngoraForms:
193 |
194 | customComponent1.ts
195 | ```TypeScript
196 | import { Component } from '@angular/core';
197 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
198 |
199 | @Component({
200 | selector: 'custom-comp1',
201 | template: `
202 |
203 | {{value}}
204 |
205 |
206 |
207 | `,
208 | providers: [
209 | {
210 | provide: NG_VALUE_ACCESSOR,
211 | multi: true,
212 | useExisting: customComp1
213 | }
214 | ]
215 | })
216 | export class customComp1 implements ControlValueAccessor {
217 |
218 | onChange = (value: any) => { }
219 | onTouched = () => { }
220 | value = 0
221 | disabled = false
222 |
223 | increment() {
224 | this.value++;
225 | this.onChange(this.value);
226 | this.onTouched();
227 | }
228 | decrement() {
229 | this.value--;
230 | this.onChange(this.value);
231 | this.onTouched();
232 | }
233 |
234 | writeValue(value: any) {
235 | this.value = value
236 | }
237 |
238 | registerOnChange(onChange: any) {
239 | this.onChange = onChange
240 | }
241 |
242 | registerOnTouched(onTouched: any){
243 | this.onTouched = onTouched
244 | }
245 |
246 | setDisabledState(disabled: boolean): void {
247 | this.disabled = disabled
248 | }
249 | }
250 | ```
251 | customComponent2.ts
252 | ```TypeScript
253 |
254 | import { Component } from '@angular/core';
255 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
256 |
257 | @Component({
258 | selector: 'custom-comp2',
259 | template: `
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | `,
269 | providers: [
270 | {
271 | provide: NG_VALUE_ACCESSOR,
272 | multi: true,
273 | useExisting: customComp2
274 | }
275 | ]
276 | })
277 | export class customComp2 implements ControlValueAccessor {
278 |
279 | onChange = (value: any) => { }
280 | onTouched = () => { }
281 | value = ''
282 | disabled = false
283 |
284 | onFileSelected(event: any) {
285 | const file = event.target.files[0];
286 | if (file) {
287 | this.value = file.name;
288 | console.log(this.value);
289 | this.onChange(this.value);
290 | }
291 | }
292 | onClick(fileUpload: any) {
293 | this.onTouched();
294 | fileUpload.click();
295 | }
296 |
297 | writeValue(value: any) {
298 | this.value = value
299 | }
300 |
301 | registerOnChange(onChange: any) {
302 | this.onChange = onChange
303 | }
304 |
305 | registerOnTouched(onTouched: any){
306 | this.onTouched = onTouched
307 | }
308 |
309 | setDisabledState(disabled: boolean): void {
310 | this.disabled = disabled
311 | }
312 | }
313 | ```
314 |
315 | ### With Angora Forms:
316 |
317 | ```TypeScript
318 | class customComp1 {
319 | template = `
320 | {{value}}
321 |
322 |
323 | `;
324 |
325 | onChange = (value: any) => {};
326 |
327 | onTouched = () => {};
328 |
329 | value = 0;
330 |
331 | disabled = false
332 |
333 | increment() {
334 | this.value++;
335 | this.onChange(this.value);
336 | this.onTouched();
337 | }
338 |
339 | decrement() {
340 | this.value--;
341 | this.onChange(this.value);
342 | this.onTouched();
343 | }
344 | }
345 |
346 | class customComp2 {
347 | template = `
348 |
349 |
350 |
351 |
352 |
353 |
354 | `;
355 |
356 | onChange = (value: any) => {};
357 |
358 | onTouched = () => {};
359 |
360 | value = '';
361 |
362 | disabled = false;
363 |
364 | onFileSelected(event: any) {
365 | const file = event.target.files[0];
366 | if (file) {
367 | this.value = file.name;
368 | console.log(this.value);
369 | this.onChange(this.value);
370 | }
371 | }
372 |
373 | onClick(fileUpload: any) {
374 | this.onTouched();
375 | fileUpload.click();
376 | }
377 | }
378 |
379 | module.exports = [customComp1, customComp2]
380 | ```
381 | ## Other Information
382 |
383 | AngoraForms is in beta and will be updated in the future.
384 |
385 | https://medium.com/@wayneleung_2900/making-angular-custom-form-components-easier-to-work-with-e2f7ace48cb2
386 |
387 | Check out the companion Form Builder web application and its github repo: https://www.angoraforms.com/FormBuilder (https://github.com/AngoraForms/AngoraFormApp)
388 |
389 | ## Contributors
390 |
391 | - Aaron Chen - [Github](https://github.com/achen220) / [LinkedIn](https://www.linkedin.com/in/aaronchen9149)
392 | - Ryan Hastings - [Github](https://github.com/rhaasti) / [LinkedIn](https://www.linkedin.com/in/rhaasti)
393 | - Wayne Leung - [Github](https://github.com/waynetwleung) / [LinkedIn](https://www.linkedin.com/in/wayne-leung-1242422a)
394 | - Curtis Lovrak - [Github](https://github.com/CurtisLovrak) / [LinkedIn](https://www.linkedin.com/in/curtislovrak)
395 | - Hadar Weinstein - [Github](https://github.com/HWein8) / [LinkedIn](https://www.linkedin.com/in/hadarweinstein)
396 |
397 | ## License
398 |
399 | This project is licensed under the MIT License.
400 |
--------------------------------------------------------------------------------
/__tests__/angora-test1.ts:
--------------------------------------------------------------------------------
1 | const {
2 | generateAngularComponent,
3 | generateMethods,
4 | typescriptIfy,
5 | generateTemplate,
6 | toKebabCase,
7 | generateProperties,
8 | formatValue,
9 | } = require("../");
10 |
11 | test("generateAngularComponent should generate a valid component", () => {
12 | class TestClass {
13 | template = "Hello World
";
14 | testMethod() {
15 | return true;
16 | }
17 | }
18 |
19 | const componentCode = generateAngularComponent(TestClass);
20 | expect(componentCode).toContain(
21 | "export class TestClass implements ControlValueAccessor"
22 | );
23 | });
24 |
25 | test("generateMethods should generate method string", () => {
26 | const methods = ["", "testMethod"];
27 | const instance = {
28 | testMethod() { return true; },
29 | };
30 |
31 | const methodString = generateMethods(instance, methods);
32 | expect(methodString).toContain(`testMethod() { return true; }`);
33 | });
34 |
35 | test("typescriptIfy should add : any to function parameters", () => {
36 | const functionCode = "(param1, param2)";
37 | expect(typescriptIfy(functionCode)).toBe("(param1: any, param2: any)");
38 | });
39 |
40 | test("generateTemplate should wrap HTML in backticks", () => {
41 | const html = "Hello World
";
42 | expect(generateTemplate(html)).toBe(`\n ${html}\n `);
43 | });
44 |
45 | test("toKebabCase should convert CamelCase to kebab-case", () => {
46 | const str = "CamelCase";
47 | expect(toKebabCase(str)).toBe("camel-case");
48 | });
49 |
50 | test("generateProperties should generate properties string", () => {
51 | const instance = {
52 | template: `Hello World
`,
53 | testProp: "testValue",
54 | onChange: (value: any) => {},
55 | };
56 |
57 | const propertiesString = generateProperties(instance);
58 | expect(propertiesString).toContain("testProp = 'testValue'");
59 | expect(propertiesString).toContain("onChange = (value: any) => { }");
60 | });
61 |
62 | test("formatValue should format value correctly", () => {
63 | expect(formatValue("test")).toBe("'test'");
64 | expect(formatValue(true)).toBe("true");
65 | expect(formatValue(123)).toBe(123);
66 | });
67 |
68 | jest.mock("fs", () => ({
69 | writeFileSync: jest.fn(),
70 | }));
71 |
72 | jest.mock("path", () => ({
73 | resolve: jest.fn(),
74 | }));
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "ts-jest",
3 | testEnvironment: "node",
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@angoraforms/angora-loader",
3 | "version": "1.0.6",
4 | "description": "Angular custom form component abstractor",
5 | "main": "webpackLoader.js",
6 | "scripts": {
7 | "test": "jest"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@babel/generator": "^7.22.0",
14 | "@babel/parser": "^7.22.0",
15 | "@babel/plugin-transform-typescript": "^7.22.0",
16 | "@babel/traverse": "^7.22.0",
17 | "@babel/types": "^7.22.0",
18 | "babel-plugin-transform-decorators-legacy": "^1.3.5",
19 | "fs": "^0.0.1-security",
20 | "path": "^0.12.7",
21 | "schema-utils": "^4.0.1"
22 | },
23 | "devDependencies": {
24 | "@babel/preset-typescript": "^7.21.5",
25 | "@types/jest": "^29.5.2",
26 | "loader-utils": "^3.2.1",
27 | "ts-jest": "^29.1.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noImplicitOverride": true,
10 | "noPropertyAccessFromIndexSignature": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "downlevelIteration": true,
16 | "experimentalDecorators": true,
17 | "emitDecoratorMetadata": true,
18 | "moduleResolution": "node",
19 | "importHelpers": true,
20 | "target": "ES2022",
21 | "module": "ES2022",
22 | "useDefineForClassFields": false,
23 | "lib": [
24 | "ES2022",
25 | "dom"
26 | ],
27 | "types":["jest", "node"]
28 | },
29 | "angularCompilerOptions": {
30 | "enableI18nLegacyMessageIdFormat": false,
31 | "strictInjectionParameters": true,
32 | "strictInputAccessModifiers": true,
33 | "strictTemplates": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/webpackLoader.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | const parser = require("@babel/parser");
5 | const traverse = require("@babel/traverse").default;
6 | const generator = require("@babel/generator").default;
7 | const t = require("@babel/types");
8 |
9 |
10 |
11 | module.exports = function myLoader(source) {
12 | // app.module.ts passed in as source parameter
13 |
14 | // import passed in options
15 | const options = this.getOptions();
16 |
17 | // object destructure the customComponents array
18 | const { customComponents } = options;
19 |
20 | const fileNames = []
21 |
22 | const imports = []
23 |
24 | customComponents.forEach((ComponentClass, index) => {
25 | const className = ComponentClass.name
26 | fileNames.push(className)
27 |
28 | const fileName = `customComponent${index + 1}.ts`; // Generate a unique filename
29 | imports.push(fileName)
30 |
31 | // Create the file path using the current working directory and the filename
32 | const filePath = path.resolve(process.cwd(), fileName); // change process.cwd() to generate files into node modules?
33 |
34 | // Generate the code for the Angular class component
35 | const componentCode = generateAngularComponent(ComponentClass);
36 |
37 | // Write the code to the file
38 | fs.writeFileSync(filePath, componentCode);
39 |
40 | });
41 |
42 | // read and stringify app.module file
43 | // const code = fs.readFileSync("./src/app/app.module.ts").toString();
44 |
45 | // generate ast for app.module file
46 | const ast = parser.parse(source, {
47 | sourceType: "module",
48 | plugins: ["typescript", "decorators-legacy"],
49 | });
50 |
51 | let modified = false; // Flag to track modification
52 | let modifiedNgModule = null; // Store the NgModule decorator node that is modified
53 |
54 | const importedClassNames = fileNames; // Get the imported class names from the fileNames array
55 | const existingClassNames = new Set()
56 |
57 | // traversal through ast of app.module file
58 | traverse(ast, {
59 | Decorator(path) {
60 | // identify where new declarations will be added
61 | // find the NgModule object
62 | if (
63 | t.isCallExpression(path.node.expression) &&
64 | t.isIdentifier(path.node.expression.callee, { name: 'NgModule' }) &&
65 | !modified // Check if modification has not been applied yet
66 | ) {
67 | const ngModuleArg = path.node.expression.arguments[0];
68 | // find the declarations array
69 | if (t.isObjectExpression(ngModuleArg)) {
70 | const declarationsProp = ngModuleArg.properties.find((prop) =>
71 | t.isIdentifier(prop.key, { name: 'declarations' })
72 | );
73 |
74 |
75 |
76 | if (
77 | declarationsProp &&
78 | t.isArrayExpression(declarationsProp.value)
79 | ) {
80 |
81 | for(let i = 0; i < declarationsProp.value.elements.slice(-importedClassNames.length).length; i++){
82 | existingClassNames.add(declarationsProp.value.elements.slice(-importedClassNames.length)[i].name)
83 | }
84 |
85 | // Create an identifier for each imported class and add to the declarations array
86 | importedClassNames.forEach((className) => {
87 | if(!existingClassNames.has(className)){
88 | const importedClassIdentifier = t.identifier(className);
89 | declarationsProp.value.elements.push(importedClassIdentifier);
90 | }
91 | // check whether each className already exists in the declarations array
92 |
93 | });
94 |
95 | modified = true; // Set the flag to indicate modification
96 | modifiedNgModule = path.node; // Mark the NgModule decorator as modified
97 | }
98 | }
99 | }
100 | },
101 | ImportDeclaration(path) {
102 |
103 |
104 | // identify where new import declarations will be inserted
105 | if (
106 | t.isStringLiteral(path.node.source, { value: './app.component' }) &&
107 | !modifiedNgModule // Skip further traversal if NgModule is already modified
108 | ) {
109 |
110 | let counter = 1 // initialize counter to act as input for file name
111 |
112 | importedClassNames.forEach((className) => {
113 | if(!path.scope.bindings[className]){
114 | // Create an import specifier for the class name
115 | const importSpecifier = t.importSpecifier(
116 | t.identifier(className),
117 | t.identifier(className)
118 | );
119 |
120 | // Create a new import declaration for the class name
121 | const newImportDeclaration = t.importDeclaration(
122 | [importSpecifier],
123 | t.stringLiteral(`../../customComponent${counter}`)
124 | );
125 |
126 | counter++
127 |
128 | // Insert the new import declaration after the existing one
129 | path.insertAfter(newImportDeclaration);
130 | }
131 | });
132 | }
133 | },
134 | });
135 |
136 | const newCode = `${generator(ast).code}`;
137 |
138 | // update app.module.ts with new updated code
139 | fs.writeFileSync("./src/app/app.module.ts", newCode)
140 |
141 | return 'done';
142 | };
143 |
144 |
145 | function generateAngularComponent(ComponentClass) {
146 | const className = ComponentClass.name;
147 | // Get the class methods
148 | const methods = Object.getOwnPropertyNames(ComponentClass.prototype);
149 | // Create an instance of each component class
150 | const instance = new ComponentClass();
151 | // identify the html code provided by the user
152 | const html = instance.template;
153 |
154 | // Generate the code for the Angular class component
155 | const componentCode = `
156 | import { Component } from '@angular/core';
157 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
158 |
159 | @Component({
160 | selector: '${toKebabCase(className)}',
161 | template: \`${generateTemplate(html)}\`,
162 | providers: [
163 | {
164 | provide: NG_VALUE_ACCESSOR,
165 | multi: true,
166 | useExisting: ${className}
167 | }
168 | ]
169 | })
170 | export class ${className} implements ControlValueAccessor {
171 |
172 | ${generateProperties(instance)}
173 |
174 | ${generateMethods(ComponentClass.prototype, methods)}
175 |
176 | writeValue(value: any) {
177 | this.value = value
178 | }
179 |
180 | registerOnChange(onChange: any) {
181 | this.onChange = onChange
182 | }
183 |
184 | registerOnTouched(onTouched: any){
185 | this.onTouched = onTouched
186 | }
187 |
188 | setDisabledState(disabled: boolean): void {
189 | this.disabled = disabled
190 | }
191 | }
192 | `;
193 |
194 | return componentCode;
195 | }
196 |
197 | // generate all methods to be added to new component
198 | function generateMethods(instance, methods) {
199 | const typeScript = ': any'
200 | return (
201 | methods.slice(1)
202 | // filter through methods return only functions
203 | // iterate through methods and add the function to the new component in the right format
204 | .map((method) => {
205 |
206 | const functionCode = instance[method].toString();
207 |
208 | const position = functionCode.indexOf(')')
209 | const params = functionCode.substring(functionCode.indexOf('('), position+1)
210 | // if there are multiple parameters add : any to other parameters
211 | if(functionCode[position - 1].match(/[A-Z]|[a-z]/g)){
212 | return functionCode.replace(params, typescriptIfy(params))
213 | } else {
214 | return functionCode
215 | }
216 | })
217 | .join("\n")
218 | );
219 | }
220 |
221 | // add ': any' to any number of parameters
222 | const typescriptIfy = (functionCode, result = '', typescript = ': any') => {
223 | if(!functionCode.length){
224 | return result
225 | }
226 | if(functionCode[0] === ',' || functionCode[0] === ')'){
227 | return typescriptIfy(functionCode.slice(1), result += (typescript + functionCode[0]), typescript)
228 | }
229 | return typescriptIfy(functionCode.slice(1), result += functionCode[0], typescript)
230 | }
231 |
232 | // generate html to be added to new component
233 | function generateTemplate(html) {
234 | return `
235 | ${html}
236 | `;
237 | }
238 |
239 | // kebab (change CustomComponent to custom-component)
240 | function toKebabCase(str) {
241 | return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
242 | }
243 |
244 | // generate all properties to be added to new component
245 | function generateProperties(instance) {
246 | const typeScript = ': any'
247 | const properties = Object.entries(instance)
248 | // iterate through the class object and add the properties to the new component
249 | const newProps = properties.filter((el) => el[0] !== 'template').map((el) => {
250 | if(el[0].toString() === 'onChange'){
251 | const position = el[1].toString().indexOf(')')
252 | return `${el[0]} = ${[el[1].toString().slice(0, position), typeScript, el[1].toString().slice(position)].join('')}`;
253 | }
254 | return `${el[0]} = ${formatValue(el[1])}`;
255 | }).join("\n")
256 | return newProps
257 | }
258 |
259 | // format values of properties so that they are added in number, boolean, or string
260 | function formatValue(value) {
261 | if (typeof value === "string") {
262 | return `'${value}'`;
263 | } else {
264 | return value;
265 | }
266 | }
267 |
268 |
269 |
270 | // module.exports = {
271 | // formatValue,
272 | // generateProperties,
273 | // toKebabCase,
274 | // generateTemplate,
275 | // typescriptIfy,
276 | // generateMethods,
277 | // generateAngularComponent
278 | // };
279 |
280 |
281 |
282 |
--------------------------------------------------------------------------------