├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package.json ├── projects ├── bootstrap │ ├── .eslintrc.json │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── bootstrap.module.ts │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── input-typeahead │ │ │ │ │ ├── input-typeahead.component.spec.ts │ │ │ │ │ ├── input-typeahead.component.ts │ │ │ │ │ └── input-typeahead.examples.ts │ │ │ │ ├── register.ts │ │ │ │ ├── select │ │ │ │ │ ├── select.component.spec.ts │ │ │ │ │ ├── select.component.ts │ │ │ │ │ └── select.examples.ts │ │ │ │ └── tabs │ │ │ │ │ └── tabs.component.ts │ │ │ └── constants.ts │ │ ├── public_api.ts │ │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── core │ ├── .eslintrc.json │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── components │ │ │ │ ├── base-dynamic-component.ts │ │ │ │ ├── base-html-element.ts │ │ │ │ ├── base-ui-component.ts │ │ │ │ ├── form-element-component.ts │ │ │ │ └── ngx-dynamic-component.ts │ │ │ ├── core.module.ts │ │ │ ├── html-elements │ │ │ │ ├── a │ │ │ │ │ └── a.component.ts │ │ │ │ ├── index.ts │ │ │ │ ├── li │ │ │ │ │ └── li.element.ts │ │ │ │ ├── ol │ │ │ │ │ └── ol.element.ts │ │ │ │ └── ul │ │ │ │ │ └── ul.element.ts │ │ │ ├── models.ts │ │ │ ├── properties │ │ │ │ ├── README.md │ │ │ │ ├── descriptor.ts │ │ │ │ ├── index.ts │ │ │ │ ├── maps.ts │ │ │ │ └── models.ts │ │ │ ├── services │ │ │ │ ├── core.service.spec.ts │ │ │ │ └── core.service.ts │ │ │ ├── styles │ │ │ │ ├── accordion.scss │ │ │ │ └── label.scss │ │ │ ├── ui-components │ │ │ │ ├── button │ │ │ │ │ ├── button.component.spec.ts │ │ │ │ │ └── button.component.ts │ │ │ │ ├── container │ │ │ │ │ ├── container.component.html │ │ │ │ │ └── container.component.ts │ │ │ │ ├── form │ │ │ │ │ ├── form.component.html │ │ │ │ │ └── form.component.ts │ │ │ │ ├── icon │ │ │ │ │ ├── icon.component.spec.ts │ │ │ │ │ └── icon.component.ts │ │ │ │ ├── input │ │ │ │ │ ├── input.component.spec.ts │ │ │ │ │ ├── input.component.ts │ │ │ │ │ └── input.examples.ts │ │ │ │ ├── label │ │ │ │ │ └── label.component.ts │ │ │ │ ├── link │ │ │ │ │ └── link.component.ts │ │ │ │ ├── radio-group │ │ │ │ │ ├── radio-group.component.spec.ts │ │ │ │ │ └── radio-group.component.ts │ │ │ │ ├── register.ts │ │ │ │ ├── repeater │ │ │ │ │ ├── repeater.component.html │ │ │ │ │ ├── repeater.component.ts │ │ │ │ │ └── repeater.examples.ts │ │ │ │ ├── select │ │ │ │ │ ├── select.component.spec.ts │ │ │ │ │ └── select.component.ts │ │ │ │ ├── text │ │ │ │ │ └── text.component.ts │ │ │ │ └── textarea │ │ │ │ │ ├── textarea.component.spec.ts │ │ │ │ │ └── textarea.component.ts │ │ │ └── utils │ │ │ │ ├── helpers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── parsers.ts │ │ │ │ └── renderer.ts │ │ ├── public_api.ts │ │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json └── tools │ ├── .eslintrc.json │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── components │ │ │ ├── add-dialog │ │ │ │ ├── add-dialog.component.spec.ts │ │ │ │ └── add-dialog.component.ts │ │ │ ├── control-editor │ │ │ │ ├── control-editor.component.scss │ │ │ │ ├── control-editor.component.spec.ts │ │ │ │ └── control-editor.component.ts │ │ │ ├── preview-editor │ │ │ │ ├── preview-editor.component.html │ │ │ │ ├── preview-editor.component.scss │ │ │ │ └── preview-editor.component.ts │ │ │ └── properties-editor │ │ │ │ ├── properties-editor.component.spec.ts │ │ │ │ └── properties-editor.component.ts │ │ ├── monaco.config.ts │ │ └── tools.module.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── src ├── _vars.scss ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── cards │ │ │ ├── cards.component.html │ │ │ ├── cards.component.scss │ │ │ ├── cards.component.spec.ts │ │ │ └── cards.component.ts │ │ ├── categories.component.ts │ │ ├── component-page.component.ts │ │ ├── components-page.component.ts │ │ ├── components.component.ts │ │ ├── editor-page.component.ts │ │ ├── example-view.component.ts │ │ ├── examples-page │ │ │ ├── examples-page.component.html │ │ │ ├── examples-page.component.scss │ │ │ ├── examples-page.component.spec.ts │ │ │ └── examples-page.component.ts │ │ ├── guides-page.component.ts │ │ ├── home-page.component.ts │ │ ├── item-properties │ │ │ ├── item-properties.component.html │ │ │ ├── item-properties.component.scss │ │ │ ├── item-properties.component.spec.ts │ │ │ └── item-properties.component.ts │ │ ├── nav.component.ts │ │ ├── page-header.component.ts │ │ ├── side-bar │ │ │ ├── side-bar.component.html │ │ │ ├── side-bar.component.scss │ │ │ ├── side-bar.component.spec.ts │ │ │ └── side-bar.component.ts │ │ └── sidenav-layout.component.ts │ ├── directives │ │ └── json-formatter.directive.ts │ ├── examples │ │ ├── contact-us.form.config.ts │ │ └── examples.config.ts │ └── utils.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.dev.json ├── tsconfig.fix.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.e2e.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/ng-cli-compat", 20 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on", 21 | "plugin:@angular-eslint/template/process-inline-templates" 22 | ], 23 | "rules": { 24 | "@typescript-eslint/explicit-member-accessibility": [ 25 | "off", 26 | { 27 | "accessibility": "explicit" 28 | } 29 | ], 30 | "arrow-parens": [ 31 | "off", 32 | "always" 33 | ], 34 | "import/order": "off", 35 | "@typescript-eslint/member-ordering": "off", 36 | "prefer-arrow/prefer-arrow-functions": "off", 37 | "@angular-eslint/component-selector": "off", 38 | "@angular-eslint/directive-selector": "off" 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | package-lock.json 14 | 15 | # profiling files 16 | chrome-profiler-events.json 17 | speed-measure-plugin.json 18 | 19 | # IDEs and editors 20 | /.idea 21 | .project 22 | .classpath 23 | .c9/ 24 | *.launch 25 | .settings/ 26 | *.sublime-workspace 27 | 28 | # IDE - VSCode 29 | .vscode/* 30 | !.vscode/settings.json 31 | !.vscode/tasks.json 32 | !.vscode/launch.json 33 | !.vscode/extensions.json 34 | .history/* 35 | 36 | # misc 37 | /.angular/cache 38 | /.sass-cache 39 | /connect.lock 40 | /coverage 41 | /libpeerconnection.log 42 | npm-debug.log 43 | yarn-error.log 44 | testem.log 45 | /typings 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | dist: trusty 5 | sudo: required 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | before_script: 12 | - npm install -g @angular/cli 13 | 14 | script: 15 | - ng lint 16 | - ng test core --watch=false --browsers=ChromeHeadless 17 | - ng build core 18 | - ng test material --watch=false --browsers=ChromeHeadless 19 | - ng build material 20 | - ng build bootstrap 21 | - ng test tools --watch=false --browsers=ChromeHeadless 22 | - ng build tools 23 | - npm run build:gh 24 | - cp dist/ngx-dynamic-components/index.html dist/ngx-dynamic-components/404.html 25 | 26 | deploy: 27 | provider: pages 28 | skip_cleanup: true 29 | github_token: $GITHUB_TOKEN 30 | local_dir: dist/ngx-dynamic-components 31 | on: 32 | branch: master 33 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 FalconSoft Ltd 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 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 12 | not safari 15.2-15.3 -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to ngx-dynamic-components!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element, promise } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): promise.Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): promise.Promise { 9 | return element(by.css('dc-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-dynamic-components", 3 | "version": "15.1.2", 4 | "description": "NGX Dynamic Components is a configuration based dynamic components library for Angular. That allows you to rapidly create dynamic forms or any other mobile-friendly web layouts.", 5 | "author": "FalconSoft Ltd", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/FalconSoft/ngx-dynamic-components" 9 | }, 10 | "homepage": "https://falconsoft.github.io/ngx-dynamic-components/", 11 | "keywords": [], 12 | "license": "MIT", 13 | "scripts": { 14 | "setup": "npm i && npm run build:libs", 15 | "ng": "ng", 16 | "start": "ng serve --configuration=development", 17 | "build": "ng build", 18 | "build:libs": "ng build core --configuration production && ng build bootstrap --configuration production && ng build tools --configuration production", 19 | "public:publish": "cd dist/@ngx-dynamic-components/core && npm publish && cd ../bootstrap && npm publish && cd ../tools && npm publish && cd ../../../", 20 | "build:publish": "npm run build:libs && npm run public:publish", 21 | "build:gh": "ng build --configuration production --base-href=/ngx-dynamic-components/", 22 | "deploy:gh": "npm run build:libs && npm run build:gh && npx ngh --dir=dist/ngx-dynamic-components/", 23 | "test": "ng test", 24 | "lint": "ng lint", 25 | "e2e": "ng e2e", 26 | "prepare": "husky install", 27 | "postinstall": "ngcc", 28 | "fix": "ng serve --configuration=fix" 29 | }, 30 | "dependencies": { 31 | "@angular/animations": "^15.1.1", 32 | "@angular/cdk": "^15.1.0", 33 | "@angular/common": "^15.1.1", 34 | "@angular/compiler": "^15.1.1", 35 | "@angular/core": "^15.1.1", 36 | "@angular/forms": "^15.1.1", 37 | "@angular/material": "^15.1.0", 38 | "@angular/platform-browser": "^15.1.1", 39 | "@angular/platform-browser-dynamic": "^15.1.1", 40 | "@angular/router": "^15.1.1", 41 | "@ng-select/ng-select": "^10.0.3", 42 | "ace-builds": "^1.14.0", 43 | "angular-resizable-element": "^7.0.2", 44 | "angular-split": "^14.1.0", 45 | "bootstrap": "^5.2.3", 46 | "core-js": "^3.19.3", 47 | "font-awesome": "^4.7.0", 48 | "isomorphic-xml2js": "0.1.3", 49 | "json-formatter-js": "^2.3.4", 50 | "jspython-interpreter": "^2.1.12", 51 | "ngx-bootstrap": "^10.2.0", 52 | "ngx-markdown": "^15.1.0", 53 | "rxjs": "^7.6.0", 54 | "tslib": "^2.4.1", 55 | "zone.js": "~0.12.0" 56 | }, 57 | "devDependencies": { 58 | "@angular-devkit/build-angular": "~15.1.2", 59 | "@angular-eslint/builder": "^15.1.0", 60 | "@angular-eslint/eslint-plugin": "^15.1.0", 61 | "@angular-eslint/eslint-plugin-template": "^15.1.0", 62 | "@angular-eslint/schematics": "15.1.0", 63 | "@angular-eslint/template-parser": "^15.1.0", 64 | "@angular/cli": "^15.1.2", 65 | "@angular/compiler-cli": "^15.1.1", 66 | "@angular/language-service": "^15.1.1", 67 | "@types/eslint": "^8.2.1", 68 | "@types/estree": "^0.0.50", 69 | "@types/jasmine": "~3.7.7", 70 | "@types/jasminewd2": "^2.0.9", 71 | "@types/node": "^12.19.11", 72 | "@typescript-eslint/eslint-plugin": "^5.48.1", 73 | "@typescript-eslint/parser": "^5.48.1", 74 | "angular-cli-ghpages": "^1.0.0", 75 | "codelyzer": "^6.0.0", 76 | "eslint": "^8.31.0", 77 | "eslint-plugin-import": "2.27.4", 78 | "eslint-plugin-jsdoc": "39.6.4", 79 | "eslint-plugin-prefer-arrow": "1.2.3", 80 | "husky": "^7.0.0", 81 | "jasmine-core": "~3.8.0", 82 | "jasmine-spec-reporter": "~5.0.0", 83 | "karma": "~6.3.4", 84 | "karma-chrome-launcher": "~3.1.0", 85 | "karma-coverage-istanbul-reporter": "^3.0.3", 86 | "karma-jasmine": "~4.0.1", 87 | "karma-jasmine-html-reporter": "^1.6.0", 88 | "ng-packagr": "^15.1.0", 89 | "protractor": "~7.0.0", 90 | "ts-node": "~7.0.0", 91 | "tslint": "~6.1.3", 92 | "typescript": "~4.8.4" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /projects/bootstrap/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": [ 4 | "!**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "projects/bootstrap/tsconfig.lib.json", 14 | "projects/bootstrap/tsconfig.spec.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "rules": { 19 | "@angular-eslint/component-selector": [ 20 | "error", 21 | { 22 | "type": "element", 23 | "prefix": "dc", 24 | "style": "kebab-case" 25 | } 26 | ], 27 | "@angular-eslint/directive-selector": [ 28 | "error", 29 | { 30 | "type": "attribute", 31 | "prefix": "dc", 32 | "style": "camelCase" 33 | } 34 | ], 35 | "@typescript-eslint/explicit-member-accessibility": [ 36 | "off", 37 | { 38 | "accessibility": "explicit" 39 | } 40 | ], 41 | "arrow-parens": [ 42 | "off", 43 | "always" 44 | ], 45 | "import/order": "off" 46 | } 47 | }, 48 | { 49 | "files": [ 50 | "*.html" 51 | ], 52 | "rules": {} 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /projects/bootstrap/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/bootstrap'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/bootstrap/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/@ngx-dynamic-components/bootstrap", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/bootstrap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngx-dynamic-components/bootstrap", 3 | "version": "15.1.2", 4 | "private": false, 5 | "description": "@ngx-dynamic-components/bootstrap is Angular 7+ library what contains a bootstrap interfaces to build a configuration driven web pages and workflows.", 6 | "author": "FalconSoft Ltd ", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/FalconSoft/ngx-dynamic-components" 11 | }, 12 | "homepage": "https://falconsoft.github.io/ngx-dynamic-components/", 13 | "keywords": [ 14 | "angular", 15 | "forms", 16 | "dynamic forms", 17 | "configurable forms", 18 | "json forms", 19 | "form library" 20 | ], 21 | "peerDependencies": { 22 | "@angular/animations": "^15.1.0", 23 | "@angular/common": "^15.1.0", 24 | "@angular/core": "^15.1.0", 25 | "@ngx-dynamic-components/core": ">=15.1.0", 26 | "bootstrap-scss": "^5.2.3", 27 | "ngx-bootstrap": "^10.2.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/bootstrap.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule} from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { DynamicComponentsCoreModule } from '@ngx-dynamic-components/core'; 6 | 7 | import { NgSelectModule } from '@ng-select/ng-select'; 8 | import { setTheme } from 'ngx-bootstrap/utils'; 9 | import { TabsModule } from 'ngx-bootstrap/tabs'; 10 | import { TypeaheadModule } from 'ngx-bootstrap/typeahead'; 11 | 12 | import { SelectComponent } from './components/select/select.component'; 13 | import { TabsComponent } from './components/tabs/tabs.component'; 14 | import { registerComponents } from './components/register'; 15 | import { InputTypeaheadComponent } from './components/input-typeahead/input-typeahead.component'; 16 | 17 | export const tabsModuleForRoot: ModuleWithProviders = TabsModule.forRoot(); 18 | 19 | @NgModule({ 20 | declarations: [SelectComponent, TabsComponent, InputTypeaheadComponent], 21 | imports: [ 22 | DynamicComponentsCoreModule, 23 | CommonModule, 24 | FormsModule, 25 | NgSelectModule, 26 | TypeaheadModule, 27 | tabsModuleForRoot 28 | ] 29 | }) 30 | export class DynamicComponentsBootstrapModule { 31 | constructor() { 32 | setTheme('bs4'); 33 | registerComponents(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export { SelectComponent } from './select/select.component'; 2 | export { TabsComponent } from './tabs/tabs.component'; 3 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/components/input-typeahead/input-typeahead.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | 4 | import { SelectComponent, example } from './select.component'; 5 | import { UIModel } from '../../models'; 6 | 7 | describe('SelectComponent', () => { 8 | let component: SelectComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ SelectComponent ], 14 | imports: [ FormsModule ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(SelectComponent); 21 | component = fixture.componentInstance; 22 | component.uiModel = example.uiModel as UIModel; 23 | component.dataModel = example.dataModel; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/components/input-typeahead/input-typeahead.examples.ts: -------------------------------------------------------------------------------- 1 | import { UIModel, ComponentExample } from '@ngx-dynamic-components/core'; 2 | import { InputTypeaheadProperties } from './input-typeahead.component'; 3 | 4 | export const example2: ComponentExample> = { 5 | uiModel: ` 6 |
7 |
8 | 9 | 10 |
11 |
12 | `, 13 | dataModel: { 14 | list: [{ label: 'Data 1', value: 'd1' }, { label: 'Data 2', value: 'd2' }] 15 | }, 16 | title: 'Dropdown options binded to dataModel', 17 | }; 18 | 19 | export const example3: ComponentExample> = { 20 | uiModel: ` 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | `, 31 | dataModel: {}, 32 | title: 'Static dropdown declaration', 33 | }; 34 | 35 | export const example1: ComponentExample> = { 36 | uiModel: ` 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | `, 51 | dataModel: {}, 52 | scripts: ` 53 | # Evaluated with JSPython https://jspython.dev/ 54 | 55 | def countryChanged(): 56 | dataModel.city = null 57 | if dataModel.country == null: 58 | dataModel.cities = [] 59 | if dataModel.country == "uk": 60 | dataModel.cities = [{label: "London", value: "lon"}, {label: "Liverpool", value: "liv"}] 61 | if dataModel.country == "ua": 62 | dataModel.cities = [{label: "Kyiv", value: "kyiv"}, {label: "Lviv", value: "lvi"}] 63 | `, 64 | title: 'Dependency between dropdowns', 65 | description: 'Handles change event from first dropdown and changes values in second.' 66 | }; 67 | 68 | export const example4: ComponentExample> = { 69 | uiModel: ` 70 |
71 |
72 | 73 | 78 | 79 |
80 | 81 | 82 |
83 | `, 84 | dataModel: {}, 85 | scripts: ` 86 | async def onTypeahead(): 87 | if searchTerm != null: 88 | # can be server-side call 89 | return [ 90 | {label: searchTerm + ' 1', value: searchTerm + ' 1'}, 91 | {label: searchTerm + ' 2', value: searchTerm + ' 2'} 92 | ] 93 | 94 | async def onChange(): 95 | print('Selected value: ', dataModel.selected) 96 | `, 97 | title: 'drop-down with typeahead and debounceTime', 98 | }; 99 | 100 | export default [example4, example1, example2]; 101 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/components/register.ts: -------------------------------------------------------------------------------- 1 | import { CoreService, ComponentDescriptor } from '@ngx-dynamic-components/core'; 2 | 3 | import { selectDescriptor } from './select/select.component'; 4 | import { tabsDescriptor } from './tabs/tabs.component'; 5 | import { packageName } from '../constants'; 6 | import { inputTypeaheadDescriptor } from './input-typeahead/input-typeahead.component'; 7 | 8 | export const COMPONENTS_LIST: ComponentDescriptor[] = [ 9 | tabsDescriptor, 10 | selectDescriptor, 11 | inputTypeaheadDescriptor 12 | ]; 13 | 14 | // Register components. 15 | export function registerComponents(): void { 16 | COMPONENTS_LIST.forEach(component => CoreService.registerComponent(component)); 17 | } 18 | 19 | export function getCategories(): { name: string; packageName: string; components: ComponentDescriptor[] }[] { 20 | const categories = COMPONENTS_LIST.reduce((map, desc) => { 21 | map[desc.category] = map[desc.category] || []; 22 | map[desc.category].push(desc); 23 | return map; 24 | }, {}); 25 | 26 | return Object.entries(categories).map(([key, val]: [string, ComponentDescriptor[]]) => ({ 27 | name: key, 28 | components: val, 29 | packageName 30 | })); 31 | } 32 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/components/select/select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | 4 | import { SelectComponent, example } from './select.component'; 5 | import { UIModel } from '../../models'; 6 | 7 | describe('SelectComponent', () => { 8 | let component: SelectComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ SelectComponent ], 14 | imports: [ FormsModule ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(SelectComponent); 21 | component = fixture.componentInstance; 22 | component.uiModel = example.uiModel as UIModel; 23 | component.dataModel = example.dataModel; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/components/select/select.examples.ts: -------------------------------------------------------------------------------- 1 | import { UIModel, ComponentExample } from '@ngx-dynamic-components/core'; 2 | import { SelectProperties } from './select.component'; 3 | 4 | export const example1: ComponentExample> = { 5 | uiModel: ` 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | `, 16 | dataModel: {}, 17 | title: 'Static dropdown declaration', 18 | }; 19 | 20 | export const example2: ComponentExample> = { 21 | uiModel: ` 22 |
23 |
24 | 25 | 26 |
27 |
28 | `, 29 | dataModel: { 30 | list: [{ label: 'Data 1', value: 'd1' }, { label: 'Data 2', value: 'd2' }] 31 | }, 32 | title: 'Dropdown options binded to dataModel', 33 | }; 34 | 35 | export const example3: ComponentExample> = { 36 | uiModel: ` 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | `, 51 | dataModel: {}, 52 | scripts: ` 53 | # Evaluated with JSPython https://jspython.dev/ 54 | 55 | def countryChanged(): 56 | dataModel.city = null 57 | if dataModel.country == null: 58 | dataModel.cities = [] 59 | if dataModel.country == "uk": 60 | dataModel.cities = [{label: "London", value: "lon"}, {label: "Liverpool", value: "liv"}] 61 | if dataModel.country == "ua": 62 | dataModel.cities = [{label: "Kyiv", value: "kyiv"}, {label: "Lviv", value: "lvi"}] 63 | `, 64 | title: 'Dependency between dropdowns', 65 | description: 'Handles change event from first dropdown and changes values in second.' 66 | }; 67 | 68 | export const example4: ComponentExample> = { 69 | uiModel: ` 70 |
71 |
72 | 73 | 78 | 79 |
80 | 81 | 82 |
83 | `, 84 | dataModel: {}, 85 | scripts: ` 86 | async def onTypeahead(): 87 | if searchTerm != null: 88 | # can be server-side call 89 | return [ 90 | {label: searchTerm + ' 1', value: searchTerm + ' 1'}, 91 | {label: searchTerm + ' 2', value: searchTerm + ' 2'} 92 | ] 93 | 94 | async def onChange(): 95 | print('Selected value: ', dataModel.selected) 96 | `, 97 | title: 'drop-down with typeahead and debounceTime', 98 | }; 99 | 100 | 101 | export default [example1, example2, example4]; 102 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/components/tabs/tabs.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit, QueryList, ViewChildren, ViewContainerRef } from '@angular/core'; 2 | import { 3 | BaseUIComponent, CoreService, StyleProperties, UIModel, 4 | ComponentExample, ComponentDescriptor, Categories, XMLResult, toXMLResult, createComponent 5 | } from '@ngx-dynamic-components/core'; 6 | import { packageName } from '../../constants'; 7 | 8 | @Component({ 9 | selector: 'dc-tabs-ui', 10 | template: ` 11 | 12 | 13 | 14 | 15 | 16 | ` 17 | }) 18 | export class TabsComponent extends BaseUIComponent implements OnInit, AfterViewInit { 19 | @ViewChildren('vc', { read:ViewContainerRef }) vContainers: QueryList; 20 | 21 | async ngOnInit(): Promise { } 22 | 23 | async ngAfterViewInit() { 24 | this.vContainers.forEach((vc, i) => createComponent(this, this.uiModel.children[i], vc)); 25 | this.setHostStyles(); 26 | this.emitEvent(this.properties.onInit); 27 | } 28 | } 29 | 30 | export class TabsProperties extends StyleProperties { 31 | header: string; 32 | } 33 | 34 | export const example: ComponentExample> = { 35 | title: 'Tabs group example', 36 | uiModel: ` 37 | 38 | 39 | Tab 1 static text content 40 | 41 | 42 | Button content 43 | 44 |
45 |

ClickCount:

46 | $.clickCount 47 |
48 |
49 |
50 | `, 51 | scripts: ` 52 | def button1_Click(): 53 | print("button1_Click") 54 | if dataModel.clickCount == null: 55 | dataModel.clickCount = 0 56 | dataModel.clickCount = dataModel.clickCount + 1 57 | `, 58 | dataModel: { 59 | clickCount: 2 60 | } 61 | }; 62 | 63 | type TabsComponentConstrutor = new() => TabsComponent; 64 | 65 | type TabsPropertiesConstrutor = new() => TabsProperties; 66 | 67 | export const tabsDescriptor: ComponentDescriptor = { 68 | name: 'tab-container', 69 | label: 'Tabs container (Bootstrap)', 70 | packageName, 71 | category: Categories.Containers, 72 | description: 'Tabs component', 73 | itemProperties: TabsProperties, 74 | component: TabsComponent, 75 | example, 76 | parseUIModel(xmlData: XMLResult): UIModel { 77 | const children = xmlData.childNodes?.map(child => { 78 | if (child.$$?.length === 1) { 79 | return CoreService.getUIModel(toXMLResult(child.$$[0])); 80 | } 81 | const itemProperties = child.$; 82 | itemProperties.height = '100%'; 83 | itemProperties.width = '100%'; 84 | return { 85 | type: 'div', 86 | children: child.$$.map((r: any) => CoreService.getUIModel(toXMLResult(r))), 87 | itemProperties 88 | }; 89 | }); 90 | return { 91 | type: 'tab-container', 92 | children 93 | }; 94 | }, 95 | children: [{ 96 | tag: 'tab', 97 | hasChildren: true, 98 | properties: [{ 99 | name: 'header', 100 | label: 'Tab title' 101 | }] 102 | }], 103 | defaultModel: ` 104 | 105 | Tab 1 static text content 106 | 107 | 108 | Tab 2 static text content 109 | 110 | ` 111 | }; 112 | -------------------------------------------------------------------------------- /projects/bootstrap/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const packageName = 'bootstrap'; 2 | -------------------------------------------------------------------------------- /projects/bootstrap/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of bootstrap 3 | */ 4 | 5 | export * from './lib/bootstrap.module'; 6 | export * from './lib/components'; 7 | export { COMPONENTS_LIST, getCategories } from './lib/components/register'; 8 | -------------------------------------------------------------------------------- /projects/bootstrap/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js'; 5 | import 'zone.js/testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting(), { 18 | teardown: { destroyAfterEach: false } 19 | } 20 | ); 21 | // Then we find all the tests. 22 | const context = require.context('./', true, /\.spec\.ts$/); 23 | // And load the modules. 24 | context.keys().map(context); 25 | -------------------------------------------------------------------------------- /projects/bootstrap/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "target": "es2015", 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "types": [], 16 | "lib": [ 17 | "dom", 18 | "es2018" 19 | ] 20 | }, 21 | "angularCompilerOptions": { 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "enableResourceInlining": true 25 | }, 26 | "exclude": [ 27 | "src/test.ts", 28 | "**/*.spec.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /projects/bootstrap/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial", 8 | "enableIvy": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/bootstrap/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": [ 4 | "!**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "projects/core/tsconfig.lib.json", 14 | "projects/core/tsconfig.spec.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "rules": { 19 | "@angular-eslint/component-selector": [ 20 | "error", 21 | { 22 | "type": "element", 23 | "prefix": "dc", 24 | "style": "kebab-case" 25 | } 26 | ], 27 | "@angular-eslint/directive-selector": [ 28 | "error", 29 | { 30 | "type": "attribute", 31 | "prefix": "dc", 32 | "style": "camelCase" 33 | } 34 | ], 35 | "@typescript-eslint/explicit-member-accessibility": [ 36 | "off", 37 | { 38 | "accessibility": "explicit" 39 | } 40 | ], 41 | "arrow-parens": [ 42 | "off", 43 | "always" 44 | ], 45 | "import/order": "off", 46 | "jsdoc/newline-after-description": "off" 47 | } 48 | }, 49 | { 50 | "files": [ 51 | "*.html" 52 | ], 53 | "rules": {} 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /projects/core/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/core'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/core/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/@ngx-dynamic-components/core", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngx-dynamic-components/core", 3 | "version": "15.1.2", 4 | "private": false, 5 | "description": "@ngx-dynamic-components/core is Angular 7+ library what contains a core interfaces to build a configuration driven web pages and workflows.", 6 | "author": "FalconSoft Ltd ", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/FalconSoft/ngx-dynamic-components" 11 | }, 12 | "homepage": "https://falconsoft.github.io/ngx-dynamic-components/", 13 | "keywords": [ 14 | "angular", 15 | "forms", 16 | "dynamic forms", 17 | "configurable forms", 18 | "json forms", 19 | "form library" 20 | ], 21 | 22 | "peerDependencies": { 23 | "isomorphic-xml2js": "0.1.3", 24 | "@angular/common": "^15.1.0", 25 | "@angular/core": "^15.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/core/src/lib/components/base-dynamic-component.ts: -------------------------------------------------------------------------------- 1 | import { OnInit, EventEmitter, OnChanges, SimpleChanges, OnDestroy, Directive, Injector, Output, ViewContainerRef } from '@angular/core'; 2 | import { UIModel, ComponentEvent } from '../models'; 3 | import { getStringEventArgs, kebabToCamelCase, parseArgFunction, queryValue } from '../utils'; 4 | import { StyleProperties, StylePropertiesList, BaseProperties } from '../properties'; 5 | import { Observable, Subject } from 'rxjs'; 6 | 7 | @Directive() 8 | export abstract class BaseDynamicComponent implements OnInit, OnChanges, OnDestroy { // eslint-disable-line 9 | dataModel: any; 10 | uiModel: UIModel = { 11 | type: undefined, 12 | itemProperties: ({} as T) 13 | }; 14 | containerRef?: ViewContainerRef; 15 | abstract eventHandlers: EventEmitter; 16 | @Output() render = new EventEmitter(); 17 | changedDataModel = new EventEmitter(); 18 | element?: HTMLElement; 19 | protected eventResults = new Map(); 20 | protected eventResults$ = new Subject(); 21 | 22 | constructor(public injector?: Injector) { } 23 | 24 | async ngOnInit(): Promise { 25 | this.setHostStyles(); 26 | this.emitEvent((this.properties as BaseProperties)?.onInit); 27 | } 28 | 29 | abstract ngOnChanges(changes: SimpleChanges): Promise; 30 | 31 | abstract ngOnDestroy(): Promise; 32 | 33 | abstract create(element: HTMLElement): void; 34 | 35 | setEventHandlerResult(event: string, result: any): void { 36 | if (event) { 37 | this.eventResults.set(event, result); 38 | this.eventResults$.next(event); 39 | } 40 | }; 41 | 42 | get properties(): T { 43 | return this.uiModel.itemProperties; 44 | } 45 | 46 | get attrs(): T { 47 | return this.uiModel.itemProperties; 48 | } 49 | 50 | show(): void { 51 | if (this.element) { 52 | this.element.classList.remove('hidden'); 53 | } 54 | } 55 | hide(): void { 56 | if (this.element) { 57 | this.element.classList.add('hidden'); 58 | } 59 | } 60 | 61 | protected emitEvent(funcSign?: string, parameters: any = null, eventName?: string): void { 62 | if (funcSign) { 63 | const fParsed = parseArgFunction(funcSign); 64 | const eventHandler = fParsed[0]; 65 | let parameter = fParsed[1]; 66 | if (parameter?.startsWith('$') && parameters === null) { 67 | parameters = queryValue(this.dataModel, parameter); 68 | parameter = parameter.replace('\$.', '').replace(/\.\w/g, (matched) => matched.replace('.', '').toUpperCase()); 69 | } 70 | 71 | const eventParameters = { 72 | uiModel: this.uiModel, 73 | argsKey: parameter, 74 | argsValue: getStringEventArgs(funcSign) ?? parameters 75 | }; 76 | 77 | if (parameter) { 78 | eventParameters[parameter] = eventParameters.argsValue; 79 | } 80 | 81 | this.eventHandlers.emit({ 82 | eventHandler, 83 | eventName, 84 | parameters: eventParameters, 85 | sender: this 86 | }); 87 | } 88 | } 89 | 90 | protected setHostStyles(): void { 91 | if (this.element && this.properties) { 92 | 93 | const props = this.properties as StyleProperties; 94 | if (props.class) { 95 | this.element.className += ' ' + props.class; 96 | } 97 | 98 | props.style?.split(';').forEach(s => { 99 | if (this.element) { 100 | const [key, val] = s.split(':').map(v => v.trim()); 101 | this.element.style[kebabToCamelCase(key) as any] = val; 102 | } 103 | }); 104 | 105 | StylePropertiesList.forEach(b => { 106 | if (props && props.hasOwnProperty(b) && this.element) { 107 | const val = (props as any)[b]; 108 | this.element.style[b as any] = val; 109 | } 110 | }); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /projects/core/src/lib/components/base-html-element.ts: -------------------------------------------------------------------------------- 1 | import type { Injector, ViewContainerRef } from '@angular/core'; 2 | import { OnInit, OnDestroy, EventEmitter, SimpleChanges, OnChanges, Directive } from '@angular/core'; 3 | import { UIModel, ComponentEvent, XMLResult } from '../models'; 4 | import { StyleProperties, StylePropertiesList, BaseProperties, propDescription } from '../properties'; 5 | import { BaseDynamicComponent } from './base-dynamic-component'; 6 | import { createComponent } from '../utils/renderer'; 7 | 8 | @Directive() 9 | export class BaseHTMLElement extends BaseDynamicComponent implements OnInit, OnDestroy, OnChanges { // eslint-disable-line 10 | dataModel: any; 11 | uiModel: UIModel; 12 | eventHandlers = new EventEmitter(); 13 | changedDataModel = new EventEmitter(); 14 | element: HTMLElement; 15 | private parentNode: Node; 16 | 17 | async ngOnDestroy(): Promise { 18 | this.emitEvent((this.properties as BaseProperties).onDestroy); 19 | if (this.element) { 20 | this.element.parentNode.removeChild(this.element); 21 | } 22 | } 23 | 24 | async ngOnChanges({dataModel, uiModel}: SimpleChanges): Promise { 25 | if (dataModel && !dataModel.firstChange && this.dataModel !== dataModel.currentValue) { 26 | this.dataModel = dataModel.currentValue; 27 | } 28 | 29 | if (uiModel && !uiModel.firstChange) { 30 | this.uiModel = uiModel.currentValue; 31 | this.setProperties(); 32 | } 33 | } 34 | 35 | create(selectorElement: HTMLElement): void { 36 | const created = Boolean(this.element); 37 | this.element = this.element || document.createElement(this.uiModel.type); 38 | this.element.innerHTML = ''; 39 | this.parentNode = this.parentNode || selectorElement.parentNode; 40 | this.setProperties(); 41 | if (!created && this.parentNode) { 42 | this.parentNode.insertBefore(this.element, selectorElement); 43 | } 44 | this.uiModel.children?.forEach(uiModel => createComponent(this, uiModel)); 45 | } 46 | 47 | private setProperties(): void { 48 | const htmlProps = this.properties as HTMLProperties; 49 | if (htmlProps.content) { 50 | this.element.textContent = htmlProps.content; 51 | } 52 | if (htmlProps.title) { 53 | this.element.title = htmlProps.title; 54 | } 55 | this.setHostStyles(); 56 | } 57 | 58 | get properties(): T { 59 | return this.uiModel.itemProperties; 60 | } 61 | 62 | get attrs(): T { 63 | return this.uiModel.itemProperties; 64 | } 65 | } 66 | 67 | export class HTMLProperties extends StyleProperties { 68 | @propDescription({ 69 | description: 'HTML element text content', 70 | example: 'Text' 71 | }) 72 | content?: string; 73 | 74 | @propDescription({ 75 | description: 'HTML element html content', 76 | example: 'Text' 77 | }) 78 | htmlContent?: string; 79 | 80 | @propDescription({ 81 | description: 'HTML element title', 82 | example: 'Description text' 83 | }) 84 | title?: string; 85 | } 86 | 87 | export type HTMLPropertiesConstrutor = new () => HTMLProperties; 88 | 89 | export function parseHTMLUIModel(xmlRes: XMLResult): UIModel { 90 | const content = xmlRes.content; 91 | return { 92 | type: xmlRes.type, 93 | itemProperties: { content } 94 | }; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /projects/core/src/lib/components/ngx-dynamic-component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, EventEmitter, Output, ViewContainerRef, 2 | ViewChild, Injector, OnChanges, SimpleChanges } from '@angular/core'; 3 | import { animationFrameScheduler } from 'rxjs'; 4 | import { UIModel, ComponentEvent } from '../models'; 5 | import { CoreService } from '../services/core.service'; 6 | import { createComponent } from '../utils/renderer'; 7 | 8 | @Component({ 9 | selector: 'ngx-dynamic-component', // eslint-disable-line 10 | template: '' 11 | }) 12 | export class NGXDynamicComponent implements OnInit, OnChanges { 13 | 14 | @Input() dataModel: any; 15 | @Input() xmlUIModel: string; 16 | @Output() render = new EventEmitter(); 17 | @Output() changedDataModel = new EventEmitter(); 18 | @Output() eventHandlers = new EventEmitter(); 19 | @ViewChild('vc', {read: ViewContainerRef, static: true}) containerRef: ViewContainerRef; 20 | 21 | uiModel: UIModel; 22 | 23 | constructor(public injector?: Injector) { } 24 | 25 | ngOnInit() { 26 | this.initParsedModel(this.xmlUIModel); 27 | } 28 | 29 | ngOnChanges({xmlUIModel, dataModel}: SimpleChanges): void { 30 | if (xmlUIModel && !xmlUIModel.firstChange && xmlUIModel.currentValue !== xmlUIModel.previousValue) { 31 | this.initParsedModel(this.xmlUIModel); 32 | } 33 | } 34 | 35 | private initParsedModel(uiModel: string): void { 36 | try { 37 | this.uiModel = CoreService.parseXMLModel(uiModel); 38 | if (this.uiModel) { 39 | createComponent(this, this.uiModel); 40 | animationFrameScheduler.schedule(() => this.render.emit(this.uiModel)); 41 | } 42 | } catch (e) { 43 | this.uiModel = null; 44 | this.eventHandlers.emit({ 45 | eventHandler: 'parseError', 46 | eventName: 'error', 47 | parameters: { 48 | uiModel: this.uiModel, 49 | error: e, 50 | }}); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /projects/core/src/lib/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | 6 | import { NGXDynamicComponent } from './components/ngx-dynamic-component'; 7 | 8 | import { ContainerComponent } from './ui-components/container/container.component'; 9 | import { TextComponent } from './ui-components/text/text.component'; 10 | import { ButtonComponent } from './ui-components/button/button.component'; 11 | import { InputComponent } from './ui-components/input/input.component'; 12 | import { IconComponent } from './ui-components/icon/icon.component'; 13 | import { RadioGroupComponent } from './ui-components/radio-group/radio-group.component'; 14 | import { TextareaComponent } from './ui-components/textarea/textarea.component'; 15 | import { registerComponents } from './ui-components/register'; 16 | import { LinkComponent } from './ui-components/link/link.component'; 17 | import { LabelComponent } from './ui-components/label/label.component'; 18 | import { FormComponent } from './ui-components/form/form.component'; 19 | import { SelectComponent } from './ui-components/select/select.component'; 20 | import { RepeaterComponent } from './ui-components/repeater/repeater.component'; 21 | 22 | @NgModule({ 23 | declarations: [ 24 | ContainerComponent, 25 | FormComponent, 26 | RepeaterComponent, 27 | TextComponent, 28 | LabelComponent, 29 | ButtonComponent, 30 | InputComponent, 31 | IconComponent, 32 | RadioGroupComponent, 33 | TextareaComponent, 34 | LinkComponent, 35 | SelectComponent, 36 | NGXDynamicComponent], 37 | imports: [ 38 | CommonModule, 39 | FormsModule, 40 | HttpClientModule 41 | ], 42 | exports: [NGXDynamicComponent, TextComponent] 43 | }) 44 | export class DynamicComponentsCoreModule { 45 | constructor() { 46 | registerComponents(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /projects/core/src/lib/html-elements/index.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDescriptor, Categories, BaseHTMLElementConstructor } from '../models'; 2 | import { BaseHTMLElement, HTMLProperties, HTMLPropertiesConstrutor, parseHTMLUIModel } from '../components/base-html-element'; 3 | 4 | const tags = ['span', 'p', 'i', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'nav', 'small']; 5 | 6 | const htmlElementDescriptor: ComponentDescriptor = { 7 | name: 'HTML_ELEMENT_TAG', 8 | label: 'HTML_ELEMENT_TAG Element', 9 | packageName: 'core', 10 | category: Categories.HTML, 11 | description: 'HTML HTML_ELEMENT_TAG element', 12 | itemProperties: HTMLProperties, 13 | component: BaseHTMLElement, 14 | example: { 15 | title: 'HTML example', 16 | uiModel: ` 17 |
18 | Text 19 | 20 |
21 | `, 22 | dataModel: {} 23 | }, 24 | parseUIModel: parseHTMLUIModel, 25 | defaultModel: '' 26 | }; 27 | 28 | export const htmlDescriptors: ComponentDescriptor[] = 29 | tags.map( 30 | tag => replaceStringValues>(htmlElementDescriptor, tag) 31 | ); 32 | 33 | function replaceStringValues>(target: T, tag: string): T { 34 | const res = {} as T; 35 | return Object.entries(target).reduce((obj, [key, value]) => { 36 | obj[key] = value; 37 | if (typeof value === 'string') { 38 | obj[key] = value.replace(/HTML_ELEMENT_TAG/g, tag); 39 | } 40 | 41 | if (key === 'example') { 42 | obj[key] = replaceStringValues(obj[key], tag); 43 | } 44 | 45 | return obj; 46 | }, res); 47 | } 48 | -------------------------------------------------------------------------------- /projects/core/src/lib/html-elements/li/li.element.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDescriptor, Categories, BaseHTMLElementConstructor } from '../../models'; 2 | import { BaseHTMLElement, HTMLProperties, HTMLPropertiesConstrutor, parseHTMLUIModel } from '../../components/base-html-element'; 3 | 4 | export const liDescriptor: ComponentDescriptor = { 5 | name: 'li', 6 | label: 'LI Element', 7 | packageName: 'core', 8 | category: Categories.HTML, 9 | description: 'HTML LI element', 10 | itemProperties: HTMLProperties, 11 | component: BaseHTMLElement, 12 | example: { 13 | title: 'HTML
  • example', 14 | uiModel: `
    15 |
  • Text
  • 16 |
  • Text in Span
  • 17 |
  • Google
  • 18 |
  • 19 |
    `, 20 | dataModel: {} 21 | }, 22 | parseUIModel: parseHTMLUIModel, 23 | defaultModel: '
  • ' 24 | }; 25 | -------------------------------------------------------------------------------- /projects/core/src/lib/html-elements/ol/ol.element.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDescriptor, Categories, BaseHTMLElementConstructor } from '../../models'; 2 | import { BaseHTMLElement, HTMLProperties, HTMLPropertiesConstrutor, parseHTMLUIModel } from '../../components/base-html-element'; 3 | 4 | export const olDescriptor: ComponentDescriptor = { 5 | name: 'ol', 6 | label: 'OL Element', 7 | packageName: 'core', 8 | category: Categories.HTML, 9 | description: 'HTML OL element', 10 | itemProperties: HTMLProperties, 11 | component: BaseHTMLElement, 12 | example: { 13 | title: 'HTML
      example', 14 | uiModel: ` 15 | 16 | 23 | 24 | `, 25 | dataModel: { 26 | appInfo: { 27 | ownerName: 'TestUser', 28 | appName: 'LongAppName' 29 | } 30 | } 31 | }, 32 | parseUIModel: parseHTMLUIModel, 33 | defaultModel: '
        ' 34 | }; 35 | -------------------------------------------------------------------------------- /projects/core/src/lib/html-elements/ul/ul.element.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDescriptor, Categories, BaseHTMLElementConstructor } from '../../models'; 2 | import { BaseHTMLElement, HTMLProperties, HTMLPropertiesConstrutor, parseHTMLUIModel } from '../../components/base-html-element'; 3 | 4 | export const ulDescriptor: ComponentDescriptor = { 5 | name: 'ul', 6 | label: 'UL Element', 7 | packageName: 'core', 8 | category: Categories.HTML, 9 | description: 'HTML UL element', 10 | itemProperties: HTMLProperties, 11 | component: BaseHTMLElement, 12 | example: { 13 | title: 'HTML
          example', 14 | uiModel: `
          15 |
            16 |
          • item-1
          • 17 |
          • item-2
          • 18 |
          • item-3
          • 19 |
          20 |
          `, 21 | dataModel: {} 22 | }, 23 | parseUIModel: parseHTMLUIModel, 24 | defaultModel: '
            ' 25 | }; 26 | -------------------------------------------------------------------------------- /projects/core/src/lib/models.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // import { BaseUIComponentConstructor, BaseHTMLElementConstructor } from './utils'; 3 | import { ComponentProperty } from './properties'; 4 | import { BaseDynamicComponent } from './components/base-dynamic-component'; 5 | import { BaseHTMLElement } from './components/base-html-element'; 6 | import { BaseUIComponent } from './components/base-ui-component'; 7 | 8 | type UIAction = (sender: UIModel, dataModel: any, uiModel: UIModel) => void; 9 | 10 | export type BaseUIComponentConstructor = new () => BaseUIComponent; 11 | export type BaseHTMLElementConstructor = new () => BaseHTMLElement; 12 | 13 | export interface AttributesMap { 14 | width?: string; 15 | height?: string; 16 | margin?: string; 17 | padding?: string; 18 | label?: string; 19 | [key: string]: any; 20 | } 21 | 22 | export interface ComponentExample { 23 | uiModel: T|string; 24 | dataModel: any; 25 | title: string; 26 | scripts?: string; 27 | description?: string; 28 | } 29 | 30 | type AttributesMapConstructor = new () => AttributesMap; 31 | 32 | export interface ChildComponent { 33 | tag: string; 34 | properties: ComponentProperty[]; 35 | hasChildren?: boolean; 36 | } 37 | 38 | export interface ComponentDescriptor { 40 | packageName: string; 41 | label: string; 42 | category: string; 43 | name: string; 44 | description: string; 45 | itemProperties: PropertiesType; 46 | component: ComponentType; 47 | example?: ComponentExample | ComponentExample[]; 48 | defaultModel?: UIModel | string; 49 | propertiesDescriptor?: Array<[string, ComponentProperty]>; 50 | parseUIModel?: (xmlData: XMLResult) => UIModel; 51 | children?: ChildComponent[] | false; 52 | } 53 | 54 | export abstract class UIModel { 55 | id?: string; 56 | type: string; 57 | itemProperties?: T; 58 | children?: UIModel[]; 59 | getComponent?(): BaseDynamicComponent { 60 | return null; 61 | } 62 | } 63 | 64 | export interface SelectedComponent { 65 | uiModel: UIModel; 66 | cssPath: string; 67 | } 68 | 69 | export interface OptionValue { 70 | label: string; 71 | value: any; 72 | } 73 | 74 | // eslint-disable-next-line no-shadow 75 | export const enum Categories { 76 | Basic = 'Basic', 77 | HTML = 'HTML', 78 | Containers = 'Containers', 79 | Data = 'Data', 80 | CustomControls = 'Custom Controls', 81 | DataTable = 'Data table' 82 | } 83 | 84 | // eslint-disable-next-line no-shadow 85 | export enum DesignerVisibility { 86 | // eslint-disable-next-line 87 | UIModel = 1, 88 | Scripts = 2, 89 | All = 3 90 | } 91 | 92 | export interface XMLResult { 93 | type: string; 94 | attrs: AttributesMap; 95 | content: string; 96 | childNodes: any[]; 97 | } 98 | 99 | export interface ComponentEvent { 100 | eventName: string; 101 | eventHandler: string; 102 | rootUIModel?: UIModel; 103 | parameters?: { 104 | uiModel?: UIModel; 105 | argsKey?: string; 106 | argsValue?: any; 107 | [key: string]: any; 108 | }; 109 | sender?: BaseDynamicComponent; 110 | } 111 | -------------------------------------------------------------------------------- /projects/core/src/lib/properties/README.md: -------------------------------------------------------------------------------- 1 | # ngx-dynamic-components properties 2 | 3 | This section about descriptors for dynamic component properties. 4 | 5 | ## Common properties. 6 | 7 | File `./descriptor.ts` contains maps `ContainerControlProperties` and `ControlProperties` with common properties (`fxFlex`, `width`, `height`, ...) that are often used in different components. 8 | 9 | ## Custom properties. 10 | 11 | Also custom components registered outside @ngx-dynamic-components package can have it's own specific properties that are automatically are registered in map `ControlProperties`. Details file `../services/core.service.ts`. 12 | 13 | For that purpose use property `propertiesDescriptor` of class `ComponentDescriptor`. Details file `../models.ts`. 14 | 15 | ## Example of property descriptor. 16 | 17 | Type of `[string, ComponentProperty]` 18 | ``` 19 | ['labelPosition', { 20 | name: 'labelPosition', label: 'Label Position', category: PropertyCategories.Main, 21 | combo: [['left', 'top', 'right', 'bottom']] 22 | }] 23 | ``` 24 | -------------------------------------------------------------------------------- /projects/core/src/lib/properties/descriptor.ts: -------------------------------------------------------------------------------- 1 | import { PropDescriptor } from './models'; 2 | 3 | export function propDescription(description: PropDescriptor): (target: any, key: string) => void { 4 | function decorate(target: any, key: string): void { 5 | let properties = target.hasOwnProperty('properties') ? target.properties : []; 6 | 7 | const newProp = {name: key, ...description}; 8 | const existIndex = properties.findIndex(p => p.name === key); 9 | 10 | if (existIndex === -1) { 11 | properties.push(newProp); 12 | } else { 13 | properties.splice(existIndex, 1, newProp); 14 | } 15 | 16 | let proto = Object.getPrototypeOf(target); 17 | 18 | while (proto.hasOwnProperty('properties')) { 19 | // Filter overridden properties. 20 | const protoProps = proto.properties.filter(protoP => !properties.map(p => p.name).includes(protoP.name)); 21 | properties = properties.concat(protoProps); 22 | proto = Object.getPrototypeOf(proto); 23 | } 24 | 25 | target.properties = properties.sort((a: any, b: any) => { 26 | if (a.name < b.name) { 27 | return -1; 28 | } 29 | if (a.name > b.name) { 30 | return 1; 31 | } 32 | return 0; 33 | }); 34 | } 35 | return decorate; 36 | } 37 | -------------------------------------------------------------------------------- /projects/core/src/lib/properties/index.ts: -------------------------------------------------------------------------------- 1 | export * from './descriptor'; 2 | export * from './models'; 3 | export * from './maps'; 4 | -------------------------------------------------------------------------------- /projects/core/src/lib/properties/maps.ts: -------------------------------------------------------------------------------- 1 | import { ComponentProperty, PropertyCategories } from './models'; 2 | 3 | // background, color, font 4 | /** 5 | * Item properties can be used in any component. 6 | */ 7 | export const controlProperties = new Map([ 8 | // General Component properties 9 | ['width', { name: 'width', label: 'Width', category: PropertyCategories.Layout }], 10 | ['height', { name: 'height', label: 'Height', category: PropertyCategories.Layout }], 11 | ['min-width', { name: 'min-width', label: 'Min width', category: PropertyCategories.Layout }], 12 | ['min-height', { name: 'min-height', label: 'Min height', category: PropertyCategories.Layout }], 13 | ['margin', { name: 'margin', label: 'Margin', category: PropertyCategories.Layout }], 14 | ['padding', { name: 'padding', label: 'Padding', category: PropertyCategories.Layout }], 15 | 16 | // Field properties 17 | ['label', { name: 'label', label: 'Label', category: PropertyCategories.Main }], 18 | ['labelPosition', { 19 | name: 'labelPosition', label: 'Label Position', category: PropertyCategories.Main, 20 | values: ['left', 'top', 'right', 'bottom'] 21 | }], 22 | ['labelWidth', { name: 'labelWidth', label: 'Label Width', category: PropertyCategories.Layout }], 23 | 24 | // Appearence 25 | ['background', { name: 'background', label: 'Background', category: PropertyCategories.Appearance }], 26 | ['color', { name: 'color', label: 'Color', category: PropertyCategories.Appearance }], 27 | ['font-weight', { 28 | name: 'font-weight', label: 'Font weight', category: PropertyCategories.Appearance, 29 | values: ['bold', 'bolder', 'lighter', 100, 200, 300, 400, 500, 600, 700, 800, 900] 30 | }], 31 | ['font-size', { 32 | name: 'font-size', label: 'Font size', category: PropertyCategories.Appearance, 33 | values: ['large', 'larger', 'medium', 'small', 'smaller', 'x-large', 'xx-large', 'x-small', 'xx-small'] 34 | }], 35 | ['font-style', { 36 | name: 'font-style', label: 'Font style', category: PropertyCategories.Appearance, 37 | values: ['italic', 'oblique', 'normal'] 38 | }], 39 | ['border', { 40 | name: 'border', label: 'Border', category: PropertyCategories.Appearance, 41 | combo: [[{ label: 'all', value: 'border' }, { label: 'top', value: 'border-top' }, { label: 'left', value: 'border-left' }, 42 | { label: 'right', value: 'border-right' }, { label: 'bottom', value: 'border-bottom' }], 'border-value'] 43 | }], 44 | 45 | // Validation properties 46 | ['required', {name: 'required', label: 'Required', category: PropertyCategories.Validation, 47 | values: ['true', 'false']}], 48 | ['minlength', {name: 'minlength', label: 'Min length', category: PropertyCategories.Validation}], 49 | ['maxlength', {name: 'maxlength', label: 'Max length', category: PropertyCategories.Validation}], 50 | ['pattern', {name: 'pattern', label: 'Pattern', category: PropertyCategories.Validation}] 51 | ]); 52 | -------------------------------------------------------------------------------- /projects/core/src/lib/services/core.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CoreService } from './core.service'; 4 | 5 | describe('CoreService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: CoreService = TestBed.inject(CoreService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/core/src/lib/services/core.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ComponentDescriptor, UIModel, XMLResult, BaseUIComponentConstructor, BaseHTMLElementConstructor } from '../models'; 3 | import { toXMLResult, parseXmlString } from '../utils'; 4 | import { controlProperties, UIModelProperty } from '../properties'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class CoreService { 10 | // eslint-disable-next-line @typescript-eslint/naming-convention 11 | private static readonly COMPONENTS_REGISTER = new Map(); 12 | 13 | public static registerComponent(desc: ComponentDescriptor): void { 14 | const { name, packageName, propertiesDescriptor } = desc; 15 | // @deprecated 16 | if (propertiesDescriptor) { 17 | propertiesDescriptor.forEach(prop => { 18 | controlProperties.set(`${name}:${prop[0]}`, prop[1]); 19 | }); 20 | } 21 | CoreService.COMPONENTS_REGISTER.set(name, desc); 22 | } 23 | 24 | public static getComponent(type: string): BaseUIComponentConstructor | BaseHTMLElementConstructor { 25 | if (CoreService.COMPONENTS_REGISTER.has(type)) { 26 | return CoreService.COMPONENTS_REGISTER.get(type).component; 27 | } 28 | 29 | throw new Error(`Component ${type} is not registered`); 30 | } 31 | 32 | public static getComponentDescriptor(type: string): ComponentDescriptor { 33 | if (CoreService.COMPONENTS_REGISTER.has(type)) { 34 | return CoreService.COMPONENTS_REGISTER.get(type); 35 | } 36 | 37 | throw new Error(`ComponentDescriptor ${type} is not registered`); 38 | } 39 | 40 | public static getComponentProperties(type: string): UIModelProperty[] { 41 | try { 42 | const desc = CoreService.getComponentDescriptor(type); 43 | return desc.itemProperties.prototype.properties; 44 | } catch (e) { 45 | throw e; 46 | } 47 | } 48 | 49 | public static getListOfComponents(): Array { 50 | return Array.from(CoreService.COMPONENTS_REGISTER.values()); 51 | } 52 | 53 | public static parseXMLModel(uiModelXml: string): UIModel { 54 | if (!uiModelXml) { 55 | return null; 56 | } 57 | const res = parseXmlString(uiModelXml); 58 | 59 | if (res) { 60 | const type = Object.keys(res)[0]; 61 | const xmlObj = res[type]; 62 | if(typeof xmlObj === 'string') 63 | { 64 | throw Error(`Invalid XML, please make sure file can't start with comment `); 65 | } 66 | xmlObj['#name'] = type; 67 | return CoreService.getUIModel(toXMLResult(xmlObj)); 68 | } 69 | } 70 | 71 | public static getUIModel(xmlRes: XMLResult): UIModel { 72 | const itemProperties = xmlRes.attrs; 73 | const type = xmlRes.type; 74 | 75 | if (itemProperties.disabled === 'true') { 76 | itemProperties.disabled = true; 77 | } else if (itemProperties.hasOwnProperty('disabled')) { 78 | itemProperties.disabled = null; 79 | } 80 | 81 | if (CoreService.COMPONENTS_REGISTER.has(type)) { 82 | const uiModel: UIModel = { 83 | type, 84 | itemProperties 85 | }; 86 | 87 | if (itemProperties.id) { 88 | uiModel.id = itemProperties.id; 89 | } 90 | try { 91 | const descr = CoreService.COMPONENTS_REGISTER.get(type); 92 | if (typeof descr.parseUIModel === 'function') { 93 | const typeUIModel = descr.parseUIModel(xmlRes); 94 | uiModel.itemProperties = { ...itemProperties, ...typeUIModel.itemProperties }; 95 | if (typeUIModel.children) { 96 | uiModel.children = typeUIModel.children; 97 | } 98 | } 99 | } catch(e) { 100 | console.error(e); 101 | throw new Error(`Error parsing <${type} ..> : ${e}`); 102 | } 103 | if (xmlRes.childNodes && !uiModel.children) { 104 | uiModel.children = xmlRes.childNodes 105 | .filter(n => n['#name'] !== '#comment') 106 | .map((r: any) => CoreService.getUIModel(toXMLResult(r))); 107 | } 108 | return uiModel; 109 | } else { 110 | throw Error(`No component for tag ${type}`); 111 | } 112 | 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /projects/core/src/lib/styles/accordion.scss: -------------------------------------------------------------------------------- 1 | accordion-group ::ng-deep { 2 | .panel.card { 3 | border: none; 4 | margin-bottom: 0; 5 | } 6 | 7 | .panel-heading { 8 | padding: .25rem; 9 | cursor: pointer; 10 | } 11 | 12 | .panel-body.card-block.card-body { 13 | padding: 0; 14 | } 15 | 16 | [aria-expanded="false"] .fa-chevron-down { 17 | display: none; 18 | } 19 | 20 | [aria-expanded="true"] .fa-chevron-right { 21 | display: none; 22 | } 23 | } 24 | 25 | .fa-chevron-right { 26 | width: 14px; 27 | } 28 | -------------------------------------------------------------------------------- /projects/core/src/lib/styles/label.scss: -------------------------------------------------------------------------------- 1 | label.right, label.bottom { 2 | order: 1; 3 | } 4 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/button/button.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { ButtonComponent, example } from './button.component'; 4 | 5 | describe('ButtonComponent', () => { 6 | let component: ButtonComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ButtonComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ButtonComponent); 18 | component = fixture.componentInstance; 19 | component.uiModel = example.uiModel; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/button/button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit, HostListener } from '@angular/core'; 2 | import { BaseUIComponent } from '../../components/base-ui-component'; 3 | import { StyleProperties, propDescription, PropertyCategories, PropTypes } from '../../properties'; 4 | import { ComponentExample, UIModel, ComponentDescriptor, Categories, AttributesMap, XMLResult } from '../../models'; 5 | 6 | @Component({ 7 | selector: 'button', // eslint-disable-line 8 | template: ` 9 | 10 | {{properties.label}} 11 | ` 12 | }) 13 | export class ButtonComponent extends BaseUIComponent implements OnInit { 14 | 15 | @HostBinding('attr.type') type = 'button'; 16 | @HostBinding('attr.disabled') disabled = null; 17 | @HostListener('click', ['$event']) 18 | onClick(evt: Event): void { 19 | this.emitEvent(this.properties.onClick, evt); 20 | this.changedDataModel.emit(this.dataModel); 21 | } 22 | 23 | ngOnInit(): Promise { 24 | if (this.properties.type) { 25 | this.type = this.properties.type; 26 | } 27 | 28 | if (this.properties.disabled === true) { 29 | this.disabled = true; 30 | } 31 | 32 | return super.ngOnInit(); 33 | } 34 | } 35 | 36 | export class ButtonProperties extends StyleProperties { 37 | @propDescription({ 38 | description: 'Button label', 39 | example: 'Click me!', 40 | }) 41 | label: string; 42 | 43 | @propDescription({ 44 | description: 'Key for action that fires onclick', 45 | example: 'onBtnClick()', 46 | type: PropTypes.EVENT 47 | }) 48 | onClick?: string; 49 | 50 | @propDescription({ 51 | description: 'Button type: button|submit|reset|link. Default: button', 52 | example: 'submit', 53 | defaultValue: 'button' 54 | }) 55 | type?: string; 56 | 57 | @propDescription({ 58 | description: 'It specifies that the button should be disabled.', 59 | example: 'true', 60 | defaultValue: 'false' 61 | }) 62 | disabled?: boolean; 63 | } 64 | 65 | export const example: ComponentExample> = { 66 | title: 'Basic button example', 67 | uiModel: ` 68 |
            69 | 70 | 71 | 72 | 73 |
            74 | `, 75 | scripts: ` 76 | def consoleLog(): 77 | print("test") 78 | `, 79 | dataModel: {} 80 | }; 81 | 82 | type ButtonComponentConstrutor = new () => ButtonComponent; 83 | 84 | type ButtonPropertiesConstrutor = new () => ButtonProperties; 85 | 86 | export const buttonDescriptor: ComponentDescriptor = { 87 | name: 'button', 88 | label: 'Button', 89 | packageName: 'core', 90 | category: Categories.Basic, 91 | description: 'Button component', 92 | itemProperties: ButtonProperties, 93 | component: ButtonComponent, 94 | example, 95 | parseUIModel(xmlRes: XMLResult): UIModel { 96 | const content = xmlRes.content; 97 | const itemProperties: AttributesMap = {}; 98 | if (typeof content === 'string') { 99 | itemProperties.label = content; 100 | } 101 | 102 | return { 103 | type: 'button', 104 | itemProperties 105 | }; 106 | }, 107 | defaultModel: ``, 108 | propertiesDescriptor: [ 109 | ['type', {name: 'type', label: 'Type', category: PropertyCategories.Main, 110 | values: ['button', 'submit', 'reset'] 111 | }] 112 | ] 113 | }; 114 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/container/container.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{properties.text}} 3 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/container/container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from '@angular/core'; 2 | import { BaseUIComponent } from '../../components/base-ui-component'; 3 | import { StyleProperties, propDescription } from '../../properties'; 4 | import { ComponentExample, UIModel, ComponentDescriptor, Categories, XMLResult } from '../../models'; 5 | 6 | @Component({ 7 | selector: 'dc-container', 8 | templateUrl: './container.component.html', 9 | styles: [` 10 | :host:not(.row):not(.input-group) { 11 | display: block; 12 | } 13 | `] 14 | }) 15 | export class ContainerComponent extends BaseUIComponent { 16 | @HostBinding('style.display') display?: string; 17 | } 18 | 19 | export class ContainerProperties extends StyleProperties { 20 | @propDescription({ 21 | description: 'Text value', 22 | example: 'Information' 23 | }) 24 | text?: string; 25 | } 26 | 27 | const example: ComponentExample> = { 28 | title: 'Section example', 29 | uiModel: ` 30 |
            31 |
            32 | Header 1 33 | Header 2 34 | 35 | Header 3 36 |
            37 |
            Text
            38 |
            39 | `, 40 | dataModel: {} 41 | }; 42 | 43 | type ContainerComponentConstrutor = new() => ContainerComponent; 44 | 45 | type ContainerPropertiesConstrutor = new() => ContainerProperties; 46 | 47 | function getParseFunction(tag: string): (xmlRes: XMLResult) => UIModel { 48 | return (xmlRes: XMLResult) => ({ 49 | type: tag, 50 | itemProperties: { 51 | text: xmlRes.content 52 | } 53 | }); 54 | } 55 | 56 | export const containerDescriptor: ComponentDescriptor = { 57 | name: 'section', 58 | packageName: 'core', 59 | label: 'Container', 60 | category: Categories.Containers, 61 | description: 'layout component', 62 | itemProperties: ContainerProperties, 63 | component: ContainerComponent, 64 | example, 65 | parseUIModel: getParseFunction('section'), 66 | defaultModel: `
            `, 67 | }; 68 | 69 | export const divDescriptor: ComponentDescriptor = { 70 | name: 'div', 71 | packageName: 'core', 72 | label: 'Container', 73 | category: Categories.Containers, 74 | description: 'layout component', 75 | itemProperties: ContainerProperties, 76 | component: ContainerComponent, 77 | parseUIModel: getParseFunction('div'), 78 | example: { 79 | title: 'Div container', 80 | uiModel: (example.uiModel as string).replace(/
            /g, ''), 81 | dataModel: {} 82 | }, 83 | defaultModel: `
            `, 84 | }; 85 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/form/form.component.html: -------------------------------------------------------------------------------- 1 |
            2 | 3 |
            4 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import type { ElementRef, ViewContainerRef } from '@angular/core'; 2 | import { Component, ViewChild } from '@angular/core'; 3 | import { BaseUIComponent } from '../../components/base-ui-component'; 4 | import { StyleProperties, propDescription, PropertyCategories, PropTypes } from '../../properties'; 5 | import { ComponentExample, UIModel, ComponentDescriptor, Categories } from '../../models'; 6 | 7 | @Component({ 8 | selector: 'dc-form', 9 | templateUrl: './form.component.html' 10 | }) 11 | export class FormComponent extends BaseUIComponent { 12 | @ViewChild('form', { static: true }) form: ElementRef; 13 | 14 | onFormSubmit(evt): void { 15 | const formData = new FormData(this.form.nativeElement); 16 | this.emitEvent(this.properties.onSubmit, { formData }); 17 | // Trigger ui validation messages. 18 | this.form.nativeElement.querySelectorAll('input,textarea,select').forEach((el: HTMLFormElement, i, list) => { 19 | el.focus(); 20 | if (list.length - 1 === i) { 21 | el.blur(); 22 | } 23 | }); 24 | } 25 | } 26 | 27 | export class FormProperties extends StyleProperties { 28 | @propDescription({ 29 | description: 'Submit handler', 30 | example: 'onFormSubmit(form)', 31 | type: PropTypes.EVENT 32 | }) 33 | onSubmit?: string; 34 | } 35 | 36 | const example: ComponentExample> = { 37 | title: 'Form example', 38 | uiModel: ` 39 |
            40 | 41 | 42 | 43 |
            44 | `, 45 | scripts: ` 46 | def onSave(form): 47 | print(form) 48 | `, 49 | dataModel: {} 50 | }; 51 | 52 | type FormComponentConstrutor = new () => FormComponent; 53 | 54 | type FormPropertiesConstrutor = new () => FormProperties; 55 | 56 | export const formDescriptor: ComponentDescriptor = { 57 | name: 'form', 58 | packageName: 'core', 59 | label: 'Form', 60 | category: Categories.Containers, 61 | description: 'layout component', 62 | itemProperties: FormProperties, 63 | component: FormComponent, 64 | example, 65 | defaultModel: `
            `, 66 | propertiesDescriptor: [['method', { 67 | name: 'method', label: 'Method', category: PropertyCategories.Main, 68 | values: ['GET', 'POST', 'PUT', 'DELETE'] 69 | }]] 70 | }; 71 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/icon/icon.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { IconsUIComponent } from './icon-ui.component'; 4 | 5 | describe('IconsUIComponent', () => { 6 | let component: IconsUIComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ IconsUIComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(IconsUIComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/icon/icon.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit } from '@angular/core'; 2 | import { BaseUIComponent } from '../../components/base-ui-component'; 3 | import { BindingProperties, propDescription } from '../../properties'; 4 | import { ComponentExample, UIModel, ComponentDescriptor, Categories } from '../../models'; 5 | 6 | @Component({ 7 | selector: 'dc-icon-ui', 8 | template: '' 9 | }) 10 | export class IconComponent extends BaseUIComponent implements OnInit { 11 | @HostBinding('style.display') display = 'inline-block'; 12 | @HostBinding('style.font-size') fontSize: string; 13 | async ngOnInit(): Promise { 14 | await super.ngOnInit(); 15 | this.fontSize = this.properties.size || '1rem'; 16 | } 17 | get iconClass(): string { 18 | return this.componentDataModel || this.properties.iconClass; 19 | } 20 | } 21 | 22 | export class IconProperties extends BindingProperties { 23 | @propDescription({ 24 | description: 'Icon size', 25 | example: '2rem', 26 | }) 27 | size?: string; 28 | 29 | @propDescription({ 30 | description: 'Icon class', 31 | example: 'fa fa-play', 32 | }) 33 | iconClass: string; 34 | } 35 | 36 | type IconComponentConstrutor = new() => IconComponent; 37 | 38 | type IconPropertiesConstrutor = new() => IconProperties; 39 | 40 | const example: ComponentExample> = { 41 | title: 'Icon compoent example', 42 | uiModel: ` 43 | 44 | `, 45 | dataModel: {} 46 | }; 47 | 48 | const ICONS = ['fa fa-send', 'fa fa-play', 'fa fa-picture-o', 'fa fa-area-chart' , 'fa fa-bank', 'fa fa-beer', 49 | 'fa fa-automobile', 'fa fa-bag', 'fa fa-book', 'fa fa-bell', 'fa fa-camera', 'fa fa-calendar', 'fa fa-user', 50 | 'fa fa-globe', 'fa fa-gift', 'fa fa-group', 'fa fa-gear', 'fa fa-info', 'fa fa-money', 'fa fa-print', 'fa fa-registered', 51 | 'fa fa-share', 'fa fa-shopping-card', 'fa fa-tags', 'fa fa-tasks', 'fa fa-star', 'fa fa-univarsity', 'fa fa-file', 'fa fa-ticket', 52 | 'fa fa-television', 'fa fa-suitcase', 'fa fa-server', 'fa fa-map', 'fa fa-leptop', 'fa fa-home', 'fa fa-hashtag', 'fa fa-clock-o']; 53 | 54 | export const iconDescriptor: ComponentDescriptor = { 55 | name: 'icon', 56 | packageName: 'core', 57 | label: 'Icon', 58 | category: Categories.Basic, 59 | description: 'Icon component', 60 | itemProperties: IconProperties, 61 | component: IconComponent, 62 | example, 63 | defaultModel: '', 64 | children: false 65 | }; 66 | 67 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/input/input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | 4 | import { InputComponent, example } from './input.component'; 5 | import { UIModel } from '@ngx-dynamic-components/core/lib/models'; 6 | 7 | describe('InputComponent', () => { 8 | let component: InputComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ InputComponent ], 14 | imports: [ FormsModule ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(InputComponent); 21 | component = fixture.componentInstance; 22 | component.uiModel = example.uiModel as UIModel; 23 | component.dataModel = example.dataModel; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/input/input.examples.ts: -------------------------------------------------------------------------------- 1 | import { ComponentExample, UIModel } from '../../models'; 2 | import { InputProperties } from './input.component'; 3 | 4 | export const example1: ComponentExample> = { 5 | title: 'Text input example', 6 | uiModel: ` 7 |
            8 | 9 |
            10 | 11 | 12 |
            13 | 14 |
            15 | 16 | 17 |
            18 | 19 |
            20 | 21 | 27 | 28 |
            29 | 30 |
            31 | `, 32 | scripts: ` 33 | def onNameInput(name): 34 | disabled = name == "" 35 | comp = getComponentById(rootUIModel, "lastName") 36 | comp.disabled = disabled or null # should set null not false 37 | 38 | async def onDebouncedInput(): 39 | dataModel.debouncedText = searchText 40 | `, 41 | dataModel: {}, 42 | description: 'Input "Last name" is disabled while input "Name" is empty.' 43 | }; 44 | 45 | export const example2: ComponentExample> = { 46 | title: 'Checkbox example', 47 | uiModel: ` 48 |
            49 |
            50 | 51 | 52 |
            53 |
            54 | `, 55 | dataModel: {}, 56 | description: 'Checkbox value is binded to dataModel\'s subscribed property.' 57 | }; 58 | 59 | export const example3: ComponentExample> = { 60 | title: 'Radio group example', 61 | uiModel: ` 62 |
            63 |
            64 | 65 | 66 |
            67 |
            68 | 69 | 70 |
            71 |
            72 | 73 | 74 |
            75 |
            76 | `, 77 | dataModel: {}, 78 | scripts: ` 79 | def onSelect(): 80 | print(val) 81 | dataModel.selected = val 82 | `, 83 | description: 'You can choose one of 3 options.' 84 | }; 85 | 86 | export default [example1, example2, example3]; 87 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/label/label.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit } from '@angular/core'; 2 | import { BaseUIComponent } from '../../components/base-ui-component'; 3 | import { BindingProperties, propDescription } from '../../properties'; 4 | import { ComponentExample, UIModel, ComponentDescriptor, Categories, AttributesMap, XMLResult } from '../../models'; 5 | import { TextProperties } from '../text/text.component'; 6 | 7 | @Component({ 8 | // eslint-disable-next-line 9 | selector: 'label', 10 | template: `{{text}}`, 11 | styles: [` 12 | :host.form-check-label > label { 13 | margin-bottom: 0; 14 | } 15 | `] 16 | }) 17 | export class LabelComponent extends BaseUIComponent implements OnInit { 18 | @HostBinding('style.display') display = 'inline-block'; 19 | @HostBinding('attr.for') for: string; 20 | 21 | async ngOnInit(): Promise { 22 | await super.ngOnInit(); 23 | this.for = this.properties.for || undefined; 24 | } 25 | 26 | get text(): string { 27 | return this.componentDataModel || this.properties.text; 28 | } 29 | } 30 | 31 | export class LabelProperties extends TextProperties { 32 | @propDescription({ 33 | description: 'The id of a labelable form-related element', 34 | example: 'checkboxId' 35 | }) 36 | for?: string; 37 | } 38 | 39 | type LabelComponentConstrutor = new() => LabelComponent; 40 | 41 | type LabelPropertiesConstrutor = new() => LabelProperties; 42 | 43 | const example: ComponentExample> = { 44 | title: 'Label compoent example', 45 | uiModel: ``, 46 | dataModel: {} 47 | }; 48 | 49 | export const labelDescriptor: ComponentDescriptor = { 50 | name: 'label', 51 | packageName: 'core', 52 | label: 'Label', 53 | category: Categories.Basic, 54 | description: 'Label component', 55 | itemProperties: LabelProperties, 56 | component: LabelComponent, 57 | example, 58 | parseUIModel(xmlRes: XMLResult): UIModel { 59 | const content = xmlRes.content; 60 | const itemProperties: AttributesMap = {}; 61 | if (typeof content === 'string') { 62 | if (content.startsWith('$.')) { 63 | itemProperties.binding = content; 64 | } else { 65 | itemProperties.text = content; 66 | } 67 | } 68 | return {type: 'label', itemProperties}; 69 | }, 70 | children: false 71 | }; 72 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/link/link.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { BaseUIComponent } from '../../components/base-ui-component'; 3 | import { StyleProperties, propDescription } from '../../properties'; 4 | import { ComponentExample, UIModel, ComponentDescriptor, Categories, AttributesMap, XMLResult } from '../../models'; 5 | 6 | @Component({ 7 | selector: 'dc-link', 8 | template: ` 9 | {{properties.label}} 11 | `, 12 | styles: [` 13 | a { 14 | color: inherit; 15 | } 16 | `] 17 | }) 18 | export class LinkComponent extends BaseUIComponent { } 19 | 20 | export class LinkProperties extends StyleProperties { 21 | @propDescription({ 22 | description: 'Link label', 23 | example: 'Click me!', 24 | }) 25 | label: string; 26 | 27 | @propDescription({ 28 | description: 'Link href used in case of type=link.', 29 | example: 'url/to/site', 30 | }) 31 | href?: string; 32 | 33 | @propDescription({ 34 | description: 'The target attribute specifies where to open the linked document.', 35 | example: '_blank', 36 | }) 37 | target?: string; 38 | } 39 | 40 | export const example: ComponentExample> = { 41 | title: 'Basic link example', 42 | uiModel: ` 43 |
            44 | Button 45 | Google 46 |
            47 | `, 48 | dataModel: {} 49 | }; 50 | 51 | type LinkComponentConstrutor = new () => LinkComponent; 52 | 53 | type LinkPropertiesConstrutor = new () => LinkProperties; 54 | 55 | export const linkDescriptor: ComponentDescriptor = { 56 | name: 'link', 57 | label: 'Link', 58 | packageName: 'core', 59 | category: Categories.Basic, 60 | description: 'Link component', 61 | itemProperties: LinkProperties, 62 | component: LinkComponent, 63 | example, 64 | parseUIModel(xmlRes: XMLResult): UIModel { 65 | const content = xmlRes.content; 66 | const itemProperties: AttributesMap = {}; 67 | if (typeof content === 'string') { 68 | itemProperties.label = content; 69 | } 70 | 71 | return { 72 | type: 'link', 73 | itemProperties: { label: content } 74 | }; 75 | }, 76 | defaultModel: 'Google' 77 | }; 78 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/radio-group/radio-group.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RadioGroupComponent, example } from './radio-group.component'; 3 | 4 | describe('RadioGroupComponent', () => { 5 | let component: RadioGroupComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ RadioGroupComponent ] 11 | }) 12 | .compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(RadioGroupComponent); 17 | component = fixture.componentInstance; 18 | component.uiModel = example.uiModel as any; 19 | component.dataModel = example.dataModel; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/radio-group/radio-group.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { propDescription, BindingProperties, PropTypes } from '../../properties'; 3 | import { OptionValue, ComponentExample, UIModel, ComponentDescriptor, Categories, XMLResult, AttributesMap } from '../../models'; 4 | import { BaseUIComponent } from '../../components/base-ui-component'; 5 | import { queryValue } from '../../utils'; 6 | 7 | @Component({ 8 | selector: 'dc-radio-group', 9 | template: ` 10 | 11 |
            12 | 13 | 14 |
            15 |
            16 | `, 17 | styleUrls: ['../../styles/label.scss'] 18 | }) 19 | export class RadioGroupComponent extends BaseUIComponent { 20 | onChange(option: OptionValue): void { 21 | this.componentDataModel = option.value; 22 | this.changedDataModel.emit(this.dataModel); 23 | this.emitEvent(this.properties.onChange); 24 | } 25 | 26 | getId(val: any): string { 27 | const selectKey = this.uiModel.id || this.properties.binding; 28 | return `select-${selectKey || ''}-option-${val}`; 29 | } 30 | 31 | get options(): OptionValue[] { 32 | const src = this.properties.itemsSource; 33 | if (Array.isArray(src)) { 34 | return src; 35 | } 36 | 37 | if (typeof src === 'string' && src.startsWith('$.')) { 38 | return queryValue(this.dataModel, src); 39 | } 40 | } 41 | } 42 | 43 | export class RadioGroupProperties extends BindingProperties { 44 | @propDescription({ 45 | description: 'Radio group options or binding to dataModel.', 46 | example: '[{label: "One", value: 1}]', 47 | }) 48 | itemsSource: string|OptionValue[]; 49 | 50 | @propDescription({ 51 | description: 'On change event handler name.', 52 | example: 'onSelect', 53 | type: PropTypes.EVENT 54 | }) 55 | onChange?: string; 56 | } 57 | 58 | export const example: ComponentExample> = { 59 | title: 'Radio group example', 60 | uiModel: ` 61 |
            62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
            70 | `, 71 | dataModel: { 72 | genderOptions: [{label: 'Man', value: 'm'}, {label: 'Woman', value: 'w'}] 73 | } 74 | }; 75 | 76 | type RadioGroupComponentConstrutor = new () => RadioGroupComponent; 77 | 78 | type RadioGroupPropertiesConstrutor = new () => RadioGroupProperties; 79 | 80 | export const radioGroupDescriptor: ComponentDescriptor = { 81 | name: 'radio-group', 82 | label: 'Single choice boxes', 83 | packageName: 'core', 84 | category: Categories.Basic, 85 | description: 'Radio group component', 86 | itemProperties: RadioGroupProperties, 87 | component: RadioGroupComponent, 88 | example, 89 | parseUIModel(xmlRes: XMLResult): UIModel { 90 | const itemProperties: AttributesMap = {}; 91 | if (!xmlRes.attrs.itemsSource && xmlRes.childNodes) { 92 | itemProperties.itemsSource = xmlRes.childNodes.map(r => ({ label: r._, value: r.$.value })); 93 | xmlRes.childNodes = null; 94 | } 95 | return { 96 | type: 'radio-group', 97 | itemProperties 98 | }; 99 | }, 100 | defaultModel: '', 101 | children: [{ 102 | tag: 'option', 103 | properties: [{ 104 | name: 'value', 105 | label: 'Radio option value' 106 | }] 107 | }] 108 | }; 109 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/register.ts: -------------------------------------------------------------------------------- 1 | import { CoreService } from '../services/core.service'; 2 | import { ComponentDescriptor } from '../models'; 3 | import { textDescriptor } from './text/text.component'; 4 | import { buttonDescriptor } from './button/button.component'; 5 | import { inputDescriptor } from './input/input.component'; 6 | import { iconDescriptor } from './icon/icon.component'; 7 | import { textareaDescriptor } from './textarea/textarea.component'; 8 | import { containerDescriptor, divDescriptor } from './container/container.component'; 9 | import { linkDescriptor } from './link/link.component'; 10 | import { labelDescriptor } from './label/label.component'; 11 | import { formDescriptor } from './form/form.component'; 12 | import { selectDescriptor } from './select/select.component'; 13 | import { aDescriptor } from '../html-elements/a/a.component'; 14 | import { ulDescriptor } from '../html-elements/ul/ul.element'; 15 | import { liDescriptor } from '../html-elements/li/li.element'; 16 | import { olDescriptor } from '../html-elements/ol/ol.element'; 17 | import { htmlDescriptors } from '../html-elements'; 18 | import { repeaterDescriptor } from './repeater/repeater.component'; 19 | 20 | export const COMPONENTS_LIST: ComponentDescriptor[] = [ 21 | containerDescriptor, 22 | divDescriptor, 23 | formDescriptor, 24 | repeaterDescriptor, 25 | textDescriptor, 26 | labelDescriptor, 27 | iconDescriptor, 28 | inputDescriptor, 29 | textareaDescriptor, 30 | buttonDescriptor, 31 | linkDescriptor, 32 | selectDescriptor, 33 | aDescriptor, 34 | ulDescriptor, 35 | olDescriptor, 36 | liDescriptor, 37 | ...htmlDescriptors 38 | ]; 39 | 40 | // Register components. 41 | export function registerComponents(): void { 42 | COMPONENTS_LIST.forEach(component => CoreService.registerComponent(component)); 43 | } 44 | 45 | export function getCategories(): { name: string; packageName: string; components: ComponentDescriptor[] }[] { 46 | const categories = COMPONENTS_LIST.reduce((map, desc) => { 47 | map[desc.category] = map[desc.category] || []; 48 | map[desc.category].push(desc); 49 | return map; 50 | }, {}); 51 | 52 | return Object.entries(categories).map(([key, val]: [string, ComponentDescriptor[]]) => ({ 53 | name: key, 54 | components: val, 55 | packageName: 'core' 56 | })); 57 | } 58 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/repeater/repeater.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/repeater/repeater.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit, QueryList, ViewChildren, ViewContainerRef } from '@angular/core'; 2 | import { BaseUIComponent } from '../../components/base-ui-component'; 3 | import { CoreService } from '../../services/core.service'; 4 | import { StyleProperties, propDescription } from '../../properties'; 5 | import { UIModel, ComponentDescriptor, Categories, XMLResult } from '../../models'; 6 | import { queryValue, toXMLResult } from '../../utils'; 7 | import { createComponent } from '../../utils/renderer'; 8 | import example from './repeater.examples'; 9 | 10 | @Component({ 11 | selector: 'dc-repeater', 12 | templateUrl: './repeater.component.html', 13 | styles: [` 14 | :host:not(.row):not(.input-group) { 15 | display: block; 16 | } 17 | `] 18 | }) 19 | export class RepeaterComponent extends BaseUIComponent implements OnInit { 20 | @HostBinding('style.display') display = undefined; 21 | @ViewChildren('vc', { read: ViewContainerRef }) vContainers: QueryList; 22 | 23 | childUIModel: UIModel; 24 | 25 | async ngOnInit(): Promise { 26 | await super.ngOnInit(); 27 | this.childUIModel = CoreService.getUIModel(toXMLResult(this.properties.childUIModel)); 28 | const dataList = this.dataList; 29 | if (this.childUIModel) { 30 | this.vContainers.forEach((vc, i) => { 31 | createComponent(this, this.childUIModel, vc, dataList[i]); 32 | }); 33 | } 34 | } 35 | 36 | get dataList(): any[] { 37 | const src = this.properties.itemsSource; 38 | if (typeof src === 'string' && src.startsWith('$.')) { 39 | return queryValue(this.dataModel, src); 40 | } 41 | return []; 42 | } 43 | 44 | getDataModel(item: Record, index: number): Record { 45 | return { ...this.dataModel, _item: item, _index: index}; 46 | } 47 | 48 | onDataModelChanged(evt: Record, item: Record): void { 49 | const src = this.properties.itemsSource; 50 | if (typeof src === 'string' && src.startsWith('$.')) { 51 | const list = this.dataList; 52 | // eslint-disable-next-line no-underscore-dangle 53 | list.splice(list.indexOf(item), 1, evt._item); 54 | } 55 | this.changedDataModel.emit(this.dataModel); 56 | } 57 | } 58 | 59 | export class RepeaterProperties extends StyleProperties { 60 | @propDescription({ 61 | description: 'Select options or binding to dataModel.', 62 | example: '$.dataList', 63 | }) 64 | itemsSource: string; 65 | 66 | @propDescription({ 67 | description: 'Child UI Model.', 68 | example: '$.dataList', 69 | }) 70 | childUIModel: any; 71 | } 72 | 73 | type RepeaterComponentConstrutor = new() => RepeaterComponent; 74 | 75 | type RepeaterPropertiesConstrutor = new() => RepeaterProperties; 76 | 77 | function repeaterParser(xmlRes: XMLResult): UIModel { 78 | const uiModel = { 79 | type: 'repeater', 80 | itemProperties: { 81 | itemsSource: xmlRes.attrs.itemsSource, 82 | childUIModel: xmlRes.childNodes[0] 83 | } 84 | }; 85 | 86 | return uiModel; 87 | } 88 | 89 | export const repeaterDescriptor: ComponentDescriptor = { 90 | name: 'repeater', 91 | packageName: 'core', 92 | label: 'Repeater', 93 | category: Categories.Containers, 94 | description: 'Repeater component', 95 | itemProperties: RepeaterProperties, 96 | component: RepeaterComponent, 97 | example, 98 | parseUIModel: repeaterParser, 99 | defaultModel: ``, 100 | }; 101 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/repeater/repeater.examples.ts: -------------------------------------------------------------------------------- 1 | import { ComponentExample, UIModel } from '../../models'; 2 | import { RepeaterProperties } from './repeater.component'; 3 | 4 | const example: ComponentExample> = { 5 | title: 'Repeater example', 6 | uiModel: ` 7 |
            8 |

            Todo List

            9 |
            10 | 11 | 12 | 13 |
            14 | 15 |
            16 | $._item.item 17 | 18 |
            19 |
            20 |
            21 | `, 22 | dataModel: { 23 | dataList: [{ 24 | item: 'learn JS' 25 | }, { 26 | item: 'learn TS' 27 | }] 28 | }, 29 | scripts: `def addItem(): 30 | dataModel.dataList.push({ 31 | "item": dataModel.item 32 | }) 33 | 34 | def removeLast(): 35 | dataModel.dataList.pop() 36 | 37 | def remove(_item): 38 | index = dataModel.dataList.indexOf(_item) 39 | dataModel.dataList.splice(index, 1) 40 | ` 41 | }; 42 | 43 | 44 | const inputExample: ComponentExample> = { 45 | title: 'Repeater input example', 46 | uiModel: ` 47 |
            48 |

            Names

            49 |
            50 | 51 | 52 | 53 |
            54 | 55 | 56 | 57 |
            58 | `, 59 | dataModel: { 60 | dataList: [{ 61 | name: 'John' 62 | }, { 63 | name: 'Jane' 64 | }] 65 | }, 66 | scripts: `def addItem(): 67 | dataModel.dataList.push({ 68 | name: '' 69 | }) 70 | 71 | def removeLast(): 72 | dataModel.dataList.pop() 73 | 74 | def printDataModel(): 75 | print(dataModel) 76 | ` 77 | }; 78 | 79 | const selectExample: ComponentExample> = { 80 | title: 'Repeater select example', 81 | uiModel: ` 82 |
            83 |

            Countries

            84 |
            85 | 86 | 87 | 88 |
            89 | 90 | 91 | 92 |
            93 | `, 94 | dataModel: { 95 | countries: ['US', 'UK', 'France', 'Ukraine', 'Germany'], 96 | dataList: [{ 97 | country: 'US' 98 | }, { 99 | country: 'UK' 100 | }] 101 | }, 102 | scripts: `def addItem(): 103 | dataModel.dataList.push({ 104 | country: '' 105 | }) 106 | 107 | def removeLast(): 108 | dataModel.dataList.pop() 109 | 110 | def printDataModel(): 111 | print(dataModel) 112 | ` 113 | }; 114 | 115 | export default [ example, inputExample, selectExample ]; 116 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/select/select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | 4 | import { SelectComponent, example } from './select.component'; 5 | import { UIModel } from '../../models'; 6 | 7 | describe('SelectComponent', () => { 8 | let component: SelectComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ SelectComponent ], 14 | imports: [ FormsModule ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(SelectComponent); 21 | component = fixture.componentInstance; 22 | component.uiModel = example.uiModel; 23 | component.dataModel = example.dataModel; 24 | fixture.detectChanges(); 25 | }); 26 | 27 | it('should create', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/text/text.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from '@angular/core'; 2 | import { BaseUIComponent } from '../../components/base-ui-component'; 3 | import { BindingProperties, propDescription, PropertyCategories } from '../../properties'; 4 | import { ComponentExample, UIModel, ComponentDescriptor, Categories, AttributesMap, XMLResult } from '../../models'; 5 | 6 | @Component({ 7 | selector: 'dc-text', 8 | template: ` 9 | 10 |

            {{text}}

            11 |

            {{text}}

            12 |

            {{text}}

            13 |

            {{text}}

            14 |
            {{text}}
            15 |
            {{text}}
            16 |

            {{text}}

            17 | {{text}} 18 | {{text}} 19 |
            20 | ` 21 | }) 22 | export class TextComponent extends BaseUIComponent { 23 | @HostBinding('style.display') display = 'inline-block'; 24 | get txtStyle(): string { 25 | return this.properties['text-style']; 26 | } 27 | 28 | get txtClass(): string { 29 | return this.properties['text-style'] + ' ' + (this.properties.class || ''); 30 | } 31 | 32 | get text(): string { 33 | const val = this.componentDataModel; 34 | return typeof val === 'undefined' ? this.properties.text : val; 35 | } 36 | } 37 | 38 | export class TextProperties extends BindingProperties { 39 | @propDescription({ 40 | description: 'Displays text data.', 41 | example: 'Text information', 42 | }) 43 | text: string; 44 | 45 | @propDescription({ 46 | description: 'Text style text data.', 47 | example: 'h2', 48 | }) 49 | // eslint-disable-next-line @typescript-eslint/naming-convention 50 | 'text-style'?: string; 51 | } 52 | 53 | type TextComponentConstrutor = new() => TextComponent; 54 | 55 | type TextPropertiesConstrutor = new() => TextProperties; 56 | 57 | const example: ComponentExample> = { 58 | title: 'Text compoent example', 59 | uiModel: `
            60 | Hello world 61 | $.title 62 | 63 |
            `, 64 | dataModel: { 65 | title: 'Binded title', 66 | title2: 'Binded title2' 67 | } 68 | }; 69 | 70 | export const textDescriptor: ComponentDescriptor = { 71 | name: 'text', 72 | packageName: 'core', 73 | label: 'Text', 74 | category: Categories.Basic, 75 | description: 'Text component', 76 | itemProperties: TextProperties, 77 | component: TextComponent, 78 | example, 79 | parseUIModel(xmlRes: XMLResult): UIModel { 80 | const content = xmlRes.content; 81 | const itemProperties: AttributesMap = {}; 82 | if (typeof content === 'string') { 83 | if (content.startsWith('$.')) { 84 | itemProperties.binding = content; 85 | } else { 86 | itemProperties.text = content; 87 | } 88 | } 89 | return {type: 'text', itemProperties}; 90 | }, 91 | children: false, 92 | propertiesDescriptor: [ 93 | ['text-style', { 94 | name: 'text-style', label: 'Text style', category: PropertyCategories.Main, 95 | values: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'display-1', 'display-2', 'display-3', 'display-4'] 96 | }] 97 | ] 98 | }; 99 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/textarea/textarea.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | 4 | import { TextareaComponent, example } from './textarea.component'; 5 | 6 | describe('TextareaComponent', () => { 7 | let component: TextareaComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ TextareaComponent ], 13 | imports: [ FormsModule ] 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(TextareaComponent); 20 | component = fixture.componentInstance; 21 | component.dataModel = example.dataModel; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /projects/core/src/lib/ui-components/textarea/textarea.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, HostBinding, HostListener, OnChanges, SimpleChanges, DoCheck } from '@angular/core'; 2 | import { propDescription } from '../../properties'; 3 | import { ComponentExample, UIModel, ComponentDescriptor, Categories, XMLResult } from '../../models'; 4 | import { FormElementComponent, FormElementProperties } from '../../components/form-element-component'; 5 | 6 | @Component({ 7 | selector: 'textarea', // eslint-disable-line 8 | template: '{{value}}' 9 | }) 10 | // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle 11 | export class TextareaComponent extends FormElementComponent implements OnInit, OnChanges, DoCheck { 12 | @HostBinding('attr.cols') cols: number; 13 | @HostBinding('attr.rows') rows: number; 14 | 15 | @HostListener('input', ['$event.target']) 16 | onInput(txtArea: HTMLTextAreaElement): void { 17 | this.componentDataModel = txtArea.value; 18 | this.emitEvent(this.properties.onInput, txtArea.value); 19 | this.changedDataModel.emit(this.dataModel); 20 | } 21 | 22 | // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle 23 | async ngOnInit(): Promise { 24 | await super.ngOnInit(); 25 | this.cols = this.properties.cols || undefined; 26 | this.rows = this.properties.rows || undefined; 27 | this.value = this.componentDataModel; 28 | } 29 | 30 | // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle 31 | async ngOnChanges(changes: SimpleChanges): Promise { 32 | await super.ngOnChanges(changes); 33 | if (changes.dataModel) { 34 | this.value = this.componentDataModel; 35 | } 36 | } 37 | 38 | // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle 39 | ngDoCheck(): void { 40 | this.value = this.componentDataModel; 41 | } 42 | } 43 | 44 | export class TextareaProperties extends FormElementProperties { 45 | @propDescription({ 46 | description: 'Number of rows in textarea', 47 | example: '5', 48 | }) 49 | rows?: number; 50 | 51 | @propDescription({ 52 | description: 'The visible width of the text control', 53 | example: '20', 54 | }) 55 | cols?: number; 56 | } 57 | 58 | export const example: ComponentExample> = { 59 | title: 'Text area example', 60 | uiModel: ` 61 | 62 | `, 63 | dataModel: { 64 | info: 'Data model textarea value' 65 | } 66 | }; 67 | 68 | type TextareaComponentConstrutor = new() => TextareaComponent; 69 | 70 | type TextareaPropertiesConstrutor = new() => TextareaProperties; 71 | 72 | export const textareaDescriptor: ComponentDescriptor = { 73 | name: 'textarea', 74 | label: 'Text Area', 75 | packageName: 'core', 76 | category: Categories.Basic, 77 | description: 'Text area component', 78 | itemProperties: TextareaProperties, 79 | component: TextareaComponent, 80 | example, 81 | parseUIModel(xmlRes: XMLResult): UIModel { 82 | const itemProperties: TextareaProperties = { 83 | readonly: xmlRes.attrs.readonly === 'true' 84 | }; 85 | return { 86 | type: 'textarea', 87 | itemProperties 88 | }; 89 | }, 90 | defaultModel: '', 91 | children: false 92 | }; 93 | -------------------------------------------------------------------------------- /projects/core/src/lib/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { XMLResult } from '../models'; 2 | 3 | /** 4 | * Safe get item by index from array. 5 | * @param arr - array. 6 | * @param idx - index. 7 | */ 8 | export function getItemByIndex(arr: any, idx: number): any { 9 | if (arr && arr.length > idx) { 10 | return arr[idx]; 11 | } 12 | } 13 | /** 14 | * Convering Javascript string Map to object 15 | * @param map - map to convert. 16 | * @param excludeKeys - array of keys to exclude from result. 17 | */ 18 | export function mapToObj(map: Map, excludeKeys: string[] = []): {[key: string]: any} { 19 | const obj = {}; 20 | map.forEach((value, key) => { 21 | if (!excludeKeys.includes(key)) { 22 | obj[key] = value; 23 | } 24 | }); 25 | return obj; 26 | } 27 | 28 | /** TODO: check if still used */ 29 | export function getCssPath(el: Node, parent: Element): string { 30 | if (el instanceof Element) { 31 | const path = []; 32 | while (el !== parent) { 33 | let selector = el.nodeName.toLowerCase(); 34 | let sib: Node = el; 35 | let nth = 1; 36 | while (sib && sib !== parent) { 37 | sib = sib.previousSibling; 38 | if (sib instanceof Element) { 39 | nth++; 40 | } 41 | } 42 | selector += `:nth-child(${nth})`; 43 | path.unshift(selector); 44 | el = el.parentNode; 45 | } 46 | return path.join(' > '); 47 | } 48 | } 49 | 50 | export const kebabStrToCamel = (s: string) => s.replace(/([-][a-z])/ig, ($1) => $1.toUpperCase().replace('-', '')); 51 | 52 | export function setFields(fields: Array, data: Record[]): Record[] { 53 | if (!fields) { 54 | return data; 55 | } 56 | return fields.reduce((res: any, field) => { 57 | if (Array.isArray(field)) { 58 | res[field[1]] = data[field[0]]; 59 | } else { 60 | res[field] = data[field]; 61 | } 62 | return res; 63 | }, {}); 64 | } 65 | 66 | export function formatObjToJsonStr(obj: any): string { 67 | if (obj === null || obj === undefined) { 68 | return ''; 69 | } 70 | 71 | if (typeof obj === 'string') { 72 | return obj; 73 | } 74 | return JSON.stringify(obj, null, '\t'); 75 | } 76 | 77 | export function toXMLResult(xmlObj: any): XMLResult { 78 | return { 79 | type: xmlObj['#name'], 80 | attrs: xmlObj.$ || {}, 81 | childNodes: xmlObj.$$, 82 | content: xmlObj._ 83 | }; 84 | } 85 | 86 | /** 87 | * Parses function signature to function name and parameter. 88 | * @param funcSignature Function signature. 89 | */ 90 | export function parseArgFunction(funcSignature: string = ''): string[] { 91 | const match = funcSignature.match(/(.{1,})\((.{0,})\)/); 92 | if (!match) { 93 | return [funcSignature]; 94 | } 95 | 96 | return [match[1], ...match[2].split(',')]; 97 | } 98 | 99 | export function getStringEventArgs(eventName: string): string | number | undefined { 100 | const [fName, fParam] = parseArgFunction(eventName); 101 | if (fParam && fParam.startsWith('\'') && fParam.endsWith('\'')) { 102 | return fParam.replace(/'/g, ''); 103 | } else if (/^\d+(\.{0,1}\d+)?$/.test(fParam)) { 104 | return Number(fParam); 105 | } 106 | } 107 | 108 | export function kebabToCamelCase(str?: string): string { 109 | return str?.replace(/-([a-z])/g, (s, char) => char.toUpperCase()); 110 | } 111 | -------------------------------------------------------------------------------- /projects/core/src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parsers'; 2 | export * from './helpers'; 3 | -------------------------------------------------------------------------------- /projects/core/src/lib/utils/parsers.ts: -------------------------------------------------------------------------------- 1 | import * as iXml from 'isomorphic-xml2js'; 2 | import { BaseDynamicComponent } from '../components/base-dynamic-component'; 3 | import { UIModel } from '../models'; 4 | 5 | /** 6 | * Gets component from uiModel by id 7 | * @param uiModel UIModel. 8 | * @param id Component identifier. 9 | */ 10 | export function getComponentById( 11 | uiModel: UIModel, 12 | id: string 13 | ): BaseDynamicComponent | undefined { 14 | function traverseUiModel( 15 | uModel: UIModel, 16 | predicate: (u) => boolean 17 | ): UIModel { 18 | if (predicate(uModel)) { 19 | return uModel; 20 | } 21 | 22 | if (uModel.children?.length) { 23 | for (const chileModel of uModel.children) { 24 | const res = traverseUiModel(chileModel, predicate); 25 | if (res) { 26 | return res; 27 | } 28 | } 29 | } 30 | 31 | return null; 32 | } 33 | const componentUIModel = traverseUiModel(uiModel, (uim) => uim.id === id); 34 | return componentUIModel?.getComponent(); 35 | } 36 | 37 | /** 38 | * A helper method that sets value based on path. 39 | * e.g. setValue({}, 'obj.prop1.value', 22) will create a corresponding object and assign value = 22 40 | */ 41 | export function setValue( 42 | objectValue: Record, 43 | path: string, 44 | value: any 45 | ): void { 46 | if (!objectValue) { 47 | objectValue = {}; 48 | } 49 | 50 | if (path === '$' || path === '') { 51 | Object.assign(objectValue, value); 52 | return; 53 | } 54 | 55 | if (path.startsWith('$.')) { 56 | path = path.substring(2); 57 | } 58 | 59 | const props = path.indexOf('/') > 0 ? path.split('/') : path.split('.'); 60 | let res: any = objectValue; 61 | 62 | for (let i = 0; i < props.length - 1; i++) { 63 | if (typeof res[props[i]] !== 'object') { 64 | res[props[i]] = {}; 65 | } 66 | res = res[props[i]]; 67 | } 68 | 69 | res[props[props.length - 1]] = value; 70 | } 71 | 72 | /** 73 | * a helper methods that query JS Object for a value based on path. obj.subObject.value 74 | */ 75 | export function queryValue( 76 | obj: any, 77 | path: string, 78 | defaultValue: any = null 79 | ): any { 80 | if (!path) { 81 | return defaultValue; 82 | } 83 | 84 | if (path === '$') { 85 | return obj; 86 | } 87 | 88 | if (path.startsWith('$.')) { 89 | path = path.substring(2); 90 | } 91 | path = path.replace('}', ''); 92 | path = path.replace('{', ''); 93 | 94 | const props = path.indexOf('/') > 0 ? path.split('/') : path.split('.'); 95 | 96 | let res = obj; 97 | for (const p of props) { 98 | res = res[p]; 99 | if (!res && res !== 0) { 100 | return res || defaultValue; 101 | } 102 | } 103 | 104 | return res; 105 | } 106 | 107 | export function parseXmlString(xmlString: string): any { 108 | let result = null; 109 | 110 | const ops = { 111 | explicitChildren: true, 112 | preserveChildrenOrder: true, 113 | }; 114 | 115 | iXml.parseString(xmlString, ops, (err, res) => { 116 | if (res) { 117 | // need to clone result as it would mutate instance 118 | result = JSON.parse(JSON.stringify(res)); 119 | } 120 | if (err) { 121 | // quite dirty way to get meaningful error message 122 | let msg = err.message.substring(err.message.indexOf('12px">') + 6); 123 | msg = msg.substring(0, msg.indexOf('')); 124 | throw new Error(msg); 125 | } 126 | }); 127 | 128 | return result; 129 | } 130 | 131 | 132 | export function parseXmlStringPromise(xmlString: string): Promise { 133 | 134 | return new Promise((s, e) => { 135 | const ops = { 136 | explicitChildren: true, 137 | preserveChildrenOrder: true, 138 | }; 139 | 140 | iXml.parseString(xmlString, ops, (err, res) => { 141 | if (res) { 142 | // need to clone result as it would mutate instance 143 | const xmlObject = JSON.parse(JSON.stringify(res)); 144 | return s(xmlObject); 145 | } 146 | if (err) { 147 | // quite dirty way to get meaningful error message 148 | let msg = err.message.substring(err.message.indexOf('12px">') + 6); 149 | msg = msg.substring(0, msg.indexOf('')); 150 | return e(msg); 151 | } 152 | return null; 153 | }); 154 | }); 155 | } 156 | -------------------------------------------------------------------------------- /projects/core/src/lib/utils/renderer.ts: -------------------------------------------------------------------------------- 1 | import { BaseUIComponentConstructor, Categories, ComponentEvent, UIModel } from '../models'; 2 | import { CoreService } from '../services/core.service'; 3 | import { NGXDynamicComponent } from '../components/ngx-dynamic-component'; 4 | import { BaseDynamicComponent } from '../components/base-dynamic-component'; 5 | 6 | type UIComponent = BaseDynamicComponent | NGXDynamicComponent; 7 | 8 | export function renderChildren(parentComponent: BaseDynamicComponent): void { 9 | if (!parentComponent.containerRef || !parentComponent.uiModel) { 10 | return; 11 | } 12 | parentComponent.containerRef.clear(); 13 | parentComponent.uiModel?.children?.forEach(m => { 14 | createComponent(parentComponent, m); 15 | }); 16 | } 17 | 18 | export function createComponent( 19 | parentComponent: UIComponent, 20 | uiModel: UIModel, 21 | containerRef = parentComponent.containerRef, 22 | dataModel = parentComponent.dataModel): void { 23 | try { 24 | const descriptor = CoreService.getComponentDescriptor(uiModel.type); 25 | const componentClass = descriptor.component; 26 | let component: BaseDynamicComponent; 27 | if (descriptor.category === Categories.HTML) { 28 | const baseHtml = componentClass as any; 29 | component = new baseHtml(parentComponent.injector); 30 | component.dataModel = dataModel; 31 | component.uiModel = uiModel; 32 | component.create(containerRef.element.nativeElement); 33 | } else if (componentClass.prototype instanceof BaseDynamicComponent) { 34 | const componentRef = containerRef.createComponent( 35 | componentClass as BaseUIComponentConstructor 36 | ); 37 | component = componentRef.instance as BaseDynamicComponent; 38 | component.dataModel = dataModel; 39 | component.uiModel = uiModel; 40 | component.create(componentRef.location.nativeElement); 41 | } 42 | 43 | if (component) { 44 | component.element.classList.add('dc-element'); 45 | uiModel.getComponent = () => component; 46 | component.changedDataModel.subscribe((evt) => { 47 | parentComponent.changedDataModel.emit(evt); 48 | }); 49 | component.eventHandlers.subscribe((evt: ComponentEvent) => { 50 | if (parentComponent instanceof NGXDynamicComponent) { 51 | evt.rootUIModel = parentComponent.uiModel; 52 | } 53 | parentComponent.eventHandlers.emit(evt); 54 | }); 55 | } else { 56 | throw new Error('Not able to create component'); 57 | } 58 | } catch (error) { 59 | console.error(error); 60 | throw new Error(`Failed to create component ${uiModel.type}`); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /projects/core/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of core 3 | */ 4 | 5 | export * from './lib/services/core.service'; 6 | export * from './lib/components/base-dynamic-component'; 7 | export * from './lib/components/base-ui-component'; 8 | export * from './lib/components/ngx-dynamic-component'; 9 | export * from './lib/core.module'; 10 | export * from './lib/models'; 11 | export * from './lib/properties'; 12 | export * from './lib/utils'; 13 | export * from './lib/utils/renderer'; 14 | export * from './lib/ui-components/register'; 15 | export * from './lib/ui-components/text/text.component'; 16 | -------------------------------------------------------------------------------- /projects/core/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js'; 5 | import 'zone.js/testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting(), { 18 | teardown: { destroyAfterEach: false } 19 | } 20 | ); 21 | // Then we find all the tests. 22 | const context = require.context('./', true, /\.spec\.ts$/); 23 | // And load the modules. 24 | context.keys().map(context); 25 | -------------------------------------------------------------------------------- /projects/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "target": "es2015", 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "types": [], 16 | "lib": [ 17 | "dom", 18 | "es2018" 19 | ] 20 | }, 21 | "angularCompilerOptions": { 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true 27 | }, 28 | "exclude": [ 29 | "src/test.ts", 30 | "**/*.spec.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /projects/core/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial", 8 | "enableIvy": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/tools/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": [ 4 | "!**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "projects/tools/tsconfig.lib.json", 14 | "projects/tools/tsconfig.spec.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "rules": { 19 | "@angular-eslint/component-selector": [ 20 | "error", 21 | { 22 | "type": "element", 23 | "prefix": "dc", 24 | "style": "kebab-case" 25 | } 26 | ], 27 | "@angular-eslint/directive-selector": [ 28 | "error", 29 | { 30 | "type": "attribute", 31 | "prefix": "dc", 32 | "style": "camelCase" 33 | } 34 | ], 35 | "@typescript-eslint/explicit-member-accessibility": [ 36 | "off", 37 | { 38 | "accessibility": "explicit" 39 | } 40 | ], 41 | "arrow-parens": [ 42 | "off", 43 | "always" 44 | ], 45 | "import/order": "off", 46 | "jsdoc/newline-after-description": "off" 47 | } 48 | }, 49 | { 50 | "files": [ 51 | "*.html" 52 | ], 53 | "rules": {} 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /projects/tools/README.md: -------------------------------------------------------------------------------- 1 | # ngx-dynamic-components 2 | 3 | NGX Dynamic Components is a dynamic configuration driven web page builder. It is designed to build anything from a single one form web page to more complex web app. 4 | 5 | Add to angular.json `"scripts": [`: 6 | ``` 7 | { "input": "node_modules/ace-builds/src-noconflict/ace.js", "bundleName": "ace-editor" }, 8 | { "input": "node_modules/ace-builds/src-noconflict/mode-json.js", "lazy": false }, 9 | { "input": "node_modules/JSPython/dist/assets/mode-interpreter.js", "lazy": false }, 10 | { "input": "node_modules/ace-builds/src-noconflict/ext-language_tools.js", "lazy": false } 11 | ``` 12 | -------------------------------------------------------------------------------- /projects/tools/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/tools'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/tools/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/@ngx-dynamic-components/tools", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngx-dynamic-components/tools", 3 | "version": "15.1.2", 4 | "private": false, 5 | "description": "@ngx-dynamic-components/tools is Angular 7+ library what contains a core interfaces to build a configuration driven web pages.", 6 | "author": "FalconSoft Ltd ", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/FalconSoft/ngx-dynamic-components" 11 | }, 12 | "homepage": "https://falconsoft.github.io/ngx-dynamic-components/", 13 | "keywords": [ 14 | "angular", 15 | "forms", 16 | "dynamic forms", 17 | "configurable forms", 18 | "json forms", 19 | "form library" 20 | ], 21 | "peerDependencies": { 22 | "@angular/common": "^15.1.0", 23 | "@angular/core": "^15.1.0", 24 | "@angular/material": "^15.1.0", 25 | "@ngx-dynamic-components/bootstrap": ">=15.1.0", 26 | "@ngx-dynamic-components/core": ">=15.1.0", 27 | "ace-builds": "^1.14.0", 28 | "angular-split": "^14.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/add-dialog/add-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddDialogComponent } from './add-dialog.component'; 4 | 5 | xdescribe('AddDialogComponent', () => { 6 | let component: AddDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/add-dialog/add-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'dc-add-dialog', 6 | template: ` 7 | 8 |

            {{cat.name}}

            9 | 11 | {{item.name}} - {{item.description}} 12 | 13 |
            14 | `, 15 | styles: [` 16 | :host { 17 | min-width: 300px; 18 | display: block; 19 | } 20 | 21 | mat-list h3 { 22 | margin: 0; 23 | text-decoration: underline; 24 | } 25 | 26 | .components mat-list-item { 27 | height: auto; 28 | cursor: pointer; 29 | } 30 | `] 31 | }) 32 | export class AddDialogComponent implements OnInit { 33 | 34 | categories = []; 35 | 36 | constructor(public dialogRef: MatDialogRef) { } 37 | 38 | ngOnInit(): void { 39 | this.categories = []; 40 | } 41 | 42 | selectComponent(item): void { 43 | this.dialogRef.close(item.defaultModel || item.example.uiModel || { 44 | type: `${item.packageName}:${item.name}`, 45 | itemProperties: {}, 46 | containerProperties: {} 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/control-editor/control-editor.component.scss: -------------------------------------------------------------------------------- 1 | $border: 1px dashed rgba(0, 0, 0, 0); 2 | 3 | ::ng-deep .preview dc-container dc-container-row.row { 4 | min-height: 100px; 5 | border: $border; 6 | &.active { 7 | border-color: grey; 8 | } 9 | } 10 | 11 | ::ng-deep .preview.edit-mode { 12 | dc-container dc-container-row.row { 13 | border-color: rgba(0, 0, 0, .25); 14 | > dc-control-editor { 15 | left: -50px; 16 | .handle { 17 | display: none; 18 | } 19 | 20 | .add-component { 21 | display: inline-block; 22 | } 23 | } 24 | .col-sm dc-ui-flex-container + dc-control-editor { 25 | left: 60px; 26 | } 27 | } 28 | } 29 | 30 | ::ng-deep .preview dc-ui-flex-container .item, 31 | ::ng-deep .preview dc-container .item, 32 | ::ng-deep .preview dc-container dc-container-row.row, 33 | ::ng-deep .preview dc-container dc-container-row.row > div[class*="col-"] { 34 | &:hover > dc-control-editor { 35 | display: block; 36 | } 37 | 38 | position: relative; 39 | border: $border; 40 | 41 | &.drag-selected { 42 | border-color: grey; 43 | dc-control-editor { 44 | display: block !important; 45 | } 46 | } 47 | 48 | dc-ui-flex-container + dc-control-editor > .handle { 49 | color: rgba(0, 0, 0, .75); 50 | } 51 | 52 | &:hover { 53 | dc-ui-flex-container + dc-control-editor > .handle { 54 | color: rgba(0, 0, 0, .8); 55 | } 56 | } 57 | 58 | dc-ui-flex-container + dc-control-editor { 59 | left: -50px; 60 | } 61 | } 62 | 63 | ::ng-deep .preview dc-container.container dc-container-row.row > dc-control-editor { 64 | left: -50px; 65 | } 66 | 67 | ::ng-deep { 68 | /* in-flight clone */ 69 | .gu-mirror { 70 | position: fixed !important; 71 | margin: 0 !important; 72 | z-index: 9999 !important; 73 | opacity: 0.8; 74 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; 75 | filter: alpha(opacity=80); 76 | pointer-events: none; 77 | 78 | box-sizing: border-box; 79 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 80 | 0 8px 10px 1px rgba(0, 0, 0, 0.14), 81 | 0 3px 14px 2px rgba(0, 0, 0, 0.12); 82 | background: #eeeeee; 83 | 84 | > dc-control-editor { 85 | display: block !important; 86 | } 87 | 88 | > dc-ui-flex-container + dc-control-editor { 89 | left: -50px; 90 | } 91 | } 92 | /* high-performance display:none; helper */ 93 | .gu-hide { 94 | left: -9999px !important; 95 | } 96 | /* added to mirrorContainer (default = body) while dragging */ 97 | .gu-unselectable { 98 | -webkit-user-select: none !important; 99 | -moz-user-select: none !important; 100 | -ms-user-select: none !important; 101 | user-select: none !important; 102 | } 103 | /* added to the source element while its mirror is dragged */ 104 | .gu-transit { 105 | position: relative; 106 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 107 | 108 | &::after { 109 | content: ' '; 110 | top: 0; 111 | display: block; 112 | position: absolute; 113 | background: #ccc; 114 | width: 100%; 115 | height: 100%; 116 | z-index: 1; 117 | } 118 | 119 | dc-control-editor { 120 | display: none !important; 121 | } 122 | } 123 | } 124 | 125 | 126 | 127 | :host { 128 | position: absolute; 129 | color: rgba(0, 0, 0, .5); 130 | top: 0; 131 | right: 0; 132 | z-index: 2; 133 | max-width: 105px; 134 | display: none; 135 | background: rgba(0, 0, 0, .25); 136 | border-radius: 4px; 137 | &:hover { 138 | cursor: move; 139 | color: rgba(0, 0, 0, .75); 140 | } 141 | 142 | ::ng-deep button { 143 | height: 35px; 144 | line-height: 35px; 145 | width: 35px; 146 | } 147 | 148 | .add-component { 149 | display: none; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/control-editor/control-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ControlEditorComponent } from './control-editor.component'; 4 | 5 | xdescribe('ControlEditorComponent', () => { 6 | let component: ControlEditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ControlEditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ControlEditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/control-editor/control-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | import type { UIModel } from '@ngx-dynamic-components/core'; 3 | import { MatDialog } from '@angular/material/dialog'; 4 | import { AddDialogComponent } from '../add-dialog/add-dialog.component'; 5 | 6 | @Component({ 7 | selector: 'dc-control-editor', 8 | template: ` 9 | 10 | 18 | 21 | 24 | `, 25 | styleUrls: ['./control-editor.component.scss'] 26 | }) 27 | export class ControlEditorComponent { 28 | 29 | @Input() uiModel: UIModel; 30 | @Output() uiModelChanged = new EventEmitter(); 31 | @Output() uiModelRemoved = new EventEmitter(); 32 | 33 | constructor(private dialog: MatDialog) {} 34 | 35 | openAddDialog(): void { 36 | const dialogRef = this.dialog.open(AddDialogComponent); 37 | 38 | dialogRef.afterClosed().subscribe(item => { 39 | this.uiModel.children.push(item); 40 | this.uiModelChanged.emit(); 41 | }); 42 | } 43 | 44 | onHover(evt): void { 45 | const dragEl = this.getParentDrag(evt.target as HTMLElement); 46 | dragEl.classList.add('drag-selected'); 47 | } 48 | 49 | onMouseLeave(evt: Event): void { 50 | const dragEl = this.getParentDrag(evt.target as HTMLElement); 51 | dragEl.classList.remove('drag-selected'); 52 | } 53 | 54 | getDragTooltip(): string { 55 | return this.uiModel.type === 'material:flex-container' ? 'Drag container' : 'Drag component'; 56 | } 57 | 58 | private getParentDrag(el: HTMLElement): HTMLElement { 59 | let dragEl = el; 60 | 61 | while (!['item', 'row', 'col-sm'].some(c => Array.from(dragEl.classList).includes(c))) { 62 | dragEl = dragEl.parentNode as HTMLElement; 63 | } 64 | 65 | return dragEl; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/preview-editor/preview-editor.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} 4 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
            20 |
            21 | 22 |
            23 |
            24 |
            25 |
            26 | 27 |
            28 |
            29 | 30 |
            31 |
            32 |
            33 |
            34 | 35 |
            36 | 42 |
            43 |
            44 |
            45 |
            46 |
            47 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/preview-editor/preview-editor.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../../src/_vars.scss'; 2 | 3 | $color: rgba(0, 0, 0, .5); 4 | 5 | :host { 6 | display: flex; 7 | } 8 | 9 | ::ng-deep mat-card { 10 | .mat-card-header-text { 11 | margin: 0 12 | } 13 | 14 | .mat-mdc-tab-body-wrapper, 15 | .mat-tab-body-wrapper { 16 | display: block; 17 | overflow: visible; 18 | height: 100%; 19 | min-height: 200px; 20 | mat-tab-body { 21 | height: 100%; 22 | } 23 | } 24 | } 25 | 26 | .preview-card mat-card-content { 27 | ::ng-deep .as-init .as-split-gutter, .as-split-area { 28 | height: auto; 29 | } 30 | } 31 | 32 | .ui-model-tab ::ng-deep .mat-tab-label { 33 | height: 24px; 34 | font-size: .85rem; 35 | } 36 | 37 | mat-card { 38 | width: 100%; 39 | padding: 0; 40 | mat-card-header, mat-card-content { 41 | padding: 1em; 42 | } 43 | 44 | mat-divider { 45 | margin: 1em 0; 46 | position: static; 47 | } 48 | 49 | mat-card-header { 50 | color: $color; 51 | background: $gray; 52 | padding: 8px 20px; 53 | mat-icon { 54 | cursor: pointer; 55 | } 56 | h3 { 57 | margin: 0; 58 | } 59 | } 60 | 61 | .preview { 62 | background: $gray; 63 | padding: 1em; 64 | } 65 | } 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/properties-editor/properties-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PropertiesEditorComponent } from './properties-editor.component'; 4 | 5 | xdescribe('PropertiesEditorComponent', () => { 6 | let component: PropertiesEditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PropertiesEditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PropertiesEditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/tools/src/lib/components/properties-editor/properties-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, HostListener, ViewChild, EventEmitter, Output, OnInit } from '@angular/core'; 2 | import { UIModelProperty, CoreService } from '@ngx-dynamic-components/core'; 3 | import type { ElementRef } from '@angular/core'; 4 | import type { MatButton } from '@angular/material/button'; 5 | import type { UIModel } from '@ngx-dynamic-components/core'; 6 | 7 | @Component({ 8 | selector: 'dc-properties-editor', 9 | template: ` 10 | 13 |
            14 | 15 | 16 | 17 |
            18 | `, 19 | styles: [` 20 | .editor-container { 21 | position: absolute; 22 | background: white; 23 | top: 0; 24 | left: 40px; 25 | z-index: 3; 26 | padding: 10px; 27 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 28 | 0 8px 10px 1px rgba(0, 0, 0, 0.14), 29 | 0 3px 14px 2px rgba(0, 0, 0, 0.12); 30 | 31 | } 32 | .editor-container.left { 33 | left: -200px; 34 | } 35 | `] 36 | }) 37 | export class PropertiesEditorComponent implements OnInit { 38 | @Input() 39 | uiModel: UIModel; 40 | 41 | @ViewChild('editorContainer') 42 | editorContainer: ElementRef; 43 | 44 | @ViewChild('editBtn') 45 | editBtn: MatButton; 46 | 47 | @Output() 48 | updatedProperty = new EventEmitter(); 49 | 50 | itemProperties: UIModelProperty[] = []; 51 | properties = []; 52 | 53 | showEditor = false; 54 | 55 | get positionClass(): string { 56 | // eslint-disable-next-line no-underscore-dangle 57 | const btnPos = this.editBtn._elementRef.nativeElement.getBoundingClientRect().right; 58 | const docWidth = document.body.offsetWidth; 59 | return docWidth - btnPos < 200 ? 'left' : ''; 60 | } 61 | 62 | @HostListener('document:click', ['$event.target']) 63 | onClick(targetElement): void { 64 | if (this.editorContainer) { 65 | const clickedInside = this.editorContainer.nativeElement.contains(targetElement); 66 | // eslint-disable-next-line no-underscore-dangle 67 | if (!clickedInside && this.editBtn._elementRef.nativeElement.contains(targetElement)) { 68 | // Clicked on button. 69 | const itemProps = this.uiModel.itemProperties || {}; 70 | this.properties = this.itemProperties.map(({name}) => { 71 | let value = itemProps[name]; 72 | if (value === undefined) { 73 | value = ''; 74 | } else if (typeof value === 'object') { 75 | value = JSON.stringify(value); 76 | } 77 | return {name, value}; 78 | }); 79 | this.showEditor = true; 80 | } else if (!clickedInside) { 81 | // Clicked outside. 82 | this.showEditor = false; 83 | this.updatedProperty.emit(); 84 | } 85 | } 86 | } 87 | 88 | ngOnInit(): void { 89 | this.itemProperties = CoreService.getComponentProperties(this.uiModel.type); 90 | } 91 | 92 | updateProperty(evt, prop): void { 93 | try { 94 | // If property value is an object or an array. 95 | this.uiModel.itemProperties[prop] = JSON.parse(evt.target.value); 96 | } catch { 97 | this.uiModel.itemProperties[prop] = evt.target.value; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /projects/tools/src/lib/monaco.config.ts: -------------------------------------------------------------------------------- 1 | declare const monaco: any; 2 | 3 | export const defaultOptions = { 4 | language: 'json', 5 | automaticLayout: true 6 | }; 7 | 8 | export function registerMonacoCompletion(): any { 9 | if (!window.hasOwnProperty('monaco')) { 10 | return setTimeout(registerMonacoCompletion, 1e2); 11 | } 12 | monaco.languages.registerCompletionItemProvider('json', { 13 | provideCompletionItems(model, position): any { 14 | const textUntilPosition = model.getValueInRange({ 15 | startLineNumber: 1, 16 | startColumn: 1, 17 | endLineNumber: position.lineNumber, 18 | endColumn: position.column 19 | }); 20 | 21 | const matchItemProperties = textUntilPosition.replace(/(\n|\s)/g, '').match(/"*"itemProperties":\{/); 22 | let suggestions = getBasicProposals(); 23 | if (matchItemProperties) { 24 | suggestions = suggestions.concat(getItemProposals()); 25 | } 26 | return { 27 | suggestions 28 | }; 29 | } 30 | }); 31 | } 32 | 33 | function getItemProposals(): any[] { 34 | return [{ 35 | label: '"fxLayout"', 36 | kind: monaco.languages.CompletionItemKind.Function, 37 | documentation: 'fxLayout', 38 | insertText: '"fxLayout": "row"' 39 | }]; 40 | } 41 | 42 | function getBasicProposals(): any[] { 43 | return [{ 44 | label: '"type"', 45 | kind: monaco.languages.CompletionItemKind.Function, 46 | documentation: 'Type of UI component.', 47 | insertText: '"type": ""' 48 | }, { 49 | label: '"itemProperties"', 50 | kind: monaco.languages.CompletionItemKind.Function, 51 | documentation: 'Item properties.', 52 | insertText: '"itemProperties": {\n\n}' 53 | }, { 54 | label: '"containerProperties"', 55 | kind: monaco.languages.CompletionItemKind.Function, 56 | documentation: 'Container properties.', 57 | insertText: '"containerProperties": {\n\n}' 58 | }, { 59 | label: '"children"', 60 | kind: monaco.languages.CompletionItemKind.Function, 61 | documentation: 'Children components list', 62 | insertText: '"children": [\n\n]' 63 | }]; 64 | } 65 | -------------------------------------------------------------------------------- /projects/tools/src/lib/tools.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { AngularSplitModule } from 'angular-split'; 6 | 7 | import { MatToolbarModule } from '@angular/material/toolbar'; 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { MatTabsModule } from '@angular/material/tabs'; 10 | import { MatDividerModule } from '@angular/material/divider'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatIconModule } from '@angular/material/icon'; 13 | import { MatTooltipModule } from '@angular/material/tooltip'; 14 | import { MatInputModule } from '@angular/material/input'; 15 | import { MatFormFieldModule } from '@angular/material/form-field'; 16 | import { MatDialogModule } from '@angular/material/dialog'; 17 | import { MatListModule } from '@angular/material/list'; 18 | 19 | import { DynamicComponentsCoreModule } from '@ngx-dynamic-components/core'; 20 | import { DynamicComponentsBootstrapModule } from '@ngx-dynamic-components/bootstrap'; 21 | 22 | import { PreviewEditorComponent } from './components/preview-editor/preview-editor.component'; 23 | import { ControlEditorComponent } from './components/control-editor/control-editor.component'; 24 | import { PropertiesEditorComponent } from './components/properties-editor/properties-editor.component'; 25 | import { AddDialogComponent } from './components/add-dialog/add-dialog.component'; 26 | 27 | export const angularSplitModuleForRoot: ModuleWithProviders = AngularSplitModule.forRoot(); 28 | 29 | @NgModule({ 30 | declarations: [ 31 | PreviewEditorComponent, ControlEditorComponent, PropertiesEditorComponent, AddDialogComponent 32 | ], 33 | imports: [ 34 | CommonModule, 35 | DynamicComponentsCoreModule, 36 | DynamicComponentsBootstrapModule, 37 | MatToolbarModule, 38 | MatButtonModule, 39 | MatTabsModule, 40 | MatDividerModule, 41 | MatCardModule, 42 | MatIconModule, 43 | MatTooltipModule, 44 | MatInputModule, 45 | MatFormFieldModule, 46 | MatDialogModule, 47 | MatListModule, 48 | FormsModule, 49 | ReactiveFormsModule, 50 | angularSplitModuleForRoot 51 | ], 52 | exports: [ PreviewEditorComponent ] 53 | }) 54 | export class ToolsModule { } 55 | -------------------------------------------------------------------------------- /projects/tools/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of tools 3 | */ 4 | 5 | export * from './lib/tools.module'; 6 | export { PreviewEditorComponent } from './lib/components/preview-editor/preview-editor.component'; 7 | -------------------------------------------------------------------------------- /projects/tools/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js'; 5 | import 'zone.js/testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting(), { 18 | teardown: { destroyAfterEach: false } 19 | } 20 | ); 21 | // Then we find all the tests. 22 | const context = require.context('./', true, /\.spec\.ts$/); 23 | // And load the modules. 24 | context.keys().map(context); 25 | -------------------------------------------------------------------------------- /projects/tools/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "target": "es2015", 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "types": [], 16 | "lib": [ 17 | "dom", 18 | "es2018" 19 | ] 20 | }, 21 | "angularCompilerOptions": { 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "enableResourceInlining": true 25 | }, 26 | "exclude": [ 27 | "src/test.ts", 28 | "**/*.spec.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /projects/tools/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "enableIvy": true, 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/tools/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/_vars.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | $app-primary: mat.define-palette(mat.$grey-palette); 4 | $app-accent: mat.define-palette(mat.$light-blue-palette); 5 | $app-warn: mat.define-palette(mat.$pink-palette); 6 | 7 | $primary: mat.get-color-from-palette($app-primary); 8 | $accent: mat.get-color-from-palette($app-accent); 9 | $warn: mat.get-color-from-palette($app-warn); 10 | 11 | $active: mat.get-color-from-palette(mat.define-palette(mat.$indigo-palette)); 12 | 13 | $gray: mat.get-color-from-palette(mat.define-palette(mat.$grey-palette, 200)); 14 | $lightGray: mat.get-color-from-palette(mat.define-palette(mat.$grey-palette, 50)); 15 | $darkGray: mat.get-color-from-palette(mat.define-palette(mat.$grey-palette, A400)); 16 | 17 | $text: rgba(0, 0, 0, .5); 18 | 19 | $panelMargin: 25px; 20 | $staticSpace: 25px; 21 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { HomePageComponent } from './components/home-page.component'; 5 | import { EditorPageComponent } from './components/editor-page.component'; 6 | import { ComponentsPageComponent } from './components/components-page.component'; 7 | import { GuidesPageComponent } from './components/guides-page.component'; 8 | import { ComponentPageComponent } from './components/component-page.component'; 9 | import { ComponentsComponent } from './components/components.component'; 10 | import { CategoriesComponent } from './components/categories.component'; 11 | import { ExamplesPageComponent } from './components/examples-page/examples-page.component'; 12 | import { ExampleViewComponent } from './components/example-view.component'; 13 | 14 | export const routes: Routes = [ 15 | { path: '', component: HomePageComponent }, 16 | { path: 'editor', component: EditorPageComponent }, 17 | { path: 'guides', component: GuidesPageComponent }, 18 | { 19 | path: 'components', component: ComponentsPageComponent, 20 | children: [ 21 | { path: '', component: ComponentsComponent }, 22 | { path: 'categories', component: CategoriesComponent }, 23 | { path: 'categories/:packageName/:category', component: ComponentsComponent }, 24 | { path: ':packageName/:component', component: ComponentPageComponent }, 25 | ] 26 | }, 27 | { 28 | path: 'examples', component: ExamplesPageComponent, 29 | children: [ 30 | { path: '', component: ExampleViewComponent }, 31 | { path: ':example', component: ExampleViewComponent } 32 | ] 33 | } 34 | ]; 35 | 36 | @NgModule({ 37 | declarations: [], 38 | imports: [ 39 | CommonModule, 40 | RouterModule.forRoot(routes) 41 | ], 42 | exports: [RouterModule] 43 | }) 44 | export class AppRoutingModule { } 45 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
            2 | 3 | 4 |
            5 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import '../vars'; 2 | 3 | .app-content { 4 | height: 100%; 5 | } 6 | ::ng-deep router-outlet + * { 7 | overflow: auto; 8 | flex: 1 1 auto; 9 | flex-direction: column; 10 | } 11 | 12 | ::ng-deep .page-drawer mat-drawer { 13 | background: $lightGray; 14 | border: 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { MatToolbarModule } from '@angular/material/toolbar'; 4 | import { AppComponent } from './app.component'; 5 | import { NavComponent } from './components/nav.component'; 6 | 7 | describe('AppComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [ 11 | RouterTestingModule, 12 | MatToolbarModule 13 | ], 14 | declarations: [ 15 | AppComponent, 16 | NavComponent 17 | ], 18 | }).compileComponents(); 19 | })); 20 | 21 | it('should create the app', () => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.debugElement.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }); 26 | 27 | it(`should have as title 'ngx-dynamic-components'`, () => { 28 | const fixture = TestBed.createComponent(AppComponent); 29 | const app = fixture.debugElement.componentInstance; 30 | expect(app.title).toEqual('ngx-dynamic-components'); 31 | }); 32 | 33 | it('should render navigation', () => { 34 | const fixture = TestBed.createComponent(AppComponent); 35 | fixture.detectChanges(); 36 | const compiled = fixture.debugElement.nativeElement; 37 | expect(compiled.querySelectorAll('mat-toolbar-row>a').length).toEqual(6); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dc-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'ngx-dynamic-components'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { MatToolbarModule } from '@angular/material/toolbar'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatTabsModule } from '@angular/material/tabs'; 9 | import { MatSidenavModule } from '@angular/material/sidenav'; 10 | import { MatListModule } from '@angular/material/list'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatTableModule } from '@angular/material/table'; 13 | import { MatExpansionModule } from '@angular/material/expansion'; 14 | import { MatIconModule } from '@angular/material/icon'; 15 | 16 | import { ResizableModule } from 'angular-resizable-element'; 17 | import { MarkdownModule } from 'ngx-markdown'; 18 | 19 | import { DynamicComponentsCoreModule } from '@ngx-dynamic-components/core'; 20 | import { ToolsModule } from '@ngx-dynamic-components/tools'; 21 | import { DynamicComponentsBootstrapModule } from '@ngx-dynamic-components/bootstrap'; 22 | 23 | 24 | import { AppRoutingModule } from './app-routing.module'; 25 | import { AppComponent } from './app.component'; 26 | import { HomePageComponent } from './components/home-page.component'; 27 | import { NavComponent } from './components/nav.component'; 28 | import { EditorPageComponent } from './components/editor-page.component'; 29 | import { GuidesPageComponent } from './components/guides-page.component'; 30 | import { ComponentsPageComponent } from './components/components-page.component'; 31 | import { PageHeaderComponent } from './components/page-header.component'; 32 | import { ComponentPageComponent } from './components/component-page.component'; 33 | import { CardsComponent } from './components/cards/cards.component'; 34 | import { SideBarComponent } from './components/side-bar/side-bar.component'; 35 | import { CategoriesComponent } from './components/categories.component'; 36 | import { ComponentsComponent } from './components/components.component'; 37 | import { ItemPropertiesComponent } from './components/item-properties/item-properties.component'; 38 | import { JsonFormatterDirective } from './directives/json-formatter.directive'; 39 | import { ExamplesPageComponent } from './components/examples-page/examples-page.component'; 40 | import { ExampleViewComponent } from './components/example-view.component'; 41 | import { SidenavLayoutComponent } from './components/sidenav-layout.component'; 42 | 43 | @NgModule({ 44 | declarations: [ 45 | AppComponent, 46 | HomePageComponent, 47 | NavComponent, 48 | EditorPageComponent, 49 | GuidesPageComponent, 50 | ComponentsPageComponent, 51 | PageHeaderComponent, 52 | ComponentPageComponent, 53 | CardsComponent, 54 | SideBarComponent, 55 | CategoriesComponent, 56 | ComponentsComponent, 57 | ItemPropertiesComponent, 58 | JsonFormatterDirective, 59 | ExamplesPageComponent, 60 | ExampleViewComponent, 61 | SidenavLayoutComponent 62 | ], 63 | imports: [ 64 | BrowserModule, 65 | DynamicComponentsCoreModule, 66 | ToolsModule, 67 | DynamicComponentsBootstrapModule, 68 | BrowserAnimationsModule, 69 | AppRoutingModule, 70 | MatToolbarModule, 71 | MatButtonModule, 72 | MatTabsModule, 73 | MatSidenavModule, 74 | MatListModule, 75 | MatCardModule, 76 | MatTableModule, 77 | MatExpansionModule, 78 | MatIconModule, 79 | ResizableModule, 80 | MarkdownModule.forRoot() 81 | ], 82 | providers: [], 83 | bootstrap: [AppComponent] 84 | }) 85 | export class AppModule { } 86 | -------------------------------------------------------------------------------- /src/app/components/cards/cards.component.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/app/components/cards/cards.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/_vars.scss"; 2 | 3 | .cards-wrapper { 4 | flex-wrap: wrap; 5 | 6 | a { 7 | text-decoration: none; 8 | margin: 25px; 9 | } 10 | 11 | mat-card { 12 | width: 160px; 13 | mat-card-header ::ng-deep .mat-card-header-text { 14 | margin: 0; 15 | } 16 | 17 | mat-card-content { 18 | color: $text; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/cards/cards.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | 5 | import { CardsComponent } from './cards.component'; 6 | 7 | describe('CardsComponent', () => { 8 | let component: CardsComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ RouterTestingModule, MatCardModule ], 14 | declarations: [ CardsComponent ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(CardsComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/components/cards/cards.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dc-cards', 5 | templateUrl: './cards.component.html', 6 | styleUrls: ['./cards.component.scss'] 7 | }) 8 | export class CardsComponent { 9 | 10 | @Input() cards; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/components/categories.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ComponentDescriptor } from '@ngx-dynamic-components/core'; 3 | import { getSlugFromStr, getCategories } from '../utils'; 4 | 5 | @Component({ 6 | selector: 'dc-categories', 7 | template: ` 8 |

            Categories

            9 |
            10 | 11 |
            12 | `, 13 | styles: [` 14 | :host { 15 | padding: 25px; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | `] 20 | }) 21 | export class CategoriesComponent implements OnInit { 22 | 23 | categories = []; 24 | 25 | constructor() { } 26 | 27 | ngOnInit(): void { 28 | this.categories = getCategories() 29 | .map(c => ({ 30 | header: `${c.packageName} ${c.name}`, 31 | content: this.getComponentsNames(c.components), 32 | link: [c.packageName, getSlugFromStr(c.name)] 33 | })); 34 | } 35 | 36 | getComponentsNames(components: ComponentDescriptor[]): string { 37 | return components.map(c => c.name).join(', '); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/components-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { getPackageCategories } from '../utils'; 3 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'dc-components-page', 9 | template: ` 10 | 11 | 12 |

            {{section.packageName}}

            13 | 14 |
            15 | 16 |
            17 | `, 18 | styles: [` 19 | h3 { 20 | margin-left: 25px; 21 | margin-bottom: -25px; 22 | } 23 | `] 24 | }) 25 | export class ComponentsPageComponent implements OnInit { 26 | 27 | sections = []; 28 | isHandSet$: Observable = this.breakpointObserver.observe(Breakpoints.Handset).pipe(map(r => r.matches)); 29 | mode: Observable = this.isHandSet$.pipe(map(r => r ? 'over' : 'side')); 30 | opened = false; 31 | 32 | constructor(private breakpointObserver: BreakpointObserver) {} 33 | 34 | ngOnInit(): void { 35 | const mapToGroup = ({name, components, packageName}) => ({name, list: components, url: [packageName]}); 36 | this.sections = getPackageCategories().map(({packageName, categories}) => ({ 37 | packageName, 38 | categories: categories.map(mapToGroup) 39 | })); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/components.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ComponentDescriptor } from '@ngx-dynamic-components/core'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | import { getSlugFromStr, COMPONENTS_LIST } from '../utils'; 7 | 8 | @Component({ 9 | selector: 'dc-components', 10 | template: ` 11 |

            Available components for {{category$ | async}}

            12 | 13 | `, 14 | styles: [` 15 | :host { 16 | padding: 25px; 17 | display: flex; 18 | flex-direction: column; 19 | } 20 | `] 21 | }) 22 | export class ComponentsComponent implements OnInit { 23 | 24 | components$: Observable; 25 | category$: Observable; 26 | 27 | constructor(private route: ActivatedRoute) { } 28 | 29 | ngOnInit(): void { 30 | const filterComponents = (p) => (c: ComponentDescriptor) => (!p.category || getSlugFromStr(c.category) === p.category) 31 | && c.packageName === p.packageName; 32 | this.components$ = this.route.params 33 | .pipe( 34 | map(p => COMPONENTS_LIST.filter(filterComponents(p)) 35 | .map(c => ({ 36 | header: `${c.packageName} ${c.name}`, 37 | content: c.description, 38 | link: ['/components', c.packageName, c.name] 39 | })))); 40 | 41 | this.category$ = this.route.params 42 | .pipe( 43 | map(p => p.category ? p.category.replace(/-/g, ' ') : 'all categories')); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/app/components/editor-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import * as contactUs from '../examples/contact-us.form.config'; 3 | 4 | @Component({ 5 | selector: 'dc-editor-page', 6 | template: ` 7 | 8 | 13 | `, 14 | styles: [` 15 | dc-preview-editor { 16 | margin: 25px; 17 | } 18 | :host { 19 | flex-direction: column; 20 | } 21 | :host ::ng-deep .mat-tab-body-wrapper { 22 | min-height: 400px; 23 | } 24 | `] 25 | }) 26 | export class EditorPageComponent implements OnInit { 27 | 28 | defaultModel = contactUs.uIModel; 29 | defaultDataModel = contactUs.dataModel; 30 | scripts: string; 31 | 32 | constructor() { } 33 | 34 | ngOnInit(): void { 35 | this.scripts = contactUs.scripts; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/components/example-view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { EXAMPLES_LIST } from '../examples/examples.config'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { map, filter } from 'rxjs/operators'; 5 | import { Observable } from 'rxjs'; 6 | 7 | @Component({ 8 | selector: 'dc-example', 9 | template: ` 10 | 11 | 18 |

            Data Model:

            19 |
            20 |
            21 | `, 22 | styles: [` 23 | :host { 24 | padding: 25px; 25 | flex-grow: 1; 26 | display: flex; 27 | } 28 | dc-preview-editor ::ng-deep .mat-tab-body-wrapper { 29 | min-height: 400px; 30 | } 31 | `] 32 | }) 33 | export class ExampleViewComponent implements OnInit { 34 | example: Observable; 35 | 36 | constructor(private route: ActivatedRoute, private router: Router) { } 37 | 38 | ngOnInit(): void { 39 | this.example = this.route.params 40 | .pipe( 41 | filter(p => p.example), 42 | map(p => { 43 | const config = EXAMPLES_LIST.find(({name}) => p.example === name); 44 | return config; 45 | })); 46 | 47 | this.route.params.subscribe(({example}) => { 48 | if (!example) { 49 | // Redirect to first example if it is not selected. 50 | this.router.navigate(['examples', EXAMPLES_LIST[0].name]); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/components/examples-page/examples-page.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/components/examples-page/examples-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FalconSoft/ngx-dynamic-components/4b8fe064534d9dfc393697ffca45c49403ead79e/src/app/components/examples-page/examples-page.component.scss -------------------------------------------------------------------------------- /src/app/components/examples-page/examples-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { MatExpansionModule } from '@angular/material/expansion'; 6 | import { MatListModule } from '@angular/material/list'; 7 | import { MatSidenavModule } from '@angular/material/sidenav'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | import { MatToolbarModule } from '@angular/material/toolbar'; 10 | 11 | import { SidenavLayoutComponent } from '../sidenav-layout.component'; 12 | import { SideBarComponent } from '../side-bar/side-bar.component'; 13 | import { ExamplesPageComponent } from './examples-page.component'; 14 | import { PageHeaderComponent } from '../page-header.component'; 15 | 16 | describe('ExamplesPageComponent', () => { 17 | let component: ExamplesPageComponent; 18 | let fixture: ComponentFixture; 19 | 20 | beforeEach(waitForAsync(() => { 21 | TestBed.configureTestingModule({ 22 | imports: [RouterTestingModule, MatExpansionModule, MatListModule, MatSidenavModule, MatIconModule, MatToolbarModule, 23 | NoopAnimationsModule], 24 | declarations: [ ExamplesPageComponent, SideBarComponent, SidenavLayoutComponent, PageHeaderComponent ] 25 | }) 26 | .compileComponents(); 27 | })); 28 | 29 | beforeEach(() => { 30 | fixture = TestBed.createComponent(ExamplesPageComponent); 31 | component = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/app/components/examples-page/examples-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { getGroupedExamples } from '../../examples/examples.config'; 3 | import { GroupItem } from '../side-bar/side-bar.component'; 4 | 5 | @Component({ 6 | selector: 'dc-examples-page', 7 | templateUrl: './examples-page.component.html', 8 | styleUrls: ['./examples-page.component.scss'] 9 | }) 10 | export class ExamplesPageComponent implements OnInit { 11 | 12 | examples: GroupItem[]; 13 | 14 | constructor() { } 15 | 16 | ngOnInit(): void { 17 | this.examples = getGroupedExamples().map(({name, examples}) => ({name, list: examples})); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/guides-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dc-guides-page', 5 | template: ` 6 | 7 | `, 8 | styles: [] 9 | }) 10 | export class GuidesPageComponent { } 11 | -------------------------------------------------------------------------------- /src/app/components/home-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | const packageJson = require('../../../package.json'); 3 | 4 | @Component({ 5 | selector: 'dc-home-page', 6 | template: ` 7 |
            8 |

            NGX Dynamic Components

            9 |

            Configuration based dynamic components library for Angular.

            10 | Current version {{version}}. 11 |
            12 |
            13 | 14 |
            15 | `, 16 | styles: [` 17 | header { 18 | padding: 60px 0; 19 | background: #3f51b5; 20 | text-align: center; 21 | color: white; 22 | } 23 | h1 { 24 | line-height: 60px; 25 | font-size: 60px; 26 | } 27 | `] 28 | }) 29 | export class HomePageComponent { 30 | version = packageJson.version; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/components/item-properties/item-properties.component.html: -------------------------------------------------------------------------------- 1 |
            2 |

            Component properties

            3 | 4 |
            5 | 6 |
            7 |

            Component events

            8 | 9 |
            10 | 11 | 12 | 13 | 14 | Name 15 | {{element.name}} 16 | 17 | 18 | Example 19 | '{{element.example}}' 20 | 21 | 22 | 23 | Description 24 | {{element.description}} 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/app/components/item-properties/item-properties.component.scss: -------------------------------------------------------------------------------- 1 | @import 'src/_vars.scss'; 2 | 3 | mat-header-cell { 4 | background: $gray; 5 | color: $text; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/components/item-properties/item-properties.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { MatTableModule } from '@angular/material/table'; 3 | 4 | import { ItemPropertiesComponent } from './item-properties.component'; 5 | import { Observable } from 'rxjs'; 6 | 7 | describe('ItemPropertiesComponent', () => { 8 | let component: ItemPropertiesComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ MatTableModule ], 14 | declarations: [ ItemPropertiesComponent ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(ItemPropertiesComponent); 21 | component = fixture.componentInstance; 22 | component.component$ = new Observable(); 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/components/item-properties/item-properties.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, SimpleChanges, OnChanges } from '@angular/core'; 2 | import { ComponentDescriptor, PropTypes } from '@ngx-dynamic-components/core'; 3 | 4 | @Component({ 5 | selector: 'dc-item-properties', 6 | templateUrl: './item-properties.component.html', 7 | styleUrls: ['./item-properties.component.scss'] 8 | }) 9 | export class ItemPropertiesComponent implements OnInit, OnChanges { 10 | 11 | @Input() component?: ComponentDescriptor; 12 | displayedColumns = ['name', 'example', 'description']; 13 | 14 | properties?: []; 15 | events?: []; 16 | 17 | constructor() { } 18 | 19 | ngOnInit(): void { 20 | this.setProperties(); 21 | } 22 | 23 | ngOnChanges({component}: SimpleChanges): void { 24 | if (component.firstChange === false) { 25 | this.setProperties(); 26 | } 27 | } 28 | 29 | private setProperties(): void { 30 | const properties = this.component.itemProperties.prototype.properties; 31 | this.properties = properties.filter(p => p.type !== PropTypes.EVENT); 32 | this.events = properties.filter(p => p.type === PropTypes.EVENT); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dc-nav', 5 | template: ` 6 | 7 | 8 | NGX Dynamic Components 9 | Components 10 | Editor 11 | Examples 12 | Github 13 | 14 | 15 | `, 16 | styles: [` 17 | .example-spacer { 18 | flex: 1 1 auto; 19 | } 20 | mat-toolbar { 21 | min-height: 56px; 22 | } 23 | mat-toolbar-row { 24 | box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px 0 rgba(0,0,0,.14), 0 1px 18px 0 rgba(0,0,0,.12); 25 | min-height: 56px; 26 | position: relative; 27 | flex-wrap: wrap; 28 | height: auto; 29 | z-index: 2; 30 | } 31 | `] 32 | }) 33 | export class NavComponent { } 34 | -------------------------------------------------------------------------------- /src/app/components/page-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dc-page-header', 5 | template: ` 6 | 7 | 8 | 13 |

            {{title}}

            14 |
            15 |
            16 | `, 17 | styles: [` 18 | mat-toolbar-row { 19 | height: 80px; 20 | } 21 | 22 | h1 { 23 | font-weight: 300; 24 | margin: 0; 25 | padding: 28px 8px; 26 | font-size: 20px; 27 | } 28 | `] 29 | }) 30 | export class PageHeaderComponent { 31 | 32 | @Input() title?: string; 33 | 34 | @Input() toggle?: boolean; 35 | 36 | opened = false; 37 | 38 | // eslint-disable-next-line @angular-eslint/no-output-native 39 | @Output() openPage = new EventEmitter(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/side-bar/side-bar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{group.name}} 6 | 7 | 8 | 9 | 10 | {{item.name}} 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/components/side-bar/side-bar.component.scss: -------------------------------------------------------------------------------- 1 | @import "src/_vars.scss"; 2 | 3 | mat-accordion.side-bar { 4 | display: block; 5 | margin: $panelMargin; 6 | width: 240px; 7 | max-height: 75vh; 8 | overflow: auto; 9 | @media only screen and (max-width: 400px) { 10 | margin: 0; 11 | } 12 | 13 | mat-expansion-panel { 14 | background-color: $gray; 15 | border-radius: 0; 16 | border-bottom: solid 1px $darkGray; 17 | box-shadow: none; 18 | 19 | &:last-child { 20 | border-bottom: none; 21 | } 22 | 23 | a { 24 | color: $text; 25 | text-decoration: none; 26 | &.active,&:hover { 27 | color: $active; 28 | } 29 | } 30 | 31 | mat-expansion-panel-header { 32 | padding: 0 15px; 33 | .title { 34 | color: $text; 35 | font-family: Arial; 36 | font-weight: 700; 37 | font-size: 13px; 38 | line-height: 16px; 39 | margin: 0; 40 | } 41 | } 42 | 43 | mat-list { 44 | padding: 0; 45 | } 46 | 47 | mat-list-item { 48 | height: auto; 49 | width: auto; 50 | font-size: 13px; 51 | line-height: 16px; 52 | padding: 5px 15px 5px 20px; 53 | } 54 | } 55 | 56 | ::ng-deep { 57 | .mat-expansion-panel-body { 58 | padding: 0; 59 | 60 | .mat-list-item-content { 61 | padding: 0; 62 | } 63 | } 64 | 65 | .mat-expansion-indicator::after { 66 | border-width: 0 1px 1px 0; 67 | margin-bottom: 5px; 68 | } 69 | 70 | .mat-expansion-panel-spacing { 71 | margin: 0; 72 | padding-bottom: 5px; 73 | } 74 | } 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/app/components/side-bar/side-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { MatExpansionModule } from '@angular/material/expansion'; 4 | import { MatListModule } from '@angular/material/list'; 5 | import { SideBarComponent } from './side-bar.component'; 6 | 7 | describe('SideBarComponent', () => { 8 | let component: SideBarComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [RouterTestingModule, MatExpansionModule, MatListModule], 14 | declarations: [ SideBarComponent ] 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(SideBarComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/app/components/side-bar/side-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dc-side-bar', 5 | templateUrl: './side-bar.component.html', 6 | styleUrls: ['./side-bar.component.scss'] 7 | }) 8 | export class SideBarComponent { 9 | 10 | @Input() groups?: GroupItem[]; 11 | 12 | getUrl(group: GroupItem, item: {name: string}): string[] { 13 | if (group.url) { 14 | return [...group.url, item.name]; 15 | } 16 | return [item.name]; 17 | } 18 | } 19 | 20 | export interface GroupItem { 21 | name: string; 22 | list: {name: string}[]; 23 | url?: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/components/sidenav-layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { GroupItem } from './side-bar/side-bar.component'; 3 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'dc-sidenav-layout', 9 | template: ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ` 42 | }) 43 | export class SidenavLayoutComponent { 44 | 45 | @Input() title?: string; 46 | categories?: GroupItem[]; 47 | isHandSet$: Observable = this.breakpointObserver.observe(Breakpoints.Handset).pipe(map(r => r.matches)); 48 | 49 | constructor(private breakpointObserver: BreakpointObserver) {} 50 | } 51 | -------------------------------------------------------------------------------- /src/app/directives/json-formatter.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, OnChanges, Input } from '@angular/core'; 2 | import JSONFormatter from 'json-formatter-js'; 3 | 4 | @Directive({ 5 | selector: '[dcJsonFormatter]' 6 | }) 7 | export class JsonFormatterDirective implements OnChanges { 8 | @Input() json: any; 9 | 10 | constructor(private elRef: ElementRef) { } 11 | 12 | ngOnChanges(): void { 13 | if (this.json) { 14 | const formatter = new JSONFormatter(this.json); 15 | this.elRef.nativeElement.innerHTML = ''; 16 | this.elRef.nativeElement.appendChild(formatter.render()); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/examples/contact-us.form.config.ts: -------------------------------------------------------------------------------- 1 | export const uIModel = ` 2 |
            3 |

            Contact us

            4 |
            5 |
            6 | 7 | 8 |
            9 |
            10 | 11 | 12 |
            13 |
            14 | 15 | 16 |
            17 |
            18 |
            19 |
            20 | 21 |