├── .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 | --------------------------------------------------------------------------------