├── .editorconfig ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── developpackage.json ├── karma.conf.js ├── ng-formly-nativescript.ts ├── package.json ├── preparedevelop.js ├── preparepublish.js ├── publishpackage.json ├── references.d.ts ├── src ├── app │ ├── App_Resources │ │ ├── Android │ │ │ ├── AndroidManifest.xml │ │ │ ├── app.gradle │ │ │ ├── drawable-hdpi │ │ │ │ ├── background.png │ │ │ │ ├── icon.png │ │ │ │ └── logo.png │ │ │ ├── drawable-ldpi │ │ │ │ ├── background.png │ │ │ │ ├── icon.png │ │ │ │ └── logo.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── background.png │ │ │ │ ├── icon.png │ │ │ │ └── logo.png │ │ │ ├── drawable-nodpi │ │ │ │ └── splash_screen.xml │ │ │ ├── drawable-xhdpi │ │ │ │ ├── background.png │ │ │ │ ├── icon.png │ │ │ │ └── logo.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── background.png │ │ │ │ ├── icon.png │ │ │ │ └── logo.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ ├── background.png │ │ │ │ ├── icon.png │ │ │ │ └── logo.png │ │ │ ├── values-v21 │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ └── iOS │ │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── icon-29.png │ │ │ │ ├── icon-29@2x.png │ │ │ │ ├── icon-29@3x.png │ │ │ │ ├── icon-40.png │ │ │ │ ├── icon-40@2x.png │ │ │ │ ├── icon-40@3x.png │ │ │ │ ├── icon-50.png │ │ │ │ ├── icon-50@2x.png │ │ │ │ ├── icon-57.png │ │ │ │ ├── icon-57@2x.png │ │ │ │ ├── icon-60@2x.png │ │ │ │ ├── icon-60@3x.png │ │ │ │ ├── icon-72.png │ │ │ │ ├── icon-72@2x.png │ │ │ │ ├── icon-76.png │ │ │ │ ├── icon-76@2x.png │ │ │ │ └── icon-83.5@2x.png │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.launchimage │ │ │ │ ├── Contents.json │ │ │ │ ├── Default-568h@2x.png │ │ │ │ ├── Default-667h@2x.png │ │ │ │ ├── Default-736h@3x.png │ │ │ │ ├── Default-Landscape.png │ │ │ │ ├── Default-Landscape@2x.png │ │ │ │ ├── Default-Landscape@3x.png │ │ │ │ ├── Default-Portrait.png │ │ │ │ ├── Default-Portrait@2x.png │ │ │ │ ├── Default.png │ │ │ │ └── Default@2x.png │ │ │ ├── LaunchScreen.AspectFill.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchScreen-AspectFill.png │ │ │ │ └── LaunchScreen-AspectFill@2x.png │ │ │ └── LaunchScreen.Center.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchScreen-Center.png │ │ │ │ └── LaunchScreen-Center@2x.png │ │ │ ├── Info.plist │ │ │ ├── LaunchScreen.storyboard │ │ │ └── build.xcconfig │ ├── app.component.ts │ ├── app.css │ ├── core │ │ ├── components │ │ │ ├── formly.attributes.spec.ts │ │ │ ├── formly.attributes.ts │ │ │ ├── formly.field.config.ts │ │ │ ├── formly.field.spec.ts │ │ │ ├── formly.field.ts │ │ │ ├── formly.form.spec.ts │ │ │ ├── formly.form.ts │ │ │ └── formly.group.ts │ │ ├── core.ts │ │ ├── services │ │ │ ├── formly.config.spec.ts │ │ │ ├── formly.config.ts │ │ │ ├── formly.event.emitter.ts │ │ │ ├── formly.form.builder.spec.ts │ │ │ ├── formly.form.builder.ts │ │ │ ├── formly.messages.spec.ts │ │ │ ├── formly.single.focus.dispatcher.ts │ │ │ └── formly.validation-messages.ts │ │ ├── templates │ │ │ ├── field.ts │ │ │ ├── field.type.ts │ │ │ └── field.wrapper.ts │ │ ├── test-utils.ts │ │ ├── utils.spec.ts │ │ └── utils.ts │ ├── index.ts │ ├── main.ts │ ├── package.json │ └── ui-bootstrap │ │ ├── formly.validation-message.spec.ts │ │ ├── formly.validation-message.ts │ │ ├── run │ │ ├── addon.ts │ │ ├── description.ts │ │ └── validation.ts │ │ ├── types │ │ ├── checkbox.ts │ │ ├── input.ts │ │ ├── multicheckbox.ts │ │ ├── radio.ts │ │ ├── select.ts │ │ ├── textarea.ts │ │ └── types.ts │ │ ├── ui-bootstrap.config.ts │ │ ├── ui-bootstrap.module.ts │ │ ├── ui-bootstrap.ts │ │ └── wrappers │ │ ├── addons.ts │ │ ├── description.ts │ │ ├── fieldset.ts │ │ ├── label.ts │ │ ├── message-validation.ts │ │ ├── sideLabel.ts │ │ └── wrappers.ts ├── package.json ├── references.d.ts └── tsconfig.json ├── test-main.js ├── tsconfig.json └── typedoc.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,css,scss,html}] 14 | charset = utf-8 15 | 16 | # 4 space indentation 17 | [*.{js,css,scss,html}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Matches the exact files either package.json or .travis.yml 22 | [{package.json,.travis.yml,bower.json}] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Users Environment Variables 23 | .lock-wscript 24 | 25 | # OS generated files # 26 | .DS_Store 27 | ehthumbs.db 28 | Icon? 29 | Thumbs.db 30 | 31 | # Node Files # 32 | /node_modules 33 | /bower_components 34 | 35 | # Coverage # 36 | /coverage/ 37 | 38 | # Typing # 39 | /src/typings/tsd/ 40 | /typings/ 41 | /tsd_typings/ 42 | 43 | # Dist # 44 | /dist 45 | /public/__build__/ 46 | /src/*/__build__/ 47 | __build__/** 48 | .webpack.json 49 | 50 | # Doc # 51 | /doc/ 52 | 53 | # IDE # 54 | .idea/ 55 | *.swp 56 | .vscode 57 | 58 | *.d.ts 59 | !references.d.ts 60 | *.js 61 | *.js.map 62 | !*.e2e.js 63 | !karma*.js 64 | !test-main.js 65 | src/lib 66 | src/platforms 67 | src/hooks 68 | src/node_modules 69 | 70 | !preparepublish.js 71 | !preparedevelop.js 72 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | .tsdrc 30 | 31 | #IntelliJ configuration files 32 | .idea 33 | 34 | dist 35 | dev 36 | docs 37 | doc 38 | lib 39 | test 40 | 41 | Thumbs.db 42 | .DS_Store 43 | *.ts 44 | !*.d.ts 45 | *.yml 46 | src/* 47 | src/package.json 48 | src/app/App_Resources 49 | src/app/app.* 50 | src/app/main.* 51 | src/app/assets 52 | src/app/package.json 53 | !src/app/core/* 54 | !src/app/ui-bootstrap/* 55 | !src/app/index.ts 56 | *.spec.* 57 | *.e2e.* 58 | CONTRIBUTING.md 59 | karma.conf.js 60 | protractor.conf.js 61 | test-main.js 62 | tsconfig.json 63 | tslint.json 64 | typedoc.json 65 | typings.json 66 | typings 67 | .travis.yml 68 | .jshintrc 69 | .editorconfig 70 | *.png 71 | resources 72 | preparepublish.js 73 | preparedevelop.js 74 | publishpackage.json 75 | developpackage.json 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Submitting Pull Requests 2 | 3 | **Please follow these basic steps to simplify pull request reviews - if you don't you'll probably just be asked to anyway.** 4 | 5 | * Please rebase your branch against the current master 6 | * Please ensure that the test suite passes **and** that code is lint free before submitting a PR by running: 7 | * ```npm test``` 8 | * If you've added new functionality, **please** include tests which validate its behaviour 9 | * Make reference to possible [issues](https://github.com/NathanWalker/nativescript-ng2-plugin-seed/issues) on PR comment 10 | 11 | ### Submitting bug reports 12 | 13 | * Please detail the affected browser(s) and operating system(s) 14 | * Please be sure to state which version of node **and** npm you're using 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nathan Walker 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-formly logo 2 | 3 | ## ng-formly-nativescript 4 | [![Angular Style Guide](https://mgechev.github.io/angular2-style-guide/images/badge.svg)](https://angular.io/styleguide) 5 | 6 | ### Setup 7 | 8 | ``` 9 | npm i -g nativescript 10 | npm i 11 | ``` 12 | 13 | ### Run demo app 14 | 15 | Run this once to get the demo setup: 16 | 17 | ``` 18 | npm run setup 19 | ``` 20 | 21 | You can then run the demo with: 22 | 23 | ``` 24 | npm run demo.ios 25 | 26 | // or... 27 | 28 | npm run demo.android (requires GenyMotion emulator to be setup or other Android virtual device) 29 | ``` 30 | 31 | ### Status 32 | 33 | This project is currently under active development. 34 | 35 | 36 | ![Demo](https://cdn.filestackcontent.com/yY7B562kQUyzaubOZjRK) 37 | 38 | 39 | -------------------------------------------------------------------------------- /developpackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-formly-nativescript", 3 | "version": "1.0.0", 4 | "description": "NativeScript powered forms for Angular.", 5 | "main": "ng-formly-nativescript.js", 6 | "typings": "index.d.ts", 7 | "nativescript": { 8 | "platforms": { 9 | "android": "2.4.0", 10 | "ios": "2.4.0" 11 | } 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/formly-js/ng-formly-nativescript.git" 16 | }, 17 | "keywords": [ 18 | "angular", 19 | "ng2", 20 | "nativescript", 21 | "forms", 22 | "json" 23 | ], 24 | "author": "Nathan Walker ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/formly-js/ng-formly-nativescript/issues" 28 | }, 29 | "homepage": "https://github.com/formly-js/ng-formly-nativescript#readme", 30 | "scripts": { 31 | "build": "tsc", 32 | "demo.ios": "npm run preparedemo && cd src && tns emulate ios", 33 | "demo.android": "npm run preparedemo && cd src && tns run android", 34 | "test": "tsc && karma start", 35 | "test-watch": "karma start --no-single-run --auto-watch", 36 | "ci": "npm run test", 37 | "docs": "typedoc --options typedoc.json src/app/app.ts", 38 | "repair": "cd src && tns plugin add .. && tns install", 39 | "preparedemo": "npm run build && cd src && tns plugin remove ng-formly-nativescript && tns plugin add .. && tns install", 40 | "setup": "cd src && npm i" 41 | }, 42 | "dependencies": { 43 | "@angular/core": "~2.2.1", 44 | "@angular/common": "~2.2.1", 45 | "@angular/compiler": "~2.2.1", 46 | "@angular/forms": "~2.2.1", 47 | "@angular/http": "~2.2.1", 48 | "@angular/platform-browser": "~2.2.1", 49 | "@angular/platform-browser-dynamic": "~2.2.1", 50 | "@angular/router": "~3.2.1", 51 | "es6-promise": "~3.1.2", 52 | "es6-shim": "^0.35.0", 53 | "nativescript-angular": "next", 54 | "parse5": "1.4.2", 55 | "punycode": "1.3.2", 56 | "querystring": "0.2.0", 57 | "reflect-metadata": "0.1.3", 58 | "rimraf": "^2.5.1", 59 | "rxjs": "5.0.0-beta.12", 60 | "tns-core-modules": "2.4.0", 61 | "url": "0.10.3" 62 | }, 63 | "devDependencies": { 64 | "@types/jasmine": "^2.5.35", 65 | "autoprefixer": "^6.3.2", 66 | "jasmine-core": "^2.3.4", 67 | "jasmine-spec-reporter": "^2.4.0", 68 | "karma": "0.13.19", 69 | "karma-chrome-launcher": "^0.2.1", 70 | "karma-coverage": "^0.5.2", 71 | "karma-jasmine": "^0.3.7", 72 | "karma-phantomjs-launcher": "^1.0.0", 73 | "karma-sourcemap-loader": "^0.3.7", 74 | "karma-typescript-preprocessor": "^0.0.21", 75 | "node-sass": "^3.4.2", 76 | "phantomjs-prebuilt": "^2.1.4", 77 | "remap-istanbul": "^0.5.1", 78 | "rimraf": "^2.5.1", 79 | "systemjs-builder": "^0.15.7", 80 | "traceur": "^0.0.91", 81 | "tsconfig-lint": "^0.5.0", 82 | "tslint": "^3.4.0", 83 | "typedoc": "^0.3.12", 84 | "typescript": "^2.0.10", 85 | "zone.js": "^0.6.21" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | var _config = { 3 | 4 | // base path that will be used to resolve all patterns (eg. files, exclude) 5 | basePath: './', 6 | 7 | // frameworks to use 8 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 9 | frameworks: ['jasmine'], 10 | 11 | // list of files / patterns to load in the browser 12 | files: [ 13 | // Polyfills. 14 | 'node_modules/es6-shim/es6-shim.js', 15 | 16 | 'node_modules/reflect-metadata/Reflect.js', 17 | 18 | // System.js for module loading 19 | 'node_modules/systemjs/dist/system-polyfills.js', 20 | 'node_modules/systemjs/dist/system.src.js', 21 | 22 | // Zone.js dependencies 23 | 'node_modules/zone.js/dist/zone.js', 24 | 'node_modules/zone.js/dist/jasmine-patch.js', 25 | 'node_modules/zone.js/dist/async-test.js', 26 | 'node_modules/zone.js/dist/fake-async-test.js', 27 | 28 | // RxJs. 29 | { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false }, 30 | { pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false }, 31 | 32 | // paths loaded via module imports 33 | // Angular itself 34 | { pattern: 'node_modules/@angular/**/*.js', included: false, watched: true }, 35 | 36 | { pattern: 'node_modules/tns-core-modules/**/*.js', included: false, watched: false }, 37 | { pattern: 'src/app/**/*.js', included: false, watched: true }, 38 | { pattern: 'node_modules/systemjs/dist/system-polyfills.js', included: false, watched: false }, // PhantomJS2 (and possibly others) might require it 39 | 40 | 'test-main.js' 41 | ], 42 | 43 | // list of files to exclude 44 | exclude: [ 45 | 'node_modules/angular2/**/*spec.js' 46 | ], 47 | 48 | // preprocess matching files before serving them to the browser 49 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 50 | preprocessors: { 51 | 'src/**/(*spec).ts': ['typescript'], 52 | 'src/**/!(*spec).js': ['coverage'] 53 | }, 54 | 55 | typescriptPreprocessor: { 56 | // options passed to the typescript compiler 57 | options: { 58 | sourceMap: false, // (optional) Generates corresponding .map file. 59 | target: 'ES5', // (optional) Specify ECMAScript target version: 'ES3' (default), or 'ES5' 60 | module: 'commonjs', // (optional) Specify module code generation: 'commonjs' or 'amd' 61 | noResolve: true, // (optional) Skip resolution and preprocessing. 62 | removeComments: true, // (optional) Do not emit comments to output. 63 | concatenateOutput: false // (optional) Concatenate and emit output to single file. By default true if module option is omited, otherwise false. 64 | }, 65 | // extra typing definitions to pass to the compiler (globs allowed) 66 | typings: [ 67 | 'typings/browser.d.ts', 68 | 'node_modules/tns-core-modules/tns-core-modules.d.ts' 69 | ], 70 | // transforming the filenames 71 | transformPath: function(path) { 72 | return path.replace(/\.ts$/, '.js'); 73 | } 74 | }, 75 | 76 | coverageReporter: { 77 | dir: 'coverage/', 78 | reporters: [ 79 | { type: 'text-summary' }, 80 | { type: 'json', subdir: '.', file: 'coverage-final.json' }, 81 | { type: 'html' } 82 | ] 83 | }, 84 | 85 | // test results reporter to use 86 | // possible values: 'dots', 'progress' 87 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 88 | reporters: ['progress', 'coverage'], 89 | 90 | // web server port 91 | port: 9876, 92 | 93 | // enable / disable colors in the output (reporters and logs) 94 | colors: true, 95 | 96 | // level of logging 97 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 98 | logLevel: config.LOG_INFO, 99 | 100 | // enable / disable watching file and executing tests whenever any file changes 101 | autoWatch: false, 102 | 103 | // start these browsers 104 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 105 | browsers: ['PhantomJS'], // you can also use Chrome 106 | 107 | // Continuous Integration mode 108 | // if true, Karma captures browsers, runs the tests and exits 109 | singleRun: true 110 | }; 111 | 112 | config.set(_config); 113 | 114 | }; 115 | -------------------------------------------------------------------------------- /ng-formly-nativescript.ts: -------------------------------------------------------------------------------- 1 | export * from './src/app/index'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-formly-nativescript", 3 | "version": "1.0.0", 4 | "description": "NativeScript powered forms for Angular.", 5 | "main": "ng-formly-nativescript.js", 6 | "typings": "index.d.ts", 7 | "nativescript": { 8 | "platforms": { 9 | "android": "2.4.0", 10 | "ios": "2.4.0" 11 | } 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/formly-js/ng-formly-nativescript.git" 16 | }, 17 | "keywords": [ 18 | "angular", 19 | "ng2", 20 | "nativescript", 21 | "forms", 22 | "json" 23 | ], 24 | "author": "Nathan Walker ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/formly-js/ng-formly-nativescript/issues" 28 | }, 29 | "homepage": "https://github.com/formly-js/ng-formly-nativescript#readme", 30 | "scripts": { 31 | "build": "tsc", 32 | "demo.ios": "npm run preparedemo && cd src && tns emulate ios", 33 | "demo.android": "npm run preparedemo && cd src && tns run android", 34 | "test": "tsc && karma start", 35 | "test-watch": "karma start --no-single-run --auto-watch", 36 | "ci": "npm run test", 37 | "docs": "typedoc --options typedoc.json src/app/app.ts", 38 | "repair": "cd src && tns plugin add .. && tns install", 39 | "preparedemo": "npm run build && cd src && tns plugin remove ng-formly-nativescript && tns plugin add .. && tns install", 40 | "setup": "cd src && npm i && cd .. && npm i" 41 | }, 42 | "dependencies": { 43 | "@angular/core": "~2.2.1", 44 | "@angular/common": "~2.2.1", 45 | "@angular/compiler": "~2.2.1", 46 | "@angular/forms": "~2.2.1", 47 | "@angular/http": "~2.2.1", 48 | "@angular/platform-browser": "~2.2.1", 49 | "@angular/platform-browser-dynamic": "~2.2.1", 50 | "@angular/router": "~3.2.1", 51 | "es6-promise": "~3.1.2", 52 | "es6-shim": "^0.35.0", 53 | "nativescript-angular": "next", 54 | "parse5": "1.4.2", 55 | "punycode": "1.3.2", 56 | "querystring": "0.2.0", 57 | "reflect-metadata": "0.1.3", 58 | "rimraf": "^2.5.1", 59 | "rxjs": "5.0.0-beta.12", 60 | "tns-core-modules": "2.4.0", 61 | "url": "0.10.3" 62 | }, 63 | "devDependencies": { 64 | "@types/jasmine": "^2.5.35", 65 | "autoprefixer": "^6.3.2", 66 | "jasmine-core": "^2.3.4", 67 | "jasmine-spec-reporter": "^2.4.0", 68 | "karma": "0.13.19", 69 | "karma-chrome-launcher": "^0.2.1", 70 | "karma-coverage": "^0.5.2", 71 | "karma-jasmine": "^0.3.7", 72 | "karma-phantomjs-launcher": "^1.0.0", 73 | "karma-sourcemap-loader": "^0.3.7", 74 | "karma-typescript-preprocessor": "^0.0.21", 75 | "node-sass": "^3.4.2", 76 | "phantomjs-prebuilt": "^2.1.4", 77 | "remap-istanbul": "^0.5.1", 78 | "rimraf": "^2.5.1", 79 | "systemjs-builder": "^0.15.7", 80 | "traceur": "^0.0.91", 81 | "tsconfig-lint": "^0.5.0", 82 | "tslint": "^3.4.0", 83 | "typedoc": "^0.3.12", 84 | "typescript": "^2.0.10", 85 | "zone.js": "^0.6.21" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /preparedevelop.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | 5 | function copyFile(src, dest, forceOverWrite) { 6 | if (!forceOverWrite && fs.existsSync(dest)) return; 7 | var buffer = fs.readFileSync(src); 8 | fs.writeFileSync(dest, buffer); 9 | 10 | } 11 | 12 | copyFile( './developpackage.json', './package.json',true); 13 | -------------------------------------------------------------------------------- /preparepublish.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | 5 | function copyFile(src, dest, forceOverWrite) { 6 | if (!forceOverWrite && fs.existsSync(dest)) return; 7 | var buffer = fs.readFileSync(src); 8 | fs.writeFileSync(dest, buffer); 9 | 10 | } 11 | 12 | copyFile( './publishpackage.json', './package.json',true); -------------------------------------------------------------------------------- /publishpackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-formly-nativescript", 3 | "version": "1.0.0", 4 | "description": "NativeScript powered forms for Angular.", 5 | "main": "ng-formly-nativescript.js", 6 | "typings": "index.d.ts", 7 | "nativescript": { 8 | "platforms": { 9 | "android": "2.4.0", 10 | "ios": "2.4.0" 11 | } 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/formly-js/ng-formly-nativescript.git" 16 | }, 17 | "keywords": [ 18 | "angular", 19 | "ng2", 20 | "nativescript", 21 | "forms", 22 | "json" 23 | ], 24 | "author": "Nathan Walker ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/formly-js/ng-formly-nativescript/issues" 28 | }, 29 | "homepage": "https://github.com/formly-js/ng-formly-nativescript#readme", 30 | "scripts": { 31 | }, 32 | "dependencies": { 33 | }, 34 | "devDependencies": { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /references.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/app/App_Resources/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/app/App_Resources/Android/app.gradle: -------------------------------------------------------------------------------- 1 | // Add your native dependencies here: 2 | 3 | // Uncomment to add recyclerview-v7 dependency 4 | //dependencies { 5 | // compile 'com.android.support:recyclerview-v7:+' 6 | //} 7 | 8 | android { 9 | defaultConfig { 10 | generatedDensities = [] 11 | applicationId = "org.nativescript.demo" 12 | } 13 | aaptOptions { 14 | additionalParameters "--no-version-vectors" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-hdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-hdpi/background.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-hdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-hdpi/logo.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-ldpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-ldpi/background.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-ldpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-ldpi/logo.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-mdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-mdpi/background.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-mdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-mdpi/logo.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-nodpi/splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xhdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xhdpi/background.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xhdpi/logo.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xxhdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xxhdpi/background.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xxhdpi/icon.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xxhdpi/logo.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xxxhdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xxxhdpi/background.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xxxhdpi/icon.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/drawable-xxxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/Android/drawable-xxxhdpi/logo.png -------------------------------------------------------------------------------- /src/app/App_Resources/Android/values-v21/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3d5afe 4 | -------------------------------------------------------------------------------- /src/app/App_Resources/Android/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /src/app/App_Resources/Android/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #F5F5F5 4 | #757575 5 | #33B5E5 6 | #272734 7 | -------------------------------------------------------------------------------- /src/app/App_Resources/Android/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 21 | 22 | 23 | 31 | 32 | 34 | 35 | 36 | 42 | 43 | 45 | 46 | -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "29x29", 5 | "idiom" : "iphone", 6 | "filename" : "icon-29.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "29x29", 11 | "idiom" : "iphone", 12 | "filename" : "icon-29@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon-29@3x.png", 19 | "scale" : "3x" 20 | }, 21 | { 22 | "size" : "40x40", 23 | "idiom" : "iphone", 24 | "filename" : "icon-40@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon-40@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "57x57", 35 | "idiom" : "iphone", 36 | "filename" : "icon-57.png", 37 | "scale" : "1x" 38 | }, 39 | { 40 | "size" : "57x57", 41 | "idiom" : "iphone", 42 | "filename" : "icon-57@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon-60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "icon-60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "29x29", 59 | "idiom" : "ipad", 60 | "filename" : "icon-29.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "icon-29@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "40x40", 71 | "idiom" : "ipad", 72 | "filename" : "icon-40.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "icon-40@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "50x50", 83 | "idiom" : "ipad", 84 | "filename" : "icon-50.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "50x50", 89 | "idiom" : "ipad", 90 | "filename" : "icon-50@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "72x72", 95 | "idiom" : "ipad", 96 | "filename" : "icon-72.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "72x72", 101 | "idiom" : "ipad", 102 | "filename" : "icon-72@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "76x76", 107 | "idiom" : "ipad", 108 | "filename" : "icon-76.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "76x76", 113 | "idiom" : "ipad", 114 | "filename" : "icon-76@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "83.5x83.5", 119 | "idiom" : "ipad", 120 | "filename" : "icon-83.5@2x.png", 121 | "scale" : "2x" 122 | } 123 | ], 124 | "info" : { 125 | "version" : 1, 126 | "author" : "xcode" 127 | } 128 | } -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-50.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-57.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-72.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "736h", 7 | "filename" : "Default-736h@3x.png", 8 | "minimum-system-version" : "8.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "extent" : "full-screen", 14 | "idiom" : "iphone", 15 | "subtype" : "736h", 16 | "filename" : "Default-Landscape@3x.png", 17 | "minimum-system-version" : "8.0", 18 | "orientation" : "landscape", 19 | "scale" : "3x" 20 | }, 21 | { 22 | "extent" : "full-screen", 23 | "idiom" : "iphone", 24 | "subtype" : "667h", 25 | "filename" : "Default-667h@2x.png", 26 | "minimum-system-version" : "8.0", 27 | "orientation" : "portrait", 28 | "scale" : "2x" 29 | }, 30 | { 31 | "orientation" : "portrait", 32 | "idiom" : "iphone", 33 | "filename" : "Default@2x.png", 34 | "extent" : "full-screen", 35 | "minimum-system-version" : "7.0", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "extent" : "full-screen", 40 | "idiom" : "iphone", 41 | "subtype" : "retina4", 42 | "filename" : "Default-568h@2x.png", 43 | "minimum-system-version" : "7.0", 44 | "orientation" : "portrait", 45 | "scale" : "2x" 46 | }, 47 | { 48 | "orientation" : "portrait", 49 | "idiom" : "ipad", 50 | "filename" : "Default-Portrait.png", 51 | "extent" : "full-screen", 52 | "minimum-system-version" : "7.0", 53 | "scale" : "1x" 54 | }, 55 | { 56 | "orientation" : "landscape", 57 | "idiom" : "ipad", 58 | "filename" : "Default-Landscape.png", 59 | "extent" : "full-screen", 60 | "minimum-system-version" : "7.0", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "orientation" : "portrait", 65 | "idiom" : "ipad", 66 | "filename" : "Default-Portrait@2x.png", 67 | "extent" : "full-screen", 68 | "minimum-system-version" : "7.0", 69 | "scale" : "2x" 70 | }, 71 | { 72 | "orientation" : "landscape", 73 | "idiom" : "ipad", 74 | "filename" : "Default-Landscape@2x.png", 75 | "extent" : "full-screen", 76 | "minimum-system-version" : "7.0", 77 | "scale" : "2x" 78 | }, 79 | { 80 | "orientation" : "portrait", 81 | "idiom" : "iphone", 82 | "filename" : "Default.png", 83 | "extent" : "full-screen", 84 | "scale" : "1x" 85 | }, 86 | { 87 | "orientation" : "portrait", 88 | "idiom" : "iphone", 89 | "filename" : "Default@2x.png", 90 | "extent" : "full-screen", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "orientation" : "portrait", 95 | "idiom" : "iphone", 96 | "filename" : "Default-568h@2x.png", 97 | "extent" : "full-screen", 98 | "subtype" : "retina4", 99 | "scale" : "2x" 100 | }, 101 | { 102 | "orientation" : "portrait", 103 | "idiom" : "ipad", 104 | "extent" : "to-status-bar", 105 | "scale" : "1x" 106 | }, 107 | { 108 | "orientation" : "portrait", 109 | "idiom" : "ipad", 110 | "filename" : "Default-Portrait.png", 111 | "extent" : "full-screen", 112 | "scale" : "1x" 113 | }, 114 | { 115 | "orientation" : "landscape", 116 | "idiom" : "ipad", 117 | "extent" : "to-status-bar", 118 | "scale" : "1x" 119 | }, 120 | { 121 | "orientation" : "landscape", 122 | "idiom" : "ipad", 123 | "filename" : "Default-Landscape.png", 124 | "extent" : "full-screen", 125 | "scale" : "1x" 126 | }, 127 | { 128 | "orientation" : "portrait", 129 | "idiom" : "ipad", 130 | "extent" : "to-status-bar", 131 | "scale" : "2x" 132 | }, 133 | { 134 | "orientation" : "portrait", 135 | "idiom" : "ipad", 136 | "filename" : "Default-Portrait@2x.png", 137 | "extent" : "full-screen", 138 | "scale" : "2x" 139 | }, 140 | { 141 | "orientation" : "landscape", 142 | "idiom" : "ipad", 143 | "extent" : "to-status-bar", 144 | "scale" : "2x" 145 | }, 146 | { 147 | "orientation" : "landscape", 148 | "idiom" : "ipad", 149 | "filename" : "Default-Landscape@2x.png", 150 | "extent" : "full-screen", 151 | "scale" : "2x" 152 | } 153 | ], 154 | "info" : { 155 | "version" : 1, 156 | "author" : "xcode" 157 | } 158 | } -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchScreen-AspectFill.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchScreen-AspectFill@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchScreen-Center.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchScreen-Center@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formly-js/ng-formly-nativescript/7da66abdd8b1e42afaa912073851213754ace6e4/src/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiresFullScreen 28 | 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/app/App_Resources/iOS/build.xcconfig: -------------------------------------------------------------------------------- 1 | // You can add custom settings here 2 | // for example you can uncomment the following line to force distribution code signing 3 | // CODE_SIGN_IDENTITY = iPhone Distribution 4 | // To build for device with XCode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html 5 | // DEVELOPMENT_TEAM = YOUR_TEAM_ID; 6 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 7 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 8 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { FormlyFieldConfig } from "ng-formly-nativescript"; 5 | import * as dialogs from 'ui/dialogs'; 6 | 7 | @Component({ 8 | selector: 'app', 9 | template: ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | `, 29 | encapsulation: ViewEncapsulation.None 30 | }) 31 | export class DemoComponent { 32 | public form: FormGroup = new FormGroup({}); 33 | public userFields: FormlyFieldConfig = [{ 34 | fieldGroup: [ 35 | { 36 | key: 'first', 37 | type: 'input', 38 | wrappers: ['sideLabel'], 39 | templateOptions: { 40 | type: 'text', 41 | label: 'First', 42 | placeholder: 'Enter firstname', 43 | divider: true 44 | } 45 | }, 46 | { 47 | key: 'last', 48 | type: 'input', 49 | wrappers: ['sideLabel'], 50 | templateOptions: { 51 | type: 'text', 52 | label: 'Last', 53 | placeholder: 'Enter lastname', 54 | divider: true 55 | } 56 | }, 57 | { 58 | key: 'email', 59 | type: 'input', 60 | wrappers: ['sideLabel'], 61 | templateOptions: { 62 | type: 'email', 63 | label: 'Email', 64 | placeholder: 'Enter email', 65 | divider: true 66 | }, 67 | modelOptions: { 68 | debounce: { 69 | default: 1000 70 | } 71 | } 72 | }, 73 | { 74 | key: 'password', 75 | type: 'input', 76 | wrappers: ['sideLabel'], 77 | templateOptions: { 78 | type: 'password', 79 | label: 'Password', 80 | placeholder: 'Password', 81 | divider: true 82 | } 83 | }, 84 | { 85 | key: 'checked', 86 | type: 'checkbox', 87 | templateOptions: { 88 | type: 'checkbox', 89 | label: 'I agree to the terms' 90 | } 91 | } 92 | ] 93 | }]; 94 | 95 | public user = { 96 | email: 'email@gmail.com', 97 | checked: false 98 | }; 99 | 100 | public savedUser: any; 101 | 102 | public save() { 103 | dialogs.alert(`Formly data saved! :)`); 104 | this.savedUser = JSON.stringify(this.user); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/app/app.css: -------------------------------------------------------------------------------- 1 | @import 'nativescript-theme-core/css/core.light.css'; 2 | -------------------------------------------------------------------------------- /src/app/core/components/formly.attributes.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 2 | import { createGenericTestComponent } from '../test-utils'; 3 | 4 | import { By } from '@angular/platform-browser'; 5 | import { Component } from '@angular/core'; 6 | import { FormlyModule } from '../core'; 7 | import { FormlyAttributes } from './formly.attributes'; 8 | import { FormlyFieldConfig } from './formly.field.config'; 9 | 10 | const createTestComponent = (html: string) => 11 | createGenericTestComponent(html, TestComponent) as ComponentFixture; 12 | 13 | function getFormlyAttributesElement(element: HTMLElement): HTMLInputElement { 14 | return element.querySelector('input'); 15 | } 16 | 17 | describe('FormlyAttributes Component', () => { 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [TestComponent], 21 | imports: [ 22 | FormlyModule.forRoot(), 23 | ], 24 | }); 25 | }); 26 | 27 | describe('templateOptions attributes', () => { 28 | it('should set element attribute if it is present in the templateOptions', () => { 29 | const fixture = createTestComponent(''); 30 | const elm = getFormlyAttributesElement(fixture.nativeElement); 31 | 32 | expect(elm.getAttribute('placeholder')).toBe('Title'); 33 | expect(elm.getAttribute('tabindex')).toBe('5'); 34 | expect(elm.getAttribute('step')).toBe('2'); 35 | expect(fixture.componentInstance.field.focus).toBeFalsy(); 36 | }); 37 | 38 | it('should change element attribute on edit templateOptions', () => { 39 | const fixture = createTestComponent(''); 40 | const elm = getFormlyAttributesElement(fixture.nativeElement); 41 | 42 | fixture.componentInstance.field = { 43 | key: 'title', 44 | focus: true, 45 | templateOptions: { 46 | placeholder: 'Title Edit', 47 | }, 48 | }; 49 | 50 | fixture.detectChanges(); 51 | 52 | expect(elm.getAttribute('placeholder')).toBe('Title Edit'); 53 | expect(elm.getAttribute('tabindex')).toBe(null); 54 | expect(elm.getAttribute('step')).toBe(null); 55 | expect(fixture.componentInstance.field.focus).toBeTruthy(); 56 | }); 57 | }); 58 | 59 | describe('focus the element', () => { 60 | it(`should focus the element when focus is set to "true" and then blurred when it's set to "false"`, () => { 61 | const fixture = createTestComponent(''); 62 | const elm = getFormlyAttributesElement(fixture.nativeElement); 63 | 64 | fixture.componentInstance.field = { focus: true, templateOptions: {} }; 65 | fixture.detectChanges(); 66 | expect(document.activeElement === elm).toBeTruthy(); 67 | 68 | fixture.componentInstance.field = { focus: false, templateOptions: {} }; 69 | fixture.detectChanges(); 70 | expect(document.activeElement === elm).toBeFalsy(); 71 | }); 72 | 73 | it('should change field focus when the element is focused', () => { 74 | const fixture = createTestComponent(''); 75 | const directive = fixture.debugElement.query(By.directive(FormlyAttributes)); 76 | 77 | directive.triggerEventHandler('focus', {}); 78 | fixture.detectChanges(); 79 | 80 | expect(fixture.componentInstance.field.focus).toBeTruthy(); 81 | }); 82 | }); 83 | }); 84 | 85 | @Component({selector: 'formly-formly-attributes-test', template: '', entryComponents: []}) 86 | class TestComponent { 87 | field: FormlyFieldConfig = { 88 | focus: false, 89 | key: 'title', 90 | templateOptions: { 91 | placeholder: 'Title', 92 | tabindex: 5, 93 | step: 2, 94 | }, 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/app/core/components/formly.attributes.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener, ElementRef, Input, Renderer, OnInit, OnChanges, SimpleChanges } from '@angular/core'; 2 | import { SingleFocusDispatcher } from '../services/formly.single.focus.dispatcher'; 3 | import { FormlyFieldConfig } from './formly.field.config'; 4 | 5 | @Directive({ 6 | selector: '[formlyAttributes]', 7 | providers: [SingleFocusDispatcher], 8 | }) 9 | export class FormlyAttributes implements OnInit, OnChanges { 10 | @Input('formlyAttributes') field: FormlyFieldConfig; 11 | @Input() formControl; 12 | // private attributes = ['placeholder', 'tabindex', 'step', 'aria-describedby']; 13 | private attributes = ['placeholder', 'checked']; 14 | // private statements = ['change', 'keydown', 'keyup', 'keypress', 'click', 'focus', 'blur']; 15 | private statements = ['tap']; 16 | 17 | // @HostListener('focus') onFocus() { 18 | // if (!this.field.focus) { 19 | // this.focusDispatcher.notify(this.field.key); 20 | // } 21 | // } 22 | 23 | constructor( 24 | private renderer: Renderer, 25 | private elementRef: ElementRef, 26 | private focusDispatcher: SingleFocusDispatcher, 27 | ) {} 28 | 29 | ngOnInit() { 30 | this.focusDispatcher.listen((key: String) => 31 | this.field.focus = this.field.key === key); 32 | } 33 | 34 | ngOnChanges(changes: SimpleChanges) { 35 | // console.log('FormlyAttributes ngOnChanges'); 36 | // console.log(changes); 37 | // for (let key in changes) { 38 | // console.log(`${key}: ${changes[key]}`); 39 | // } 40 | if (changes['field']) { 41 | const previousOptions = changes['field'].previousValue.templateOptions || {}, 42 | templateOptions = this.field.templateOptions; 43 | 44 | this.attributes 45 | .filter(attribute => templateOptions[attribute] !== '' || templateOptions[attribute] !== undefined) 46 | .map(attribute => { 47 | if (previousOptions[attribute] !== templateOptions[attribute]) { 48 | // console.log('setting attribute:', attribute); 49 | // NativeScript property mapping 50 | let attrName = attribute; 51 | if (attribute === 'placeholder') { 52 | attrName = 'hint'; 53 | } 54 | this.renderer.setElementAttribute(this.elementRef.nativeElement, attrName, templateOptions[attribute]); 55 | } 56 | }); 57 | this.statements 58 | .filter(statement => { 59 | if (previousOptions[statement] !== templateOptions[statement]) { 60 | if (typeof templateOptions[statement] === 'function') { 61 | this.renderer.listen(this.elementRef.nativeElement, statement, () => { 62 | templateOptions[statement](this.field, this.formControl); 63 | }); 64 | } 65 | } 66 | }); 67 | 68 | if (this.field.focus || (changes['field'].previousValue.focus !== undefined && changes['field'].previousValue.focus !== this.field.focus)) { 69 | this.renderer.invokeElementMethod(this.elementRef.nativeElement, this.field.focus ? 'focus' : 'blur', []); 70 | if (this.field.focus) { 71 | // TODO: Raise a Event which can be used for streaming 72 | this.focusDispatcher.notify(this.field.key); 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/core/components/formly.field.config.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup } from '@angular/forms'; 2 | export interface FormlyFieldConfig { 3 | key?: string; 4 | id?: string; 5 | templateOptions?: FormlyTemplateOptions; 6 | optionsTypes?: any; 7 | validation?: any; 8 | validators?: any; 9 | asyncValidators?: any; 10 | template?: string; 11 | component?: any; 12 | wrappers?: string[]; 13 | fieldGroup?: Array; 14 | fieldArray?: FormlyFieldConfig; 15 | hideExpression?: boolean | string | ((model, formState) => boolean); 16 | className?: string; 17 | type?: string; 18 | expressionProperties?: any; 19 | focus?: boolean; 20 | modelOptions?: any; 21 | lifecycle?: FormlyLifeCycleOptions; 22 | defaultValue?: any; 23 | parsers?: [(value: any, index: number) => {}]; 24 | } 25 | 26 | export interface FormlyTemplateOptions { 27 | type?: string; 28 | label?: string; 29 | placeholder?: string; 30 | disabled?: Boolean; 31 | options?: Array; 32 | rows?: number; 33 | cols?: number; 34 | description?: string; 35 | hidden?: boolean; 36 | max?: number; 37 | min?: number; 38 | minLength?: number; 39 | maxLength?: number; 40 | pattern?: string; 41 | required?: Boolean; 42 | tabindex?: number; 43 | step?: number; 44 | focus?: Function; 45 | blur?: Function; 46 | keyup?: Function; 47 | keydown?: Function; 48 | click?: Function; 49 | change?: Function; 50 | keypress?: Function; 51 | [additionalProperties: string]: any; 52 | } 53 | 54 | export interface FormlyLifeCycleFn { 55 | (form?: FormGroup, field?: FormlyFieldConfig, model?, options?): void; 56 | } 57 | 58 | export interface FormlyLifeCycleOptions { 59 | onInit?: FormlyLifeCycleFn; 60 | onChanges?: FormlyLifeCycleFn; 61 | doCheck?: FormlyLifeCycleFn; 62 | afterContentInit?: FormlyLifeCycleFn; 63 | afterContentChecked?: FormlyLifeCycleFn; 64 | afterViewInit?: FormlyLifeCycleFn; 65 | afterViewChecked?: FormlyLifeCycleFn; 66 | onDestroy?: FormlyLifeCycleFn; 67 | } 68 | -------------------------------------------------------------------------------- /src/app/core/components/formly.field.spec.ts: -------------------------------------------------------------------------------- 1 | import { fakeAsync, tick, TestBed, ComponentFixture } from '@angular/core/testing'; 2 | import { createGenericTestComponent } from '../test-utils'; 3 | 4 | import { Component, ViewChild, ViewContainerRef } from '@angular/core'; 5 | import { FormGroup, FormControl } from '@angular/forms'; 6 | import { 7 | FormlyModule, 8 | FieldType, 9 | FieldWrapper, 10 | } from '../core'; 11 | import { FormlyValueChangeEvent } from '../services/formly.event.emitter'; 12 | 13 | const createTestComponent = (html: string) => 14 | createGenericTestComponent(html, TestComponent) as ComponentFixture; 15 | 16 | function getFormlyFieldElement(element: HTMLElement): HTMLInputElement { 17 | return element.querySelector('formly-field'); 18 | } 19 | 20 | function getInputField(element: HTMLElement, index = 0): HTMLInputElement { 21 | return element.querySelectorAll('input')[index]; 22 | } 23 | 24 | function getLabelWrapper(element: HTMLElement): HTMLElement { 25 | return element.querySelector('label'); 26 | } 27 | 28 | let testComponentInputs; 29 | 30 | describe('FormlyField Component', () => { 31 | beforeEach(() => { 32 | TestBed.configureTestingModule({ 33 | declarations: [TestComponent, FormlyFieldText, FormlyWrapperLabel], 34 | imports: [ 35 | FormlyModule.forRoot({ 36 | types: [ 37 | { 38 | name: 'text', 39 | component: FormlyFieldText, 40 | }, 41 | { 42 | name: 'other', 43 | component: FormlyFieldText, 44 | wrappers: ['label'], 45 | }, 46 | ], 47 | wrappers: [{ 48 | name: 'label', 49 | component: FormlyWrapperLabel, 50 | }], 51 | manipulators: [ 52 | { class: Manipulator, method: 'run' }, 53 | ], 54 | }), 55 | ], 56 | }); 57 | }); 58 | 59 | it('should render template option', () => { 60 | testComponentInputs = { 61 | field: { template: '
Nested property keys
'}, 62 | }; 63 | 64 | const fixture = createTestComponent(''); 65 | 66 | expect(fixture.nativeElement.innerText).toEqual('Nested property keys'); 67 | }); 68 | 69 | it('should render field type', () => { 70 | testComponentInputs = { 71 | field: { 72 | key: 'title', 73 | type: 'text', 74 | templateOptions: { 75 | placeholder: 'Title', 76 | }, 77 | }, 78 | form: new FormGroup({title: new FormControl()}), 79 | }; 80 | 81 | const fixture = createTestComponent(''); 82 | 83 | expect(getLabelWrapper(fixture.nativeElement)).toEqual(null); 84 | expect(getFormlyFieldElement(fixture.nativeElement).getAttribute('style')).toEqual(null); 85 | expect(getInputField(fixture.nativeElement).getAttribute('placeholder')).toEqual('Title'); 86 | }); 87 | 88 | it('should render fieldGroup', () => { 89 | testComponentInputs = { 90 | field: { 91 | fieldGroup: [ 92 | { 93 | key: 'title1', 94 | type: 'text', 95 | templateOptions: { placeholder: 'Title1' }, 96 | }, 97 | { 98 | key: 'title2', 99 | type: 'text', 100 | templateOptions: { placeholder: 'Title2' }, 101 | }, 102 | ], 103 | }, 104 | form: new FormGroup({ title1: new FormControl(), title2: new FormControl() }), 105 | }; 106 | 107 | const fixture = createTestComponent(''); 108 | 109 | expect(getInputField(fixture.nativeElement, 0).getAttribute('placeholder')).toEqual('Title1'); 110 | expect(getInputField(fixture.nativeElement, 1).getAttribute('placeholder')).toEqual('Title2'); 111 | }); 112 | 113 | it('should hide field when passed a boolean', () => { 114 | testComponentInputs = { 115 | field: { 116 | key: 'title', 117 | type: 'text', 118 | hideExpression: true, 119 | templateOptions: { 120 | label: 'Title', 121 | placeholder: 'Title', 122 | }, 123 | }, 124 | form: new FormGroup({title: new FormControl()}), 125 | }; 126 | 127 | const fixture = createTestComponent(''); 128 | 129 | expect(getFormlyFieldElement(fixture.nativeElement).getAttribute('style')).toEqual('display: none;'); 130 | }); 131 | 132 | it('should hide field when passed a string', () => { 133 | testComponentInputs = { 134 | field: { 135 | key: 'title', 136 | type: 'text', 137 | hideExpression: 'true', 138 | templateOptions: { 139 | label: 'Title', 140 | placeholder: 'Title', 141 | }, 142 | }, 143 | form: new FormGroup({title: new FormControl()}), 144 | }; 145 | 146 | const fixture = createTestComponent(''); 147 | 148 | expect(getFormlyFieldElement(fixture.nativeElement).getAttribute('style')).toEqual('display: none;'); 149 | }); 150 | 151 | it('should hide field when passed a function', () => { 152 | testComponentInputs = { 153 | field: { 154 | key: 'title', 155 | type: 'text', 156 | hideExpression: () => true, 157 | templateOptions: { 158 | label: 'Title', 159 | placeholder: 'Title', 160 | }, 161 | }, 162 | form: new FormGroup({title: new FormControl()}), 163 | }; 164 | 165 | const fixture = createTestComponent(''); 166 | 167 | expect(getFormlyFieldElement(fixture.nativeElement).getAttribute('style')).toEqual('display: none;'); 168 | }); 169 | 170 | describe('model changes', () => { 171 | beforeEach(() => { 172 | testComponentInputs = { 173 | field: { 174 | key: 'title', 175 | type: 'text', 176 | templateOptions: { 177 | label: 'Title', 178 | placeholder: 'Title', 179 | }, 180 | }, 181 | form: new FormGroup({ title: new FormControl() }), 182 | }; 183 | }); 184 | 185 | it('should change model value', () => { 186 | const fixture = createTestComponent(''); 187 | spyOn(fixture.componentInstance, 'changeModel'); 188 | fixture.componentInstance.form.get('title').setValue('address'); 189 | 190 | expect(fixture.componentInstance.changeModel).toHaveBeenCalledWith(new FormlyValueChangeEvent('title', 'address')); 191 | }); 192 | 193 | it('should change model value after debounce time', fakeAsync(() => { 194 | testComponentInputs.field.modelOptions = { 195 | debounce: { default: 5 }, 196 | }; 197 | 198 | const fixture = createTestComponent(''); 199 | spyOn(fixture.componentInstance, 'changeModel'); 200 | fixture.componentInstance.form.get('title').setValue('address'); 201 | 202 | expect(fixture.componentInstance.changeModel).not.toHaveBeenCalled(); 203 | tick(6); 204 | expect(fixture.componentInstance.changeModel).toHaveBeenCalledWith(new FormlyValueChangeEvent('title', 'address')); 205 | })); 206 | }); 207 | 208 | describe('wrapper', () => { 209 | beforeEach(() => { 210 | testComponentInputs = { 211 | field: { 212 | key: 'title', 213 | type: 'text', 214 | templateOptions: { 215 | label: 'Title', 216 | placeholder: 'Title', 217 | }, 218 | }, 219 | form: new FormGroup({ title: new FormControl() }), 220 | }; 221 | }); 222 | 223 | it('should render field without wrapper or key', () => { 224 | delete testComponentInputs.field.key; 225 | 226 | const fixture = createTestComponent(''); 227 | const elm = getFormlyFieldElement(fixture.nativeElement); 228 | expect(getInputField(elm)).toBeDefined(); 229 | }); 230 | 231 | it('should render field wrapper', () => { 232 | testComponentInputs.field.wrappers = ['label']; 233 | 234 | const fixture = createTestComponent(''); 235 | const elm = getFormlyFieldElement(fixture.nativeElement); 236 | 237 | expect(getLabelWrapper(elm).innerText).toEqual('Title'); 238 | expect(getInputField(elm).getAttribute('placeholder')).toEqual('Title'); 239 | }); 240 | 241 | it('should render pre-wrapper', () => { 242 | testComponentInputs.field.templateOptions.preWrapper = true; 243 | 244 | const fixture = createTestComponent(''); 245 | const elm = getFormlyFieldElement(fixture.nativeElement); 246 | 247 | expect(getLabelWrapper(elm).innerText).toEqual('Title'); 248 | }); 249 | 250 | it('should render post-wrapper', () => { 251 | testComponentInputs.field.templateOptions.postWrapper = true; 252 | 253 | const fixture = createTestComponent(''); 254 | const elm = getFormlyFieldElement(fixture.nativeElement); 255 | 256 | expect(getLabelWrapper(elm).innerText).toEqual('Title'); 257 | }); 258 | 259 | it('should render pre/post-wrapper using templateManipulators option', () => { 260 | testComponentInputs.field.templateOptions.templateManipulators = { preWrapper: () => 'label' }; 261 | 262 | const fixture = createTestComponent(''); 263 | const elm = getFormlyFieldElement(fixture.nativeElement); 264 | 265 | expect(getLabelWrapper(elm).innerText).toEqual('Title'); 266 | }); 267 | }); 268 | 269 | it('should render options Types', () => { 270 | testComponentInputs = { 271 | field: { 272 | key: 'title', 273 | type: 'text', 274 | optionsTypes: ['other'], 275 | templateOptions: { 276 | placeholder: 'Title', 277 | }, 278 | }, 279 | form: new FormGroup({title: new FormControl()}), 280 | }; 281 | 282 | const fixture = createTestComponent(''); 283 | expect(getLabelWrapper(fixture.nativeElement)).toEqual(null); 284 | expect(getFormlyFieldElement(fixture.nativeElement).getAttribute('style')).toEqual(null); 285 | expect(getInputField(fixture.nativeElement).getAttribute('placeholder')).toEqual('Title'); 286 | }); 287 | 288 | it('expression properties', () => { 289 | testComponentInputs = { 290 | field: { 291 | key: 'title', 292 | type: 'text', 293 | optionsTypes: ['other'], 294 | templateOptions: { 295 | placeholder: 'Title', 296 | }, 297 | expressionProperties: { 298 | 'templateOptions.disabled': 'model.title !== undefined', 299 | 'templateOptions.placeholder': 'Updated', 300 | 'validation.show': true, 301 | }, 302 | }, 303 | form: new FormGroup({title: new FormControl()}), 304 | }; 305 | 306 | createTestComponent(''); 307 | }); 308 | }); 309 | 310 | @Component({selector: 'formly-formly-field-test', template: '', entryComponents: []}) 311 | class TestComponent { 312 | field = testComponentInputs.field; 313 | form = testComponentInputs.form; 314 | model = testComponentInputs.model || {}; 315 | 316 | changeModel(event) {} 317 | } 318 | 319 | @Component({ 320 | selector: 'formly-field-text', 321 | template: ``, 322 | }) 323 | export class FormlyFieldText extends FieldType {} 324 | 325 | @Component({ 326 | selector: 'formly-wrapper-label', 327 | template: ` 328 | 329 | 330 | `, 331 | }) 332 | export class FormlyWrapperLabel extends FieldWrapper { 333 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 334 | } 335 | 336 | export class Manipulator { 337 | run(fc) { 338 | fc.templateManipulators.postWrapper.push((field) => { 339 | if (field && field.templateOptions && field.templateOptions.postWrapper) { 340 | return 'label'; 341 | } 342 | }); 343 | 344 | fc.templateManipulators.preWrapper.push((field) => { 345 | if (field && field.templateOptions && field.templateOptions.preWrapper) { 346 | return 'label'; 347 | } 348 | }); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/app/core/components/formly.field.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, OnInit, EventEmitter, ElementRef, Input, Output, DoCheck, 3 | ViewContainerRef, ViewChild, ComponentRef, Renderer, ComponentFactoryResolver, 4 | ViewEncapsulation 5 | } from '@angular/core'; 6 | import { FormGroup } from '@angular/forms'; 7 | import { FormlyPubSub, FormlyEventEmitter, FormlyValueChangeEvent } from '../services/formly.event.emitter'; 8 | import { FormlyConfig } from '../services/formly.config'; 9 | import { Field } from '../templates/field'; 10 | import { evalExpression } from '../utils'; 11 | import { FormlyFieldConfig } from './formly.field.config'; 12 | import 'rxjs/add/operator/debounceTime'; 13 | import 'rxjs/add/operator/map'; 14 | 15 | @Component({ 16 | selector: 'formly-field', 17 | template: ` 18 | 19 | ` 20 | }) 21 | export class FormlyField implements DoCheck, OnInit { 22 | @Input() model: any; 23 | @Input() form: FormGroup; 24 | @Input() field: FormlyFieldConfig; 25 | @Input() options: any = {}; 26 | @Input() 27 | get hide() { return this._hide; } 28 | set hide(value: boolean) { 29 | this._hide = value; 30 | // this.renderer.setElementStyle(this.elementRef.nativeElement, 'display', value ? 'none' : ''); 31 | if (this.field.fieldGroup) { 32 | for (let i = 0; i < this.field.fieldGroup.length; i++) { 33 | this.psEmit(this.field.fieldGroup[i].key, 'hidden', this._hide); 34 | } 35 | } else { 36 | this.psEmit(this.field.key, 'hidden', this._hide); 37 | } 38 | } 39 | 40 | @Output() modelChange: EventEmitter = new EventEmitter(); 41 | 42 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 43 | private _hide; 44 | 45 | constructor( 46 | private elementRef: ElementRef, 47 | private formlyPubSub: FormlyPubSub, 48 | private renderer: Renderer, 49 | private formlyConfig: FormlyConfig, 50 | private componentFactoryResolver: ComponentFactoryResolver, 51 | ) {} 52 | 53 | ngDoCheck() { 54 | this.checkExpressionChange(); 55 | this.checkVisibilityChange(); 56 | } 57 | 58 | ngOnInit() { 59 | this.createFieldComponents(); 60 | } 61 | 62 | changeModel(event: FormlyValueChangeEvent) { 63 | this.modelChange.emit(event); 64 | } 65 | 66 | private createFieldComponents() { 67 | if (this.field && !this.field.template && !this.field.fieldGroup && !this.field.fieldArray) { 68 | let debounce = 0; 69 | if (this.field.modelOptions && this.field.modelOptions.debounce && this.field.modelOptions.debounce.default) { 70 | debounce = this.field.modelOptions.debounce.default; 71 | } 72 | let fieldComponentRef = this.createFieldComponent(); 73 | 74 | if (this.field.key) { 75 | let valueChanges = (fieldComponentRef.instance).valueChanges; 76 | if (debounce > 0) { 77 | valueChanges = valueChanges.debounceTime(debounce); 78 | } 79 | valueChanges.subscribe((value) => { 80 | console.log(value); 81 | this.changeModel(new FormlyValueChangeEvent(this.field.key, value)); 82 | }); 83 | // let valueChanges = fieldComponentRef.instance.formControl.valueChanges; 84 | // console.log('got valueChange in FormlyField:', valueChanges); 85 | // if (debounce > 0) { 86 | // valueChanges = valueChanges.debounceTime(debounce); 87 | // } 88 | // if (this.field.parsers && this.field.parsers.length > 0) { 89 | // this.field.parsers.map(parserFn => { 90 | // valueChanges = valueChanges.map(parserFn); 91 | // }); 92 | // } 93 | 94 | // valueChanges.subscribe((event) => this.changeModel(new FormlyValueChangeEvent(this.field.key, event));); 95 | } 96 | 97 | let update = new FormlyEventEmitter(); 98 | update.subscribe((option: any) => { 99 | this.field.templateOptions[option.key] = option.value; 100 | }); 101 | 102 | this.formlyPubSub.setEmitter(this.field.key, update); 103 | } else if (this.field.fieldGroup || this.field.fieldArray) { 104 | this.createFieldComponent(); 105 | } 106 | 107 | // TODO support this.field.hideExpression as a callback/observable 108 | this.hide = this.field.hideExpression ? true : false; 109 | } 110 | 111 | private createFieldComponent(): ComponentRef { 112 | if (this.field.fieldGroup) { 113 | this.field.type = this.field.type || 'formly-group'; 114 | } 115 | let type = this.formlyConfig.getType(this.field.type); 116 | let fieldComponent = this.fieldComponent; 117 | const fieldManipulators = this.getManipulators(this.field.templateOptions); 118 | let preWrappers = this.runManipulators(fieldManipulators.preWrapper, this.field); 119 | let postWrappers = this.runManipulators(fieldManipulators.postWrapper, this.field); 120 | if (!type.wrappers) type.wrappers = []; 121 | if (!this.field.wrappers) this.field.wrappers = []; 122 | let wrappers = [...preWrappers, ...this.field.wrappers, ...postWrappers]; 123 | wrappers.map(wrapperName => { 124 | let wrapperRef = this.createComponent(fieldComponent, this.formlyConfig.getWrapper(wrapperName).component); 125 | fieldComponent = wrapperRef.instance.fieldComponent; 126 | }); 127 | 128 | return this.createComponent(fieldComponent, type.component); 129 | } 130 | 131 | private createComponent(fieldComponent, component): ComponentRef { 132 | let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component); 133 | let ref = >fieldComponent.createComponent(componentFactory); 134 | 135 | (Object).assign(ref.instance, { 136 | model: this.model, 137 | form: this.form, 138 | field: this.field, 139 | options: this.options, 140 | }); 141 | 142 | return ref; 143 | } 144 | 145 | private psEmit(fieldKey: string, eventKey: string, value: any) { 146 | if (this.formlyPubSub && this.formlyPubSub.getEmitter(fieldKey) && this.formlyPubSub.getEmitter(fieldKey).emit) { 147 | this.formlyPubSub.getEmitter(fieldKey).emit(new FormlyValueChangeEvent(eventKey, value)); 148 | } 149 | } 150 | 151 | private getManipulators(options) { 152 | let preWrapper = []; 153 | let postWrapper = []; 154 | if (options && options.templateManipulators) { 155 | addManipulators(options.templateManipulators); 156 | } 157 | addManipulators(this.formlyConfig.templateManipulators); 158 | return {preWrapper, postWrapper}; 159 | 160 | function addManipulators(manipulators) { 161 | const {preWrapper: pre = [], postWrapper: post = []} = (manipulators || {}); 162 | preWrapper = preWrapper.concat(pre); 163 | postWrapper = postWrapper.concat(post); 164 | } 165 | } 166 | 167 | private runManipulators(manipulators: Function[], field: FormlyFieldConfig) { 168 | let wrappers = []; 169 | if (manipulators) { 170 | manipulators.map(manipulator => { 171 | if (manipulator(field)) { 172 | wrappers.push(manipulator(field)); 173 | } 174 | }); 175 | return wrappers; 176 | } 177 | } 178 | 179 | private checkVisibilityChange() { 180 | if (this.field && this.field.hideExpression !== undefined && this.field.hideExpression) { 181 | const hideExpressionResult: boolean = evalExpression( 182 | this.field.hideExpression, 183 | this, 184 | [this.model, this.options.formState], 185 | ); 186 | 187 | if (hideExpressionResult !== this.hide) { 188 | this.hide = hideExpressionResult; 189 | } 190 | } 191 | } 192 | 193 | private checkExpressionChange() { 194 | if (this.field && this.field.expressionProperties !== undefined) { 195 | const expressionProperties = this.field.expressionProperties; 196 | 197 | if (expressionProperties) { 198 | for (let key in expressionProperties) { 199 | 200 | const expressionValue = evalExpression( 201 | expressionProperties[key].expression, 202 | this, 203 | [this.model, this.options.formState], 204 | ); 205 | 206 | evalExpression( 207 | expressionProperties[key].expressionValueSetter, 208 | this, 209 | [expressionValue, this.model, this.field.templateOptions, this.field.validation], 210 | ); 211 | } 212 | 213 | const formControl = this.form.get(this.field.key), 214 | field = this.field; 215 | if (formControl) { 216 | if (formControl.status === 'DISABLED' && !field.templateOptions.disabled) { 217 | formControl.enable(); 218 | } 219 | if (formControl.status !== 'DISABLED' && field.templateOptions.disabled) { 220 | formControl.disable(); 221 | } 222 | if (!formControl.dirty && formControl.invalid && field.validation && !field.validation.show) { 223 | formControl.markAsUntouched(); 224 | } 225 | if (!formControl.dirty && formControl.invalid && field.validation && field.validation.show) { 226 | formControl.markAsTouched(); 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/app/core/components/formly.form.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 2 | import { createGenericTestComponent } from '../test-utils'; 3 | import { FormlyWrapperLabel, FormlyFieldText } from './formly.field.spec'; 4 | 5 | import { Component, OnInit } from '@angular/core'; 6 | import { FormlyModule } from '../core'; 7 | import { FormGroup } from '@angular/forms'; 8 | import { FieldType } from '../templates/field.type'; 9 | import { clone } from '../utils'; 10 | import { FormlyFieldConfig } from './formly.field.config'; 11 | 12 | const createTestComponent = (html: string) => 13 | createGenericTestComponent(html, TestComponent) as ComponentFixture; 14 | 15 | let testComponentInputs; 16 | 17 | describe('Formly Form Component', () => { 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({declarations: [TestComponent, FormlyFieldText, FormlyWrapperLabel, RepeatComponent], imports: [FormlyModule.forRoot({ 20 | types: [ 21 | { 22 | name: 'text', 23 | component: FormlyFieldText, 24 | }, 25 | { 26 | name: 'other', 27 | component: FormlyFieldText, 28 | wrappers: ['label'], 29 | }, 30 | { 31 | name: 'repeat', 32 | component: RepeatComponent, 33 | }, 34 | ], 35 | wrappers: [{ 36 | name: 'label', 37 | component: FormlyWrapperLabel, 38 | }], 39 | })]}); 40 | }); 41 | 42 | it('should initialize inputs with default values', () => { 43 | testComponentInputs = { 44 | fields: [{ 45 | fieldGroup: [{ 46 | key: 'name', 47 | type: 'text', 48 | }], 49 | }, { 50 | key: 'investments', 51 | type: 'repeat', 52 | fieldArray: { 53 | fieldGroup: [{ 54 | key: 'investmentName', 55 | type: 'text', 56 | }], 57 | }, 58 | }], 59 | form: new FormGroup({}), 60 | options: {}, 61 | model: { 62 | investments: [{investmentName: 'FA'}, {}], 63 | }, 64 | }; 65 | createTestComponent(''); 66 | testComponentInputs.form.controls.investments.removeAt(1); 67 | testComponentInputs.options.resetModel(); 68 | }); 69 | }); 70 | 71 | @Component({selector: 'formly-form-test', template: '', entryComponents: []}) 72 | class TestComponent { 73 | fields = testComponentInputs.fields; 74 | form = testComponentInputs.form; 75 | model = testComponentInputs.model || {}; 76 | options = testComponentInputs.options; 77 | } 78 | 79 | @Component({ 80 | selector: 'formly-repeat-section', 81 | template: ` 82 |
83 | 88 | 89 | 90 |
91 | `, 92 | }) 93 | export class RepeatComponent extends FieldType implements OnInit { 94 | get newOptions() { 95 | return clone(this.options); 96 | } 97 | get controls() { 98 | return this.form.controls[this.field.key]['controls']; 99 | } 100 | 101 | get fields(): FormlyFieldConfig[] { 102 | return this.field.fieldArray.fieldGroup; 103 | } 104 | 105 | remove(i) { 106 | this.form.controls[this.field.key]['controls'].splice(i, 1); 107 | this.model.splice(i, 1); 108 | } 109 | 110 | ngOnInit() { 111 | if (this.model) { 112 | this.model.map(() => { 113 | let formGroup = new FormGroup({}); 114 | this.form.controls[this.field.key]['controls'].push(formGroup); 115 | }); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/app/core/components/formly.form.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnChanges, Input, SimpleChanges, ViewEncapsulation } from '@angular/core'; 2 | import { AbstractControl, FormControl, FormGroup, FormArray } from '@angular/forms'; 3 | import { FormlyValueChangeEvent } from './../services/formly.event.emitter'; 4 | import { FormlyFieldConfig } from './formly.field.config'; 5 | import { FormlyFormBuilder } from '../services/formly.form.builder'; 6 | import { assignModelValue, isNullOrUndefined, isObject, reverseDeepMerge, getKey, getValueForKey, getFieldModel } from '../utils'; 7 | 8 | @Component({ 9 | selector: 'formly-form', 10 | template: ` 11 | 12 | 17 | 18 | 19 | 20 | ` 21 | }) 22 | export class FormlyForm implements OnChanges { 23 | @Input() model: any = {}; 24 | @Input() form: FormGroup = new FormGroup({}); 25 | @Input() fields: FormlyFieldConfig[] = []; 26 | @Input() options: any; 27 | private initialModel: any; 28 | 29 | constructor(private formlyBuilder: FormlyFormBuilder) {} 30 | 31 | ngOnChanges(changes: SimpleChanges) { 32 | if (changes['fields']) { 33 | this.model = this.model || {}; 34 | this.form = this.form || (new FormGroup({})); 35 | this.setOptions(); 36 | this.formlyBuilder.buildForm(this.form, this.fields, this.model, this.options); 37 | this.updateInitialValue(); 38 | } else if (changes['model'] && this.fields && this.fields.length > 0) { 39 | this.form.patchValue(this.model); 40 | } 41 | } 42 | 43 | fieldModel(field: FormlyFieldConfig) { 44 | if (field.key && (field.fieldGroup || field.fieldArray)) { 45 | return getFieldModel(this.model, field, true); 46 | } 47 | return this.model; 48 | } 49 | 50 | changeModel(event: FormlyValueChangeEvent) { 51 | assignModelValue(this.model, event.key, event.value); 52 | } 53 | 54 | setOptions() { 55 | this.options = this.options || {}; 56 | this.options.resetModel = this.resetModel.bind(this); 57 | this.options.updateInitialValue = this.updateInitialValue.bind(this); 58 | } 59 | 60 | private resetModel(model?: any) { 61 | model = isNullOrUndefined(model) ? this.initialModel : model; 62 | this.form.patchValue(model); 63 | this.resetFormGroup(model, this.form); 64 | this.resetFormModel(model, this.model); 65 | } 66 | 67 | private resetFormModel(model: any, formModel: any, path?: (string | number)[]) { 68 | if (!isObject(model) && !Array.isArray(model)) { 69 | return; 70 | } 71 | 72 | // removes 73 | for (let key in formModel) { 74 | if (!(key in model) || isNullOrUndefined(model[key])) { 75 | if (!this.form.get((path || []).concat(key))) { 76 | // don't remove if bound to a control 77 | delete formModel[key]; 78 | } 79 | } 80 | } 81 | 82 | // inserts and updates 83 | for (let key in model) { 84 | if (!isNullOrUndefined(model[key])) { 85 | if (key in formModel) { 86 | this.resetFormModel(model[key], formModel[key], (path || []).concat(key)); 87 | } 88 | else { 89 | formModel[key] = model[key]; 90 | } 91 | } 92 | } 93 | } 94 | 95 | private resetFormGroup(model: any, form: FormGroup, actualKey?: string) { 96 | for (let controlKey in form.controls) { 97 | let key = getKey(controlKey, actualKey); 98 | if (form.controls[controlKey] instanceof FormGroup) { 99 | this.resetFormGroup(model, form.controls[controlKey], key); 100 | } 101 | if (form.controls[controlKey] instanceof FormArray) { 102 | this.resetArray(model, form.controls[controlKey], key); 103 | } 104 | if (form.controls[controlKey] instanceof FormControl) { 105 | form.controls[controlKey].setValue(getValueForKey(model, key)); 106 | } 107 | } 108 | } 109 | 110 | private resetArray(model: any, formArray: FormArray, key: string) { 111 | let newValue = getValueForKey(model, key); 112 | 113 | // removes and updates 114 | for (let i = formArray.controls.length - 1; i >= 0; i--) { 115 | if (formArray.controls[i] instanceof FormGroup) { 116 | if (newValue && !isNullOrUndefined(newValue[i])) { 117 | this.resetFormGroup(newValue[i], formArray.controls[i]); 118 | } 119 | else { 120 | formArray.removeAt(i); 121 | let value = getValueForKey(this.model, key); 122 | if (Array.isArray(value)) { 123 | value.splice(i, 1); 124 | } 125 | } 126 | } 127 | } 128 | 129 | // inserts 130 | if (Array.isArray(newValue) && formArray.controls.length < newValue.length) { 131 | let remaining = newValue.length - formArray.controls.length; 132 | let initialLength = formArray.controls.length; 133 | for (let i = 0; i < remaining; i++) { 134 | let pos = initialLength + i; 135 | getValueForKey(this.model, key).push(newValue[pos]); 136 | formArray.controls.push(new FormGroup({})); 137 | } 138 | } 139 | } 140 | 141 | private updateInitialValue() { 142 | let obj = reverseDeepMerge(this.form.value, this.model); 143 | this.initialModel = JSON.parse(JSON.stringify(obj)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/app/core/components/formly.group.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { AbstractControl } from '@angular/forms'; 3 | import { FieldType } from '../templates/field.type'; 4 | import { clone } from '../utils'; 5 | 6 | @Component({ 7 | selector: 'formly-group', 8 | template: ` 9 | 10 | ` 11 | }) 12 | export class FormlyGroup extends FieldType { 13 | 14 | get newOptions() { 15 | return clone(this.options); 16 | } 17 | 18 | get formlyGroup(): AbstractControl { 19 | if (this.field.key) { 20 | return this.form.get(this.field.key); 21 | } else { 22 | return this.form; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/core/core.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders, ANALYZE_FOR_ENTRY_COMPONENTS } from '@angular/core'; 2 | import { NativeScriptModule, NativeScriptFormsModule } from "nativescript-angular"; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { FormlyForm } from './components/formly.form'; 5 | import { FormlyFieldConfig } from './components/formly.field.config'; 6 | import { FormlyField } from './components/formly.field'; 7 | import { FormlyAttributes } from './components/formly.attributes'; 8 | import { FormlyConfig, ConfigOption, FORMLY_CONFIG_TOKEN } from './services/formly.config'; 9 | import { FormlyFormBuilder } from './services/formly.form.builder'; 10 | import { FormlyValidationMessages } from './services/formly.validation-messages'; 11 | import { FormlyPubSub, FormlyEventEmitter } from './services/formly.event.emitter'; 12 | import { Field } from './templates/field'; 13 | import { FieldType } from './templates/field.type'; 14 | import { FieldWrapper } from './templates/field.wrapper'; 15 | import { FormlyGroup } from './components/formly.group'; 16 | import { SingleFocusDispatcher } from './services/formly.single.focus.dispatcher'; 17 | 18 | export { 19 | FormlyAttributes, 20 | FormlyFormBuilder, 21 | FormlyField, 22 | FormlyFieldConfig, 23 | FormlyForm, 24 | FormlyConfig, 25 | FormlyPubSub, 26 | FormlyValidationMessages, 27 | FormlyEventEmitter, 28 | SingleFocusDispatcher, 29 | 30 | Field, 31 | FieldType, 32 | FieldWrapper, 33 | }; 34 | 35 | const FORMLY_DIRECTIVES = [FormlyForm, FormlyField, FormlyAttributes, FormlyGroup]; 36 | 37 | @NgModule({ 38 | declarations: FORMLY_DIRECTIVES, 39 | entryComponents: [FormlyGroup], 40 | exports: FORMLY_DIRECTIVES, 41 | imports: [ 42 | NativeScriptModule, 43 | ReactiveFormsModule, 44 | NativeScriptFormsModule 45 | ], 46 | }) 47 | export class FormlyModule { 48 | static forRoot(config: ConfigOption = {}): ModuleWithProviders { 49 | return { 50 | ngModule: FormlyModule, 51 | providers: [ 52 | FormlyFormBuilder, 53 | FormlyConfig, 54 | FormlyPubSub, 55 | FormlyValidationMessages, 56 | { provide: FORMLY_CONFIG_TOKEN, useValue: config, multi: true }, 57 | { provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: config, multi: true }, 58 | ], 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/core/services/formly.config.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormlyConfig } from './formly.config'; 2 | import { Validators } from '@angular/forms'; 3 | import { Component } from '@angular/core'; 4 | 5 | describe('FormlyConfig service', () => { 6 | let config: FormlyConfig; 7 | beforeEach(() => { 8 | config = new FormlyConfig([{ 9 | wrappers: [{ name: 'layout', component: TestComponent }], 10 | types: [{ name: 'input' }], 11 | validators: [{ name: 'required', validation: Validators.required }], 12 | }]); 13 | }); 14 | 15 | describe('wrappers', () => { 16 | it('should add wrapper', () => { 17 | config.setWrapper({ name: 'custom_wrapper', component: TestComponent }); 18 | 19 | expect(config.getWrapper('layout').name).toEqual('layout'); 20 | expect(config.getWrapper('custom_wrapper').name).toEqual('custom_wrapper'); 21 | }); 22 | 23 | it('should throw when wrapper not found', () => { 24 | const config = new FormlyConfig(); 25 | expect(() => config.getWrapper('custom_wrapper')).toThrowError('[Formly Error] There is no wrapper by the name of "custom_wrapper"'); 26 | }); 27 | }); 28 | 29 | describe('types', () => { 30 | it('should add type', () => { 31 | config.setType({ name: 'custom_input' }); 32 | 33 | expect(config.getType('input').name).toEqual('input'); 34 | expect(config.getType('custom_input').name).toEqual('custom_input'); 35 | }); 36 | 37 | it('should add type as an array', () => { 38 | config.setType([{ name: 'custom_input1' }, { name: 'custom_input2' }]); 39 | 40 | expect(config.getType('custom_input1').name).toEqual('custom_input1'); 41 | expect(config.getType('custom_input2').name).toEqual('custom_input2'); 42 | }); 43 | 44 | it('should throw when type not found', () => { 45 | const config = new FormlyConfig(); 46 | expect(() => config.getType('custom_input')).toThrowError('[Formly Error] There is no type by the name of "custom_input"'); 47 | }); 48 | }); 49 | 50 | describe('validators', () => { 51 | it('should add validator', () => { 52 | config.setValidator({ name: 'null', validation: Validators.nullValidator }); 53 | 54 | expect(config.getValidator('null').name).toEqual('null'); 55 | expect(config.getValidator('required').name).toEqual('required'); 56 | }); 57 | 58 | it('should throw when validator not found', () => { 59 | const config = new FormlyConfig(); 60 | expect(() => config.getValidator('custom_validator')).toThrowError('[Formly Error] There is no validator by the name of "custom_validator"'); 61 | }); 62 | }); 63 | }); 64 | 65 | @Component({selector: 'formly-test-cmp', template: '', entryComponents: []}) 66 | class TestComponent { 67 | } 68 | -------------------------------------------------------------------------------- /src/app/core/services/formly.config.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, OpaqueToken } from '@angular/core'; 2 | import { FormlyGroup } from '../components/formly.group'; 3 | import { reverseDeepMerge } from './../utils'; 4 | import { FormlyFieldConfig } from '../components/formly.field.config'; 5 | 6 | export const FORMLY_CONFIG_TOKEN = new OpaqueToken('FORMLY_CONFIG_TOKEN'); 7 | 8 | /** 9 | * Maintains list of formly field directive types. This can be used to register new field templates. 10 | */ 11 | @Injectable() 12 | export class FormlyConfig { 13 | types: {[name: string]: TypeOption} = { 14 | 'formly-group': { 15 | name: 'formly-group', 16 | component: FormlyGroup, 17 | }, 18 | }; 19 | validators: {[name: string]: ValidatorOption} = {}; 20 | wrappers: {[name: string]: WrapperOption} = {}; 21 | 22 | public templateManipulators = { 23 | preWrapper: [], 24 | postWrapper: [], 25 | }; 26 | 27 | public extras = { 28 | fieldTransform: undefined, 29 | }; 30 | 31 | constructor(@Inject(FORMLY_CONFIG_TOKEN) configs: ConfigOption[] = []) { 32 | configs.map(config => { 33 | if (config.types) { 34 | config.types.map(type => this.setType(type)); 35 | } 36 | if (config.validators) { 37 | config.validators.map(validator => this.setValidator(validator)); 38 | } 39 | if (config.wrappers) { 40 | config.wrappers.map(wrapper => this.setWrapper(wrapper)); 41 | } 42 | if (config.manipulators) { 43 | config.manipulators.map(manipulator => this.setManipulator(manipulator)); 44 | } 45 | }); 46 | } 47 | 48 | setType(options: TypeOption | TypeOption[]) { 49 | if (Array.isArray(options)) { 50 | options.map((option) => { 51 | this.setType(option); 52 | }); 53 | } else { 54 | if (!this.types[options.name]) { 55 | this.types[options.name] = {}; 56 | } 57 | this.types[options.name].component = options.component; 58 | this.types[options.name].name = options.name; 59 | this.types[options.name].extends = options.extends; 60 | this.types[options.name].defaultOptions = options.defaultOptions; 61 | if (options.wrappers) { 62 | options.wrappers.map((wrapper) => { 63 | this.setTypeWrapper(options.name, wrapper); 64 | }); 65 | } 66 | } 67 | } 68 | 69 | getType(name: string): TypeOption { 70 | if (!this.types[name]) { 71 | throw new Error(`[Formly Error] There is no type by the name of "${name}"`); 72 | } 73 | 74 | if (!this.types[name].component && this.types[name].extends) { 75 | this.types[name].component = this.getType(this.types[name].extends).component; 76 | } 77 | 78 | return this.types[name]; 79 | } 80 | 81 | getMergedField(field: FormlyFieldConfig = {}): any { 82 | let name = field.type; 83 | if (!this.types[name]) { 84 | throw new Error(`[Formly Error] There is no type by the name of "${name}"`); 85 | } 86 | 87 | if (!this.types[name].component && this.types[name].extends) { 88 | this.types[name].component = this.getType(this.types[name].extends).component; 89 | } 90 | 91 | if (this.types[name].defaultOptions) { 92 | reverseDeepMerge(field, this.types[name].defaultOptions); 93 | } 94 | 95 | let extendDefaults = this.types[name].extends && this.getType(this.types[name].extends).defaultOptions; 96 | if (extendDefaults) { 97 | reverseDeepMerge(field, extendDefaults); 98 | } 99 | 100 | if (field && field.optionsTypes) { 101 | field.optionsTypes.map(option => { 102 | let defaultOptions = this.getType(option).defaultOptions; 103 | if (defaultOptions) { 104 | reverseDeepMerge(field, defaultOptions); 105 | } 106 | }); 107 | } 108 | reverseDeepMerge(field, this.types[name]); 109 | } 110 | 111 | setWrapper(options: WrapperOption) { 112 | this.wrappers[options.name] = options; 113 | if (options.types) { 114 | options.types.map((type) => { 115 | this.setTypeWrapper(type, options.name); 116 | }); 117 | } 118 | } 119 | 120 | getWrapper(name: string): WrapperOption { 121 | if (!this.wrappers[name]) { 122 | throw new Error(`[Formly Error] There is no wrapper by the name of "${name}"`); 123 | } 124 | 125 | return this.wrappers[name]; 126 | } 127 | 128 | setTypeWrapper(type, name) { 129 | if (!this.types[type]) { 130 | this.types[type] = {}; 131 | } 132 | if (!this.types[type].wrappers) { 133 | this.types[type].wrappers = <[string]>[]; 134 | } 135 | this.types[type].wrappers.push(name); 136 | } 137 | 138 | setValidator(options: ValidatorOption) { 139 | this.validators[options.name] = options; 140 | } 141 | 142 | getValidator(name: string): ValidatorOption { 143 | if (!this.validators[name]) { 144 | throw new Error(`[Formly Error] There is no validator by the name of "${name}"`); 145 | } 146 | 147 | return this.validators[name]; 148 | } 149 | 150 | setManipulator(manipulator) { 151 | new manipulator.class()[manipulator.method](this); 152 | } 153 | } 154 | 155 | export interface TypeOption { 156 | name: string; 157 | component?: any; 158 | wrappers?: string[]; 159 | extends?: string; 160 | defaultOptions?: any; 161 | } 162 | 163 | export interface WrapperOption { 164 | name: string; 165 | component: any; 166 | types?: string[]; 167 | } 168 | 169 | export interface ValidatorOption { 170 | name: string; 171 | validation: any; 172 | } 173 | 174 | export interface ValidationMessageOption { 175 | name: string; 176 | message: any; 177 | } 178 | 179 | export interface ManipulatorsOption { 180 | class?: Function; 181 | method?: string; 182 | } 183 | 184 | export interface ConfigOption { 185 | types?: [TypeOption]; 186 | wrappers?: [WrapperOption]; 187 | validators?: [ValidatorOption]; 188 | validationMessages?: [ValidationMessageOption]; 189 | manipulators?: [ManipulatorsOption]; 190 | } 191 | -------------------------------------------------------------------------------- /src/app/core/services/formly.event.emitter.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs/Subject'; 2 | 3 | export class FormlyValueChangeEvent { 4 | constructor(public key: string, public value: any) {} 5 | } 6 | 7 | export class FormlyEventEmitter extends Subject { 8 | emit(value) { 9 | super.next(value); 10 | } 11 | } 12 | 13 | export class FormlyPubSub { 14 | emitters = {}; 15 | 16 | setEmitter(key, emitter) { 17 | this.emitters[key] = emitter; 18 | } 19 | 20 | getEmitter(key) { 21 | return this.emitters[key]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/core/services/formly.form.builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormlyFormBuilder, FormlyConfig, FormlyFieldConfig } from './../core'; 2 | import { FormGroup, Validators, FormControl } from '@angular/forms'; 3 | import { Component } from '@angular/core'; 4 | 5 | describe('FormlyFormBuilder service', () => { 6 | let builder: FormlyFormBuilder, 7 | form: FormGroup, 8 | field: FormlyFieldConfig; 9 | beforeEach(() => { 10 | form = new FormGroup({}); 11 | builder = new FormlyFormBuilder( 12 | new FormlyConfig([{ 13 | types: [{ name: 'input', component: TestComponent }], 14 | wrappers: [{ name: 'label', component: TestComponent, types: ['input'] }], 15 | validators: [{ name: 'required', validation: Validators.required }], 16 | }]), 17 | ); 18 | }); 19 | 20 | describe('initialise default TemplateOptions', () => { 21 | it('should not set the default value if the specified key or type is undefined', () => { 22 | field = { key: 'title' }; 23 | builder.buildForm(form, [field], {}, {}); 24 | 25 | expect(field.templateOptions).toEqual(undefined); 26 | }); 27 | 28 | it('should set the default value if the specified key and type is defined', () => { 29 | field = { key: 'title', type: 'input', templateOptions: { placeholder: 'Title' } }; 30 | builder.buildForm(form, [field], {}, {}); 31 | 32 | expect(field.templateOptions).toEqual({ label: '', placeholder: 'Title', focus: false }); 33 | }); 34 | 35 | it('should set the default value if the specified key and type is defined for fieldGroup', () => { 36 | field = { 37 | key: 'fieldgroup', 38 | fieldGroup: [{ key: 'title', type: 'input', templateOptions: { placeholder: 'Title' } }], 39 | }; 40 | builder.buildForm(form, [field], {}, {}); 41 | expect(field.fieldGroup[0].templateOptions).toEqual({ label: '', placeholder: 'Title', focus: false }); 42 | }); 43 | }); 44 | 45 | describe('generate field id', () => { 46 | it('should not generate id if it is defined', () => { 47 | field = { key: 'title', id: 'title_id' }; 48 | builder.buildForm(form, [field], {}, {}); 49 | 50 | expect(field.id).toEqual('title_id'); 51 | }); 52 | 53 | it('should generate id if it is not defined', () => { 54 | field = { key: 'title' }; 55 | builder.buildForm(form, [field], {}, {}); 56 | 57 | expect(field.id).toEqual('formly_1__title_0'); 58 | }); 59 | 60 | it('should generate an unique id for each form', () => { 61 | let field1 = { key: 'title' }, 62 | field2 = { key: 'title' }; 63 | 64 | builder.buildForm(form, [field1], {}, {}); 65 | builder.buildForm(form, [field2], {}, {}); 66 | 67 | expect(field1['id']).not.toEqual(field2['id']); 68 | }); 69 | }); 70 | 71 | describe('form control creation and addition', () => { 72 | it('should let component create the form control', () => { 73 | let field = { key: 'title', type: 'input', component: new TestComponentThatCreatesControl() }; 74 | 75 | builder.buildForm(form, [field], {}, {}); 76 | 77 | let control: FormControl = form.get('title'); 78 | expect(control).not.toBeNull(); 79 | expect(control.value).toEqual('created by component'); 80 | }); 81 | }); 82 | 83 | describe('merge field options', () => { 84 | it('nested property key', () => { 85 | field = { key: 'nested.title', type: 'input' }; 86 | builder.buildForm(form, [field], {}, {}); 87 | 88 | expect(field.key).toEqual('nested.title'); 89 | expect(field.wrappers).toEqual(['label']); 90 | }); 91 | }); 92 | 93 | describe('initialise field validators', () => { 94 | const expectValidators = (invalidValue, validValue, errors?) => { 95 | const formControl = form.get('title'); 96 | expect(typeof field.validators.validation).toBe('function'); 97 | 98 | formControl.patchValue(invalidValue); 99 | expect(formControl.valid).toBeFalsy(); 100 | if (errors) { 101 | expect(formControl.errors).toEqual(errors); 102 | } 103 | 104 | formControl.patchValue(validValue); 105 | expect(formControl.valid).toBeTruthy(); 106 | }; 107 | 108 | const expectAsyncValidators = (value) => { 109 | const formControl = form.get('title'); 110 | expect(typeof field.asyncValidators.validation).toBe('function'); 111 | 112 | formControl.patchValue(value); 113 | expect(formControl.status).toBe('PENDING'); 114 | }; 115 | 116 | beforeEach(() => { 117 | field = { key: 'title', type: 'input' }; 118 | }); 119 | 120 | describe('validation.show', () => { 121 | it('should show error when option `show` is true', () => { 122 | field.validators = { validation: ['required'] }; 123 | field.validation = { show: true }; 124 | builder.buildForm(form, [field], {}, {}); 125 | 126 | expect(form.get('title').touched).toBeTruthy(); 127 | }); 128 | 129 | it('should not show error when option `show` is false', () => { 130 | field.validators = { validation: ['required'] }; 131 | field.validation = { show: false }; 132 | builder.buildForm(form, [field], {}, {}); 133 | 134 | expect(form.get('title').touched).toBeFalsy(); 135 | }); 136 | }); 137 | 138 | describe('validators', () => { 139 | describe('with validation option', () => { 140 | it(`using pre-defined type`, () => { 141 | field.validators = { validation: ['required'] }; 142 | builder.buildForm(form, [field], {}, {}); 143 | 144 | expectValidators(null, 'test'); 145 | }); 146 | 147 | it(`using custom type`, () => { 148 | field.validators = { validation: [Validators.required] }; 149 | builder.buildForm(form, [field], {}, {}); 150 | 151 | expectValidators(null, 'test'); 152 | }); 153 | }); 154 | 155 | describe('without validation option', () => { 156 | it(`using function`, () => { 157 | field.validators = { required: (form) => form.value }; 158 | builder.buildForm(form, [field], {}, {}); 159 | 160 | expectValidators(null, 'test', {required: true}); 161 | }); 162 | 163 | it(`using expression property`, () => { 164 | field.validators = { 165 | required: { expression: (form) => form.value }, 166 | }; 167 | builder.buildForm(form, [field], {}, {}); 168 | 169 | expectValidators(null, 'test', {required: true}); 170 | }); 171 | }); 172 | }); 173 | 174 | describe('asyncValidators', () => { 175 | it(`uses asyncValidator objects`, () => { 176 | field.asyncValidators = { custom: (control: FormControl) => new Promise(resolve => resolve( control.value !== 'test'))}; 177 | builder.buildForm(form, [field], {}, {}); 178 | 179 | expectAsyncValidators('test'); 180 | }); 181 | 182 | it(`uses asyncValidator objects`, () => { 183 | field.asyncValidators = { validation: [(control: FormControl) => 184 | new Promise(resolve => resolve( control.value !== 'john' ? null : { uniqueUsername: true }))] }; 185 | builder.buildForm(form, [field], {}, {}); 186 | 187 | expectAsyncValidators('test'); 188 | }); 189 | }); 190 | 191 | describe('using templateOptions', () => { 192 | const options = [ 193 | { name: 'required', value: true, valid: 'test', invalid: null }, 194 | { name: 'pattern', value: '[0-9]{5}', valid: '75964', invalid: 'ddd' }, 195 | { name: 'minLength', value: 5, valid: '12345', invalid: '123' }, 196 | { name: 'maxLength', value: 10, valid: '123', invalid: '12345678910' }, 197 | { name: 'min', value: 5, valid: 6, invalid: 3 }, 198 | { name: 'max', value: 10, valid: 8, invalid: 11 }, 199 | ]; 200 | 201 | options.map(option => { 202 | it(`${option.name}`, () => { 203 | field.templateOptions = { [option.name]: option.value }; 204 | builder.buildForm(form, [field], {}, {}); 205 | 206 | expectValidators(option.invalid, option.valid); 207 | }); 208 | }); 209 | }); 210 | }); 211 | }); 212 | 213 | @Component({selector: 'formly-test-cmp', template: '', entryComponents: []}) 214 | class TestComponent { 215 | } 216 | 217 | class TestComponentThatCreatesControl { 218 | 219 | createControl(model, field) { 220 | return new FormControl('created by component'); 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /src/app/core/services/formly.form.builder.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FormGroup, FormArray, FormControl, Validators } from '@angular/forms'; 3 | import { FormlyConfig } from './formly.config'; 4 | import { evalStringExpression, evalExpressionValueSetter, getFieldId, assignModelValue, isObject } from './../utils'; 5 | import { FormlyFieldConfig } from '../components/formly.field.config'; 6 | 7 | @Injectable() 8 | export class FormlyFormBuilder { 9 | private defaultPath; 10 | private validationOpts = ['required', 'pattern', 'minLength', 'maxLength', 'min', 'max']; 11 | private formId = 0; 12 | private model; 13 | 14 | constructor(private formlyConfig: FormlyConfig) {} 15 | 16 | buildForm(form: FormGroup, fields: FormlyFieldConfig[] = [], model, options) { 17 | this.model = model; 18 | this.formId++; 19 | let fieldTransforms = (options && options.fieldTransform) || this.formlyConfig.extras.fieldTransform; 20 | if (!Array.isArray(fieldTransforms)) { 21 | fieldTransforms = [fieldTransforms]; 22 | } 23 | 24 | fieldTransforms.forEach(fieldTransform => { 25 | if (fieldTransform) { 26 | fields = fieldTransform(fields, model, form, options); 27 | if (!fields) { 28 | throw new Error('fieldTransform must return an array of fields'); 29 | } 30 | } 31 | }); 32 | 33 | this.registerFormControls(form, fields, model, options); 34 | } 35 | 36 | private registerFormControls(form: FormGroup, fields: FormlyFieldConfig[], model, options) { 37 | fields.map((field, index) => { 38 | field.id = getFieldId(`formly_${this.formId}`, field, index); 39 | if (field.key && field.type) { 40 | this.initFieldTemplateOptions(field); 41 | this.initFieldValidation(field); 42 | this.initFieldAsyncValidation(field); 43 | 44 | 45 | let path: any = field.key; 46 | if (typeof path === 'string') { 47 | if (field.defaultValue) { 48 | this.defaultPath = path; 49 | } 50 | path = path.split('.'); 51 | } 52 | 53 | if (path.length > 1) { 54 | const rootPath = path.shift(); 55 | let nestedForm = (form.get(rootPath) ? form.get(rootPath) : new FormGroup({}, field.validators ? field.validators.validation : undefined, field.asyncValidators ? field.asyncValidators.validation : undefined)); 56 | if (!form.get(rootPath)) { 57 | form.addControl(rootPath, nestedForm); 58 | } 59 | if (!model[rootPath]) { 60 | model[rootPath] = isNaN(rootPath) ? {} : []; 61 | } 62 | 63 | const originalKey = field.key; 64 | // Should this reassignment not be refactored? 65 | field.key = path; 66 | this.buildForm(nestedForm, [field], model[rootPath], {}); 67 | field.key = originalKey; 68 | } else { 69 | 70 | this.formlyConfig.getMergedField(field); 71 | this.initFieldExpression(field); 72 | this.initFieldValidation(field); 73 | this.initFieldAsyncValidation(field); 74 | this.addFormControl(form, field, model[path[0]] || field.defaultValue || ''); 75 | if (field.defaultValue && !model[path[0]]) { 76 | let path = this.defaultPath.split('.'); 77 | path = path.pop(); 78 | assignModelValue(this.model, path, field.defaultValue); 79 | this.defaultPath = undefined; 80 | } 81 | } 82 | } 83 | 84 | if (field.fieldGroup) { 85 | if (field.key) { 86 | let nestedForm = form.get(field.key), 87 | nestedModel = model[field.key] || {}; 88 | 89 | if (!nestedForm) { 90 | nestedForm = new FormGroup( 91 | {}, 92 | field.validators ? field.validators.validation : undefined, 93 | field.asyncValidators ? field.asyncValidators.validation : undefined, 94 | ); 95 | form.addControl(field.key, nestedForm); 96 | } 97 | 98 | this.buildForm(nestedForm, field.fieldGroup, nestedModel, {}); 99 | } else { 100 | this.buildForm(form, field.fieldGroup, model, {}); 101 | } 102 | } 103 | 104 | if (field.fieldArray && field.key) { 105 | if (!(form.get(field.key) instanceof FormArray)) { 106 | const arrayForm = new FormArray( 107 | [], 108 | field.validators ? field.validators.validation : undefined, 109 | field.asyncValidators ? field.asyncValidators.validation : undefined, 110 | ); 111 | form.setControl(field.key, arrayForm); 112 | } 113 | } 114 | }); 115 | } 116 | 117 | private initFieldExpression(field: FormlyFieldConfig) { 118 | if (field.expressionProperties) { 119 | for (let key in field.expressionProperties) { 120 | if (typeof field.expressionProperties[key] === 'string') { 121 | // cache built expression 122 | field.expressionProperties[key] = { 123 | expression: evalStringExpression(field.expressionProperties[key], ['model', 'formState']), 124 | expressionValueSetter: evalExpressionValueSetter(key, ['expressionValue', 'model', 'templateOptions', 'validation']), 125 | }; 126 | } 127 | } 128 | } 129 | 130 | if (typeof field.hideExpression === 'string') { 131 | // cache built expression 132 | field.hideExpression = evalStringExpression(field.hideExpression, ['model', 'formState']); 133 | } 134 | } 135 | 136 | private initFieldTemplateOptions(field: FormlyFieldConfig) { 137 | field.templateOptions = (Object).assign({ 138 | label: '', 139 | placeholder: '', 140 | focus: false, 141 | }, field.templateOptions); 142 | } 143 | 144 | private initFieldAsyncValidation(field: FormlyFieldConfig) { 145 | let validators = []; 146 | if (field.asyncValidators) { 147 | for (let validatorName in field.asyncValidators) { 148 | if (validatorName !== 'validation') { 149 | validators.push((control: FormControl) => { 150 | let validator = field.asyncValidators[validatorName]; 151 | if (isObject(validator)) { 152 | validator = validator.expression; 153 | } 154 | 155 | return new Promise((resolve) => { 156 | return validator(control).then(result => { 157 | resolve(result ? null : {[validatorName]: true}); 158 | }); 159 | }); 160 | }); 161 | } 162 | } 163 | } 164 | if (field.asyncValidators && Array.isArray(field.asyncValidators.validation)) { 165 | field.asyncValidators.validation.map(validate => { 166 | if (typeof validate === 'string') { 167 | validators.push(this.formlyConfig.getValidator(validate).validation); 168 | } else { 169 | validators.push(validate); 170 | } 171 | }); 172 | } 173 | 174 | if (validators.length) { 175 | if (field.asyncValidators && !Array.isArray(field.asyncValidators.validation)) { 176 | field.asyncValidators.validation = Validators.composeAsync([field.asyncValidators.validation, ...validators]); 177 | } else { 178 | field.asyncValidators = { 179 | validation: Validators.composeAsync(validators), 180 | }; 181 | } 182 | } 183 | } 184 | 185 | private initFieldValidation(field: FormlyFieldConfig) { 186 | let validators = []; 187 | this.validationOpts.filter(opt => field.templateOptions[opt]).map((opt) => { 188 | validators.push(this.getValidation(opt, field.templateOptions[opt])); 189 | }); 190 | if (field.validators) { 191 | for (let validatorName in field.validators) { 192 | if (validatorName !== 'validation') { 193 | validators.push((control: FormControl) => { 194 | let validator = field.validators[validatorName]; 195 | if (isObject(validator)) { 196 | validator = validator.expression; 197 | } 198 | 199 | return validator(control) ? null : {[validatorName]: true}; 200 | }); 201 | } 202 | } 203 | } 204 | 205 | if (field.validators && Array.isArray(field.validators.validation)) { 206 | field.validators.validation.map(validate => { 207 | if (typeof validate === 'string') { 208 | validators.push(this.formlyConfig.getValidator(validate).validation); 209 | } else { 210 | validators.push(validate); 211 | } 212 | }); 213 | } 214 | 215 | if (validators.length) { 216 | if (field.validators && !Array.isArray(field.validators.validation)) { 217 | field.validators.validation = Validators.compose([field.validators.validation, ...validators]); 218 | } else { 219 | field.validators = { 220 | validation: Validators.compose(validators), 221 | }; 222 | } 223 | } 224 | } 225 | 226 | private addFormControl(form: FormGroup, field: FormlyFieldConfig, model) { 227 | /* Although the type of the key property in FormlyFieldConfig is declared to be a string, 228 | the recurstion of this FormBuilder uses an Array. 229 | This should probably be addressed somehow. */ 230 | let name: string = typeof field.key === 'string' ? field.key : field.key[0]; 231 | if (field.component && field.component.createControl) { 232 | form.addControl(name, field.component.createControl(model, field)); 233 | } else { 234 | form.addControl(name, new FormControl( 235 | { value: model, disabled: field.templateOptions.disabled }, 236 | field.validators ? field.validators.validation : undefined, 237 | field.asyncValidators ? field.asyncValidators.validation : undefined, 238 | )); 239 | } 240 | if (field.validation && field.validation.show) { 241 | form.get(field.key).markAsTouched(); 242 | } 243 | } 244 | 245 | private getValidation(opt, value) { 246 | switch (opt) { 247 | case this.validationOpts[0]: 248 | return Validators[opt]; 249 | case this.validationOpts[1]: 250 | case this.validationOpts[2]: 251 | case this.validationOpts[3]: 252 | return Validators[opt](value); 253 | case this.validationOpts[4]: 254 | case this.validationOpts[5]: 255 | return (changes) => { 256 | if (this.checkMinMax(opt, changes.value, value)) { 257 | return null; 258 | } else { 259 | return {[opt]: true}; 260 | } 261 | }; 262 | } 263 | } 264 | 265 | private checkMinMax(opt, changes, value) { 266 | if (opt === this.validationOpts[4]) { 267 | return parseInt(changes) > value; 268 | } else { 269 | return parseInt(changes) < value; 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/app/core/services/formly.messages.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormlyValidationMessages } from './formly.validation-messages'; 2 | 3 | describe('FormlyValidationMessages service', () => { 4 | let formlyMessages: FormlyValidationMessages; 5 | beforeEach(() => { 6 | formlyMessages = new FormlyValidationMessages([{ 7 | validationMessages: [ 8 | { name: 'required', message: 'This field is required.' }, 9 | ], 10 | }]); 11 | }); 12 | 13 | it('get validator error message', () => { 14 | expect(formlyMessages.getValidatorErrorMessage('required')).toEqual('This field is required.'); 15 | expect(formlyMessages.getValidatorErrorMessage('maxlength')).toEqual(undefined); 16 | }); 17 | 18 | it('add validator error message', () => { 19 | formlyMessages.addStringMessage('maxlength', 'Maximum Length Exceeded.'); 20 | expect(formlyMessages.getValidatorErrorMessage('maxlength')).toEqual('Maximum Length Exceeded.'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/core/services/formly.single.focus.dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | export type SingleFocusDispatcherListener = (key: string) => void; 3 | 4 | /** 5 | * Class to coordinate single focus based on key. 6 | * Intended to be consumed as an Angular service. 7 | * This service ensures that 'focus' is true for single field and, and also focus out on previous focused field. 8 | */ 9 | @Injectable() 10 | export class SingleFocusDispatcher { 11 | private _listeners: SingleFocusDispatcherListener[] = []; 12 | 13 | /** Notify other items that focus for the given key has been set. */ 14 | notify(key: string) { 15 | for (let listener of this._listeners) { 16 | listener(key); 17 | } 18 | } 19 | 20 | /** Listen for future changes to item selection. */ 21 | listen(listener: SingleFocusDispatcherListener) { 22 | this._listeners.push(listener); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/core/services/formly.validation-messages.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { FORMLY_CONFIG_TOKEN } from './formly.config'; 3 | 4 | @Injectable() 5 | export class FormlyValidationMessages { 6 | messages = {}; 7 | 8 | constructor(@Inject(FORMLY_CONFIG_TOKEN) configs = []) { 9 | configs.map(config => { 10 | if (config.validationMessages) { 11 | config.validationMessages.map(validation => this.addStringMessage(validation.name, validation.message)); 12 | } 13 | }); 14 | } 15 | 16 | addStringMessage(validator, message) { 17 | this.messages[validator] = message; 18 | } 19 | 20 | getMessages() { 21 | return this.messages; 22 | } 23 | 24 | getValidatorErrorMessage(prop) { 25 | return this.messages[prop]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/core/templates/field.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '@angular/core'; 2 | import { FormGroup, AbstractControl } from '@angular/forms'; 3 | import { FormlyTemplateOptions, FormlyFieldConfig } from '../components/formly.field.config'; 4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 5 | 6 | export abstract class Field { 7 | @Input() form: FormGroup; 8 | @Input() field: FormlyFieldConfig; 9 | @Input() model: any; 10 | @Input() options; 11 | 12 | valueChanges: BehaviorSubject; 13 | 14 | get key() { return this.field.key; } 15 | get formControl(): AbstractControl { return this.form.get(this.key); } 16 | 17 | /** 18 | * @deprecated Use `to` instead. 19 | **/ 20 | get templateOptions(): FormlyTemplateOptions { 21 | console.warn(`${this.constructor['name']}: 'templateOptions' is deprecated. Use 'to' instead.`); 22 | 23 | return this.to; 24 | } 25 | 26 | get to(): FormlyTemplateOptions { return this.field.templateOptions; } 27 | 28 | get valid(): boolean { return this.formControl.touched && !this.formControl.valid; } 29 | 30 | get id(): string { return this.field.id; } 31 | 32 | get formState() { return this.options.formState || {}; } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/core/templates/field.type.ts: -------------------------------------------------------------------------------- 1 | import { Field } from './field'; 2 | import { OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy, AfterViewChecked } from '@angular/core'; 3 | 4 | export abstract class FieldType extends Field implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy { 5 | 6 | ngOnInit() { 7 | this.lifeCycleHooks('onInit'); 8 | } 9 | 10 | ngOnChanges(changes) { 11 | this.lifeCycleHooks('onChanges'); 12 | } 13 | 14 | ngDoCheck() { 15 | this.lifeCycleHooks('doCheck'); 16 | } 17 | 18 | ngAfterContentInit() { 19 | this.lifeCycleHooks('afterContentInit'); 20 | } 21 | 22 | ngAfterContentChecked() { 23 | this.lifeCycleHooks('afterContentChecked'); 24 | } 25 | 26 | ngAfterViewInit() { 27 | this.lifeCycleHooks('afterViewInit'); 28 | } 29 | 30 | ngAfterViewChecked() { 31 | this.lifeCycleHooks('afterViewChecked'); 32 | } 33 | 34 | ngOnDestroy() { 35 | this.lifeCycleHooks('onDestroy'); 36 | } 37 | 38 | private get lifecycle() { 39 | return this.field.lifecycle; 40 | } 41 | 42 | private lifeCycleHooks(type) { 43 | if (this.lifecycle && this.lifecycle[type]) { 44 | this.lifecycle[type].bind(this)(this.form, this.field, this.model, this.options); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/core/templates/field.wrapper.ts: -------------------------------------------------------------------------------- 1 | import { ViewContainerRef } from '@angular/core'; 2 | import { Field } from './field'; 3 | 4 | export abstract class FieldWrapper extends Field { 5 | fieldComponent: ViewContainerRef; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/core/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 2 | 3 | export function createGenericTestComponent(html: string, type: {new (...args: any[]): T}): ComponentFixture { 4 | TestBed.overrideComponent(type, {set: {template: html}}); 5 | const fixture = TestBed.createComponent(type); 6 | fixture.detectChanges(); 7 | return fixture as ComponentFixture; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/core/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { reverseDeepMerge, assignModelValue, getFieldId, getValueForKey, getKey, evalExpression, getKeyPath, getFieldModel } from './utils'; 2 | import { FormlyFieldConfig } from './components/formly.field.config'; 3 | 4 | describe('FormlyUtils service', () => { 5 | describe('reverseDeepMerge', () => { 6 | it('should properly reverse deep merge', () => { 7 | let foo = {foo: 'bar', obj: {}}; 8 | let bar = {foo: 'foo', foobar: 'foobar', fun: () => console.log('demo'), obj: {}}; 9 | reverseDeepMerge(foo, bar); 10 | 11 | expect(foo['foo']).toEqual('bar'); 12 | expect(foo['foobar']).toEqual('foobar'); 13 | }); 14 | }); 15 | 16 | describe('assignModelValue', () => { 17 | it('should properly assign model value', () => { 18 | let model = {}; 19 | assignModelValue(model, 'path.to.save', 2); 20 | expect(model['path']['to']['save']).toBe(2); 21 | }); 22 | }); 23 | 24 | describe('getValueForKey', () => { 25 | it('should properly get value', () => { 26 | let model = { 27 | value: 2, 28 | 'looks.nested': 'foo', 29 | nested: { 30 | value: 'bar', 31 | }, 32 | }; 33 | expect(getValueForKey(model, 'path.to.save')).toBe(undefined); 34 | expect(getValueForKey(model, 'value')).toBe(2); 35 | expect(getValueForKey(model, 'looks.nested')).toBe(undefined); 36 | expect(getValueForKey(model, 'nested.value')).toBe('bar'); 37 | }); 38 | }); 39 | 40 | describe('getKey', () => { 41 | it('should properly get key', () => { 42 | expect(getKey('key', 'path.to.save')).toBe('path.to.save.key'); 43 | expect(getKey('key', undefined)).toBe('key'); 44 | }); 45 | }); 46 | 47 | describe('getFieldId', () => { 48 | it('should properly get the field id if id is set in options', () => { 49 | let options: FormlyFieldConfig = {id: '1'}; 50 | let id = getFieldId('formly_1', options, 2); 51 | expect(id).toBe('1'); 52 | }); 53 | 54 | it('should properly get the field id if id is not set in options', () => { 55 | let options: FormlyFieldConfig = {type: 'input', key: 'email'}; 56 | let id = getFieldId('formly_1', options, 2); 57 | expect(id).toBe('formly_1_input_email_2'); 58 | }); 59 | }); 60 | 61 | describe('getKeyPath', () => { 62 | 63 | it('should get an empty key path for an empty key', () => { 64 | let keyPath = getKeyPath({}); 65 | expect(keyPath).toEqual([]); 66 | keyPath = getKeyPath({key: null}); 67 | expect(keyPath).toEqual([]); 68 | keyPath = getKeyPath({key: ''}); 69 | expect(keyPath).toEqual([]); 70 | }); 71 | 72 | it('should get the correct key path of length 1 for a simple string', () => { 73 | let keyPath = getKeyPath({key: 'property'}); 74 | expect(keyPath).toEqual(['property']); 75 | }); 76 | 77 | it('should get the correct key path of length 2 for a simple string with an index', () => { 78 | let keyPath = getKeyPath({key: 'property[2]'}); 79 | expect(keyPath).toEqual(['property', 2]); 80 | }); 81 | 82 | it('should get the correct key path of length 3 for a simple nested property', () => { 83 | let keyPath = getKeyPath({key: 'property1.property2.property3'}); 84 | expect(keyPath).toEqual(['property1', 'property2', 'property3']); 85 | }); 86 | 87 | it('should get the correct key path of length 4 with one index for a nested property containing 1 index property', () => { 88 | let keyPath = getKeyPath({key: 'property1.property2[4].property3'}); 89 | expect(keyPath).toEqual(['property1', 'property2', 4, 'property3']); 90 | }); 91 | 92 | it('should get the correct key path of length 5 with one index for a complex array key', () => { 93 | let keyPath = getKeyPath({key: ['property1.property2[4].property3', 'property4']}); 94 | expect(keyPath).toEqual(['property1', 'property2', 4, 'property3', 'property4']); 95 | }); 96 | 97 | it('should get the correct key path if the path contains a numeric path element', () => { 98 | let keyPath = getKeyPath({key: ['property1.2.property2']}); 99 | expect(keyPath).toEqual(['property1', 2, 'property2']); 100 | }); 101 | 102 | it('should attach the key path to the field config', () => { 103 | let fieldConfig = {key: 'property1.property2[4].property3'}; 104 | getKeyPath(fieldConfig); 105 | expect(fieldConfig['_formlyKeyPath']).toEqual(['property1', 'property2', 4, 'property3']); 106 | }); 107 | 108 | }); 109 | 110 | }); 111 | 112 | 113 | describe ('getFieldModel', () => { 114 | 115 | it('should extract te correct simple property', () => { 116 | 117 | let config: FormlyFieldConfig = {key: 'property1'}; 118 | let model: any = {property1: 3}; 119 | let fieldModel: any = getFieldModel(model, config, true); 120 | expect(fieldModel).toEqual(3); 121 | 122 | }); 123 | 124 | 125 | it('should extract te correct nested property', () => { 126 | 127 | let config: FormlyFieldConfig = {key: 'property1.property2[2]'}; 128 | let model: any = {property1: {property2: [1, 1, 2]}}; 129 | let fieldModel: any = getFieldModel(model, config, true); 130 | expect(fieldModel).toEqual(2); 131 | 132 | config = {key: 'property1.property2[2].property3'}; 133 | model = {property1: {property2: [1, 1, {property3: 'test'}]}}; 134 | fieldModel = getFieldModel(model, config, true); 135 | expect(fieldModel).toEqual('test'); 136 | 137 | config = {key: 'property1.property2.property3'}; 138 | model = {property1: {property2: {property3: 'test'}}}; 139 | fieldModel = getFieldModel(model, config, true); 140 | expect(fieldModel).toEqual('test'); 141 | 142 | 143 | }); 144 | 145 | it('should create the necessary empty objects in a simple property path', () => { 146 | 147 | let config: FormlyFieldConfig = {key: 'property1'}; 148 | let model: any = {}; 149 | getFieldModel(model, config, true); 150 | expect(model).toEqual({}); 151 | 152 | config = {key: 'property1', fieldGroup: []}; 153 | model = {}; 154 | getFieldModel(model, config, true); 155 | expect(model).toEqual({property1: {}}); 156 | 157 | config = {key: 'property1', fieldArray: {}}; 158 | model = {}; 159 | getFieldModel(model, config, true); 160 | expect(model).toEqual({property1: []}); 161 | 162 | }); 163 | 164 | it('should create the necessary empty objects in a nested property path', () => { 165 | 166 | let config: FormlyFieldConfig = {key: 'property1.property2'}; 167 | let model: any = {}; 168 | getFieldModel(model, config, true); 169 | expect(model).toEqual({property1: {}}); 170 | 171 | config = {key: 'property1.property2', fieldGroup: []}; 172 | model = {}; 173 | getFieldModel(model, config, true); 174 | expect(model).toEqual({property1: {property2: {}}}); 175 | 176 | config = {key: 'property1.property2', fieldArray: {}}; 177 | model = {}; 178 | getFieldModel(model, config, true); 179 | expect(model).toEqual({property1: {property2: []}}); 180 | 181 | config = {key: 'property1.property2.property3'}; 182 | model = {}; 183 | getFieldModel(model, config, true); 184 | expect(model).toEqual({property1: {property2: {}}}); 185 | 186 | config = {key: 'property1.property2.property3', fieldGroup: []}; 187 | model = {}; 188 | getFieldModel(model, config, true); 189 | expect(model).toEqual({property1: {property2: {property3: {}}}}); 190 | 191 | config = {key: 'property1.property2.property3', fieldArray: {}}; 192 | model = {}; 193 | getFieldModel(model, config, true); 194 | expect(model).toEqual({property1: {property2: {property3: []}}}); 195 | 196 | config = {key: 'property1.property2[2]'}; 197 | model = {}; 198 | getFieldModel(model, config, true); 199 | expect(model).toEqual({property1: {property2: []}}); 200 | 201 | config = {key: 'property1.property2[2]', fieldGroup: []}; 202 | model = {}; 203 | getFieldModel(model, config, true); 204 | expect(model).toEqual({property1: {property2: [undefined, undefined, {}]}}); 205 | 206 | config = {key: 'property1.property2[2]', fieldArray: {}}; 207 | model = {}; 208 | getFieldModel(model, config, true); 209 | expect(model).toEqual({property1: {property2: [undefined, undefined, []]}}); 210 | 211 | config = {key: 'property1.property2[2].property3', fieldGroup: []}; 212 | model = {}; 213 | getFieldModel(model, config, true); 214 | expect(model).toEqual({property1: {property2: [undefined, undefined, {property3: {}}]}}); 215 | 216 | config = {key: 'property1.property2[2].property3', fieldArray: {}}; 217 | model = {}; 218 | getFieldModel(model, config, true); 219 | expect(model).toEqual({property1: {property2: [undefined, undefined, {property3: []}]}}); 220 | 221 | config = {key: 'property1.property2[2].property3'}; 222 | model = {}; 223 | getFieldModel(model, config, true); 224 | expect(model).toEqual({property1: {property2: [undefined, undefined, {}]}}); 225 | 226 | }); 227 | 228 | describe('evalExpression', () => { 229 | it('should evaluate the value correctly', () => { 230 | let expression = () => { return this.model.val; }; 231 | this.model = { 232 | val: 2, 233 | }; 234 | expect(evalExpression(expression, this, [this.model])).toBe(2); 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /src/app/core/utils.ts: -------------------------------------------------------------------------------- 1 | import { FormlyFieldConfig } from './core'; 2 | 3 | export function getFieldId(formId: string, options: FormlyFieldConfig, index: string|number) { 4 | if (options.id) return options.id; 5 | let type = options.type; 6 | if (!type && options.template) type = 'template'; 7 | return [formId, type, options.key, index].join('_'); 8 | } 9 | 10 | export function getKeyPath(field: {key?: string|string[], fieldGroup?: any, fieldArray?: any}): (string|number)[] { 11 | /* We store the keyPath in the field for performance reasons. This function will be called frequently. */ 12 | if (field['_formlyKeyPath'] !== undefined) { 13 | return field['_formlyKeyPath']; 14 | } 15 | let keyPath: (string|number)[] = []; 16 | if (field.key) { 17 | /* Also allow for an array key, hence the type check */ 18 | let pathElements = typeof field.key === 'string' ? field.key.split('.') : field.key; 19 | for (let pathElement of pathElements) { 20 | if (typeof pathElement === 'string') { 21 | /* replace paths of the form names[2] by names.2, cfr. angular formly */ 22 | pathElement = pathElement.replace(/\[(\w+)\]/g, '.$1'); 23 | keyPath = keyPath.concat(pathElement.split('.')); 24 | } else { 25 | keyPath.push(pathElement); 26 | } 27 | } 28 | for (let i = 0; i < keyPath.length; i++) { 29 | let pathElement = keyPath[i]; 30 | if (typeof pathElement === 'string' && stringIsInteger(pathElement)) { 31 | keyPath[i] = parseInt(pathElement); 32 | } 33 | } 34 | } 35 | field['_formlyKeyPath'] = keyPath; 36 | return keyPath; 37 | } 38 | 39 | function stringIsInteger(str: string) { 40 | return !isNullOrUndefined(str) && /^\d+$/.test(str); 41 | } 42 | 43 | export function getFieldModel(model: any, field: FormlyFieldConfig, constructEmptyObjects: boolean): any { 44 | let keyPath: (string|number)[] = getKeyPath(field); 45 | let value: any = model; 46 | for (let i = 0; i < keyPath.length; i++) { 47 | let path = keyPath[i]; 48 | let pathValue = value[path]; 49 | if (isNullOrUndefined(pathValue) && constructEmptyObjects) { 50 | if (i < keyPath.length - 1) { 51 | /* TODO? : It would be much nicer if we could construct object instances of the correct class, for instance by using factories. */ 52 | value[path] = typeof keyPath[i + 1] === 'number' ? [] : {}; 53 | } else if (field.fieldGroup) { 54 | value[path] = {}; 55 | } else if (field.fieldArray) { 56 | value[path] = []; 57 | } 58 | } 59 | value = value[path]; 60 | if (!value) { 61 | break; 62 | } 63 | } 64 | return value; 65 | } 66 | 67 | export function assignModelValue(model, path, value) { 68 | if (typeof path === 'string') { 69 | path = path.split('.'); 70 | } 71 | 72 | if (path.length > 1) { 73 | const e = path.shift(); 74 | if (!model[e]) { 75 | model[e] = isNaN(path[0]) ? {} : []; 76 | } 77 | assignModelValue(model[e], path, value); 78 | } else { 79 | model[path[0]] = value; 80 | } 81 | } 82 | 83 | export function getValueForKey(model, path) { 84 | if (typeof path === 'string') { 85 | path = path.split('.'); 86 | } 87 | if (path.length > 1) { 88 | const e = path.shift(); 89 | if (!model[e]) { 90 | model[e] = isNaN(path[0]) ? {} : []; 91 | } 92 | return getValueForKey(model[e], path); 93 | } else { 94 | return model[path[0]]; 95 | } 96 | } 97 | 98 | export function getKey(controlKey: string, actualKey: string) { 99 | return actualKey ? actualKey + '.' + controlKey : controlKey; 100 | } 101 | 102 | export function reverseDeepMerge(dest, source = undefined) { 103 | let args = Array.prototype.slice.call(arguments); 104 | if (!args[1]) { 105 | return dest; 106 | } 107 | args.forEach((src, index) => { 108 | if (!index) { 109 | return; 110 | } 111 | for (let srcArg in src) { 112 | if (isNullOrUndefined(dest[srcArg]) || isBlankString(dest[srcArg])) { 113 | if (isFunction(src[srcArg])) { 114 | dest[srcArg] = src[srcArg]; 115 | } else { 116 | dest[srcArg] = clone(src[srcArg]); 117 | } 118 | } else if (objAndSameType(dest[srcArg], src[srcArg])) { 119 | reverseDeepMerge(dest[srcArg], src[srcArg]); 120 | } 121 | } 122 | }); 123 | return dest; 124 | } 125 | 126 | export function isNullOrUndefined(value) { 127 | return value === undefined || value === null; 128 | } 129 | 130 | export function isBlankString(value) { 131 | return value === ''; 132 | } 133 | 134 | export function isFunction(value) { 135 | return typeof(value) === 'function'; 136 | } 137 | 138 | export function objAndSameType(obj1, obj2) { 139 | return isObject(obj1) && isObject(obj2) && 140 | Object.getPrototypeOf(obj1) === Object.getPrototypeOf(obj2); 141 | } 142 | 143 | export function isObject(x) { 144 | return x != null && typeof x === 'object'; 145 | } 146 | 147 | export function clone(value) { 148 | if (!isObject(value)) { 149 | return value; 150 | } 151 | return Array.isArray(value) ? value.slice(0) : (Object).assign({}, value); 152 | } 153 | 154 | export function evalStringExpression(expression: string, argNames: string[]) { 155 | try { 156 | return Function.bind.apply(Function, [void 0].concat(argNames.concat(`return ${expression};`)))(); 157 | } catch (error) { 158 | console.error(error); 159 | } 160 | } 161 | 162 | export function evalExpressionValueSetter(expression: string, argNames: string[]) { 163 | try { 164 | return Function.bind 165 | .apply(Function, [void 0].concat(argNames.concat(`${expression} = expressionValue;`)))(); 166 | } catch (error) { 167 | console.error(error); 168 | } 169 | } 170 | 171 | export function evalExpression(expression: string | Function | boolean, thisArg: any, argVal: any[]): boolean { 172 | if (expression instanceof Function) { 173 | return expression.apply(thisArg, argVal); 174 | } else { 175 | return expression ? true : false; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core/core' 2 | export * from './ui-bootstrap/ui-bootstrap' 3 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformNativeScriptDynamic, NativeScriptModule, NativeScriptFormsModule } from "nativescript-angular"; 2 | // angular 3 | import { NgModule } from "@angular/core"; 4 | 5 | import { DemoComponent } from './app.component'; 6 | 7 | import { FormlyModule, FormlyBootstrapModule } from "ng-formly-nativescript"; 8 | 9 | 10 | @NgModule({ 11 | imports: [ 12 | NativeScriptModule, 13 | NativeScriptFormsModule, 14 | FormlyModule.forRoot(), 15 | FormlyBootstrapModule 16 | ], 17 | declarations: [ 18 | DemoComponent, 19 | ], 20 | bootstrap: [ 21 | DemoComponent 22 | ], 23 | }) 24 | class DemoModule { } 25 | 26 | platformNativeScriptDynamic().bootstrapModule(DemoModule); 27 | -------------------------------------------------------------------------------- /src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "nativescript": { 4 | "id": "org.nativescript.helloworldng" 5 | }, 6 | "name": "tns-template-hello-world-ng", 7 | "main": "main.js", 8 | "version": "2.3.3", 9 | "author": "Telerik ", 10 | "description": "Nativescript Angular Hello World template", 11 | "license": "BSD", 12 | "keywords": [ 13 | "telerik", 14 | "mobile", 15 | "angular", 16 | "nativescript", 17 | "{N}", 18 | "tns", 19 | "appbuilder", 20 | "template" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/NativeScript/template-hello-world-ng" 25 | }, 26 | "homepage": "https://github.com/NativeScript/template-hello-world-ng", 27 | "android": { 28 | "v8Flags": "--expose_gc" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/formly.validation-message.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 2 | import { createGenericTestComponent } from '../core/test-utils'; 3 | 4 | import { Component } from '@angular/core'; 5 | import { FormControl, Validators } from '@angular/forms'; 6 | import { FormlyModule, FormlyFieldConfig } from '../core/core'; 7 | import { FormlyValidationMessage } from './formly.validation-message'; 8 | 9 | const createTestComponent = (html: string) => 10 | createGenericTestComponent(html, TestComponent) as ComponentFixture; 11 | 12 | function getFormlyValidationMessageElement(element: HTMLElement): HTMLDivElement { 13 | return element.querySelector('formly-validation-message'); 14 | } 15 | 16 | describe('FormlyValidationMessage Component', () => { 17 | beforeEach(() => { 18 | TestBed.configureTestingModule({ 19 | declarations: [FormlyValidationMessage, TestComponent], 20 | imports: [ 21 | FormlyModule.forRoot({ 22 | validationMessages: [ 23 | { name: 'required', message: (err, field) => `${field.templateOptions.label} is required.`}, 24 | { name: 'maxlength', message: 'Maximum Length Exceeded.' }, 25 | ], 26 | }), 27 | ], 28 | }); 29 | }); 30 | 31 | it('should not render message with a valid value', () => { 32 | const fixture = createTestComponent(''); 33 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement); 34 | fixture.componentInstance.formControl.setValue('12'); 35 | fixture.detectChanges(); 36 | 37 | expect(formlyMessageElm.textContent).not.toMatch(/Maximum Length Exceeded/); 38 | expect(formlyMessageElm.textContent).not.toMatch(/Title is required/); 39 | }); 40 | 41 | describe('render validation message', () => { 42 | it('with a simple validation message', () => { 43 | const fixture = createTestComponent(''); 44 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement); 45 | fixture.componentInstance.formControl.setValue('test'); 46 | fixture.detectChanges(); 47 | 48 | expect(formlyMessageElm.textContent).toMatch(/Maximum Length Exceeded/); 49 | expect(formlyMessageElm.textContent).not.toMatch(/Title is required/); 50 | }); 51 | 52 | it('with a function validation message', () => { 53 | const fixture = createTestComponent(''); 54 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement); 55 | 56 | expect(formlyMessageElm.textContent).toMatch(/Title is required/); 57 | expect(formlyMessageElm.textContent).not.toMatch(/Maximum Length Exceeded/); 58 | }); 59 | 60 | it('with a validator.message property', () => { 61 | const fixture = createTestComponent(''); 62 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement); 63 | fixture.componentInstance.field = (Object).assign({}, fixture.componentInstance.field, { 64 | validators: { 65 | required: { 66 | expression: (control: FormControl) => false, 67 | message: `Custom title: Should have atleast 3 Characters`, 68 | }, 69 | }, 70 | }); 71 | 72 | fixture.detectChanges(); 73 | 74 | expect(formlyMessageElm.textContent).toMatch(/Custom title: Should have atleast 3 Characters/); 75 | expect(formlyMessageElm.textContent).not.toMatch(/Maximum Length Exceeded/); 76 | expect(formlyMessageElm.textContent).not.toMatch(/Title is required/); 77 | }); 78 | }); 79 | 80 | }); 81 | 82 | @Component({selector: 'formly-validation-message-test', template: '', entryComponents: []}) 83 | class TestComponent { 84 | formControl = new FormControl(null, [Validators.required, Validators.maxLength(3)]); 85 | field: FormlyFieldConfig = { 86 | type: 'input', 87 | key: 'title', 88 | templateOptions: { 89 | label: 'Title', 90 | }, 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/formly.validation-message.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { FormlyFieldConfig, FormlyValidationMessages } from '../core/core'; 4 | 5 | @Component({ 6 | selector: 'formly-validation-message', 7 | template: ``, 8 | }) 9 | export class FormlyValidationMessage { 10 | @Input() fieldForm: FormControl; 11 | @Input() field: FormlyFieldConfig; 12 | 13 | constructor(private formlyMessages: FormlyValidationMessages) {} 14 | 15 | get errorMessage() { 16 | for (let error in this.fieldForm.errors) { 17 | if (this.fieldForm.errors.hasOwnProperty(error)) { 18 | let message = this.formlyMessages.getValidatorErrorMessage(error); 19 | ['validators', 'asyncValidators'].map(validators => { 20 | if (this.field[validators] && this.field[validators][error] && this.field[validators][error].message) { 21 | message = this.field.validators[error].message; 22 | } 23 | }); 24 | 25 | if (typeof message === 'function') { 26 | return message(this.fieldForm.errors[error], this.field); 27 | } 28 | 29 | return message; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/run/addon.ts: -------------------------------------------------------------------------------- 1 | import { FormlyConfig } from '../../core/core'; 2 | 3 | export class TemplateAddons { 4 | run(fc: FormlyConfig) { 5 | fc.templateManipulators.postWrapper.push((field) => { 6 | if (field && field.templateOptions && (field.templateOptions.addonLeft || field.templateOptions.addonRight)) { 7 | return 'addons'; 8 | } 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/run/description.ts: -------------------------------------------------------------------------------- 1 | import { FormlyFieldConfig, FormlyConfig } from '../../core/core'; 2 | 3 | export class TemplateDescription { 4 | run(fc: FormlyConfig) { 5 | fc.templateManipulators.postWrapper.push((field: FormlyFieldConfig) => { 6 | if (field && field.templateOptions && field.templateOptions.description) { 7 | return 'description'; 8 | } 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/run/validation.ts: -------------------------------------------------------------------------------- 1 | import { FormlyFieldConfig, FormlyConfig } from '../../core/core'; 2 | 3 | export class TemplateValidation { 4 | run(fc: FormlyConfig) { 5 | fc.templateManipulators.postWrapper.push((field: FormlyFieldConfig) => { 6 | if (field && field.validators) { 7 | return 'validation-message'; 8 | } 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/types/checkbox.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { FormControl, AbstractControl } from '@angular/forms'; 3 | import { FieldType, FormlyFieldConfig } from '../../core/core'; 4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 5 | 6 | @Component({ 7 | selector: 'formly-field-checkbox', 8 | template: ` 9 | 10 | 13 | 14 | 15 | ` 16 | }) 17 | export class FormlyFieldCheckbox extends FieldType { 18 | 19 | constructor() { 20 | super(); 21 | this.valueChanges = new BehaviorSubject(false); 22 | } 23 | 24 | static createControl(model: any, field: FormlyFieldConfig): AbstractControl { 25 | return new FormControl( 26 | { checked: model ? 'true' : 'false', disabled: field.templateOptions.disabled }, 27 | field.validators ? field.validators.validation : undefined, 28 | field.asyncValidators ? field.asyncValidators.validation : undefined, 29 | ); 30 | } 31 | 32 | public onPropertyChanged(event: any): void { 33 | if (event.propertyName === 'checked') { 34 | this.valueChanges.next(event.value); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/types/input.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { FieldType } from '../../core/core'; 3 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 4 | 5 | @Component({ 6 | selector: 'formly-field-input', 7 | template: ` 8 | 9 | 10 | 11 | ` 12 | }) 13 | export class FormlyFieldInput extends FieldType { 14 | 15 | constructor() { 16 | super(); 17 | this.valueChanges = new BehaviorSubject(''); 18 | } 19 | 20 | get type() { 21 | return this.to.type || 'text'; 22 | } 23 | 24 | public onPropertyChanged(event: any): void { 25 | if (event.propertyName === 'text') { 26 | this.valueChanges.next(event.value); 27 | } 28 | } 29 | 30 | ngOnInit() { 31 | // init with existing value 32 | this.valueChanges.next(this.model[this.key]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/types/multicheckbox.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormGroup, FormControl, AbstractControl } from '@angular/forms'; 3 | import { FieldType, FormlyFieldConfig } from '../../core/core'; 4 | 5 | @Component({ 6 | selector: 'formly-field-multicheckbox', 7 | template: ` 8 | 9 | 10 | 11 | 12 | `, 13 | }) 14 | export class FormlyFieldMultiCheckbox extends FieldType { 15 | static createControl(model: any, field: FormlyFieldConfig): AbstractControl { 16 | let controlGroupConfig = field.templateOptions.options.reduce((previous, option) => { 17 | previous[option.key] = new FormControl(model ? model[option.key] : undefined); 18 | return previous; 19 | }, {}); 20 | 21 | return new FormGroup(controlGroupConfig); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/types/radio.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FieldType } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-field-radio', 6 | template: ` 7 | 8 | 9 | 11 | 12 | 13 | 14 | `, 15 | }) 16 | export class FormlyFieldRadio extends FieldType {} 17 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/types/select.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FieldType } from '../../core/core'; 3 | 4 | export class SelectOption { 5 | label: string; 6 | value?: string; 7 | group?: SelectOption[]; 8 | 9 | constructor(label: string, value?: string, children?: SelectOption[]) { 10 | this.label = label; 11 | this.value = value; 12 | this.group = children; 13 | } 14 | } 15 | 16 | 17 | @Component({ 18 | selector: 'formly-field-select', 19 | template: `` 20 | // template: ` 21 | // 32 | // `, 33 | }) 34 | export class FormlyFieldSelect extends FieldType { 35 | get labelProp(): string { return this.to['labelProp'] || 'label'; } 36 | get valueProp(): string { return this.to['valueProp'] || 'value'; } 37 | get groupProp(): string { return this.to['groupProp'] || 'group'; } 38 | 39 | get selectOptions() { 40 | let options: SelectOption[] = []; 41 | this.to.options.map((option: SelectOption) => { 42 | if (!option[this.groupProp]) { 43 | options.push(option); 44 | } else { 45 | let filteredOption: SelectOption[] = options.filter((filteredOption) => { 46 | return filteredOption.label === option[this.groupProp]; 47 | }); 48 | if (filteredOption[0]) { 49 | filteredOption[0].group.push({ 50 | label: option[this.labelProp], 51 | value: option[this.valueProp], 52 | }); 53 | } 54 | else { 55 | options.push({ 56 | label: option[this.groupProp], 57 | group: [{ value: option[this.valueProp], label: option[this.labelProp] }], 58 | }); 59 | } 60 | } 61 | }); 62 | return options; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/types/textarea.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FieldType } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-field-textarea', 6 | template: ` 7 | 9 | 10 | `, 11 | }) 12 | export class FormlyFieldTextArea extends FieldType { 13 | } 14 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/types/types.ts: -------------------------------------------------------------------------------- 1 | import { FormlyFieldCheckbox } from './checkbox'; 2 | import { FormlyFieldMultiCheckbox } from './multicheckbox'; 3 | import { FormlyFieldInput } from './input'; 4 | import { FormlyFieldRadio } from './radio'; 5 | import { FormlyFieldTextArea } from './textarea'; 6 | import { FormlyFieldSelect } from './select'; 7 | 8 | export { 9 | FormlyFieldCheckbox, 10 | FormlyFieldMultiCheckbox, 11 | FormlyFieldInput, 12 | FormlyFieldRadio, 13 | FormlyFieldTextArea, 14 | FormlyFieldSelect, 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/ui-bootstrap.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigOption } from '../core/services/formly.config'; 2 | import { FormlyWrapperAddons } from './wrappers/addons'; 3 | import { TemplateDescription } from './run/description'; 4 | import { TemplateValidation } from './run/validation'; 5 | import { TemplateAddons } from './run/addon'; 6 | import { 7 | FormlyFieldInput, 8 | FormlyFieldCheckbox, 9 | FormlyFieldRadio, 10 | FormlyFieldSelect, 11 | FormlyFieldTextArea, 12 | FormlyFieldMultiCheckbox, 13 | } from './types/types'; 14 | import { 15 | FormlyWrapperLabel, 16 | FormlyWrapperSideLabel, 17 | FormlyWrapperDescription, 18 | FormlyWrapperValidationMessages, 19 | FormlyWrapperFieldset, 20 | } from './wrappers/wrappers'; 21 | 22 | export const FIELD_TYPE_COMPONENTS = [ 23 | // types 24 | FormlyFieldInput, 25 | FormlyFieldCheckbox, 26 | FormlyFieldRadio, 27 | FormlyFieldSelect, 28 | FormlyFieldTextArea, 29 | FormlyFieldMultiCheckbox, 30 | 31 | // wrappers 32 | FormlyWrapperLabel, 33 | FormlyWrapperSideLabel, 34 | FormlyWrapperDescription, 35 | FormlyWrapperValidationMessages, 36 | FormlyWrapperFieldset, 37 | FormlyWrapperAddons, 38 | ]; 39 | 40 | export const BOOTSTRAP_FORMLY_CONFIG: ConfigOption = { 41 | types: [ 42 | { 43 | name: 'input', 44 | component: FormlyFieldInput, 45 | wrappers: ['label'], 46 | }, 47 | { 48 | name: 'checkbox', 49 | component: FormlyFieldCheckbox, 50 | wrappers: ['fieldset'], 51 | }, 52 | { 53 | name: 'radio', 54 | component: FormlyFieldRadio, 55 | wrappers: ['fieldset', 'label'], 56 | }, 57 | { 58 | name: 'select', 59 | component: FormlyFieldSelect, 60 | wrappers: ['fieldset', 'label'], 61 | }, 62 | { 63 | name: 'textarea', 64 | component: FormlyFieldTextArea, 65 | wrappers: ['fieldset', 'label'], 66 | }, 67 | { 68 | name: 'multicheckbox', 69 | component: FormlyFieldMultiCheckbox, 70 | wrappers: ['fieldset', 'label'], 71 | }, 72 | ], 73 | wrappers: [ 74 | {name: 'label', component: FormlyWrapperLabel}, 75 | {name: 'sideLabel', component: FormlyWrapperSideLabel}, 76 | {name: 'description', component: FormlyWrapperDescription}, 77 | {name: 'validation-message', component: FormlyWrapperValidationMessages}, 78 | {name: 'fieldset', component: FormlyWrapperFieldset}, 79 | {name: 'addons', component: FormlyWrapperAddons}, 80 | ], 81 | manipulators: [ 82 | {class: TemplateDescription, method: 'run'}, 83 | {class: TemplateValidation, method: 'run'}, 84 | {class: TemplateAddons, method: 'run'}, 85 | ], 86 | }; 87 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/ui-bootstrap.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NativeScriptModule, NativeScriptFormsModule } from "nativescript-angular"; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { FormlyModule } from '../core/core'; 5 | import { BOOTSTRAP_FORMLY_CONFIG, FIELD_TYPE_COMPONENTS } from './ui-bootstrap.config'; 6 | import { FormlyValidationMessage } from './formly.validation-message'; 7 | 8 | @NgModule({ 9 | declarations: [...FIELD_TYPE_COMPONENTS, FormlyValidationMessage], 10 | imports: [ 11 | NativeScriptModule, 12 | ReactiveFormsModule, 13 | FormlyModule.forRoot(BOOTSTRAP_FORMLY_CONFIG), 14 | ], 15 | }) 16 | export class FormlyBootstrapModule { 17 | } 18 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/ui-bootstrap.ts: -------------------------------------------------------------------------------- 1 | export * from './types/types'; 2 | export * from './wrappers/wrappers'; 3 | export { FormlyValidationMessage } from './formly.validation-message'; 4 | export { FormlyBootstrapModule } from './ui-bootstrap.module'; 5 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/wrappers/addons.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; 2 | import { FieldWrapper } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-wrapper-addons', 6 | template: ` 7 | 8 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | `, 23 | encapsulation: ViewEncapsulation.None 24 | }) 25 | export class FormlyWrapperAddons extends FieldWrapper { 26 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 27 | 28 | addonRightClick($event) { 29 | if (this.to['addonRight'].onClick) { 30 | this.to['addonRight'].onClick(this.to, this, $event); 31 | } 32 | } 33 | 34 | addonLeftClick($event) { 35 | if (this.to['addonLeft'].onClick) { 36 | this.to['addonLeft'].onClick(this.to, this, $event); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/wrappers/description.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; 2 | import { FieldWrapper } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-wrapper-description', 6 | template: ` 7 | 8 | 9 | 10 | 11 | ` 12 | }) 13 | export class FormlyWrapperDescription extends FieldWrapper { 14 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/wrappers/fieldset.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; 2 | import { FieldWrapper } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-wrapper-fieldset', 6 | template: ` 7 | 8 | 9 | 10 | ` 11 | }) 12 | export class FormlyWrapperFieldset extends FieldWrapper { 13 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/wrappers/label.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; 2 | import { FieldWrapper } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-wrapper-label', 6 | template: ` 7 | 8 | 9 | 10 | 11 | 12 | ` 13 | }) 14 | export class FormlyWrapperLabel extends FieldWrapper { 15 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/wrappers/message-validation.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; 2 | import { FieldWrapper } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-wrapper-validation-messages', 6 | template: ` 7 | 8 | 9 | 10 | 11 | `, 12 | encapsulation: ViewEncapsulation.None 13 | }) 14 | export class FormlyWrapperValidationMessages extends FieldWrapper { 15 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 16 | 17 | get validationId() { 18 | return this.field.id + '-message'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/wrappers/sideLabel.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; 2 | import { FieldWrapper } from '../../core/core'; 3 | 4 | @Component({ 5 | selector: 'formly-wrapper-side-label', 6 | template: ` 7 | 8 | 9 | 10 | 11 | 12 | ` 13 | }) 14 | export class FormlyWrapperSideLabel extends FieldWrapper { 15 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/ui-bootstrap/wrappers/wrappers.ts: -------------------------------------------------------------------------------- 1 | import { FormlyWrapperFieldset } from './fieldset'; 2 | import { FormlyWrapperLabel } from './label'; 3 | import { FormlyWrapperSideLabel } from './sideLabel'; 4 | import { FormlyWrapperDescription } from './description'; 5 | import { FormlyWrapperValidationMessages } from './message-validation'; 6 | 7 | export { 8 | FormlyWrapperFieldset, 9 | FormlyWrapperLabel, 10 | FormlyWrapperSideLabel, 11 | FormlyWrapperDescription, 12 | FormlyWrapperValidationMessages, 13 | }; 14 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "NativeScript Application", 3 | "license": "SEE LICENSE IN ", 4 | "readme": "NativeScript Application", 5 | "repository": "", 6 | "nativescript": { 7 | "id": "org.nativescript.demo", 8 | "tns-ios": { 9 | "version": "2.4.0" 10 | }, 11 | "tns-android": { 12 | "version": "2.4.0" 13 | } 14 | }, 15 | "dependencies": { 16 | "@angular/common": "~2.2.1", 17 | "@angular/compiler": "~2.2.1", 18 | "@angular/core": "~2.2.1", 19 | "@angular/forms": "~2.2.1", 20 | "@angular/http": "~2.2.1", 21 | "@angular/platform-browser": "~2.2.1", 22 | "@angular/platform-browser-dynamic": "~2.2.1", 23 | "@angular/router": "~3.2.1", 24 | "@types/jasmine": "^2.5.35", 25 | "es6-promise": "~3.1.2", 26 | "es6-shim": "^0.35.0", 27 | "nativescript-angular": "next", 28 | "nativescript-theme-core": "^1.0.2", 29 | "ng-formly-nativescript": "file:..", 30 | "parse5": "1.4.2", 31 | "punycode": "1.3.2", 32 | "querystring": "0.2.0", 33 | "reflect-metadata": "0.1.3", 34 | "rimraf": "^2.5.1", 35 | "rxjs": "5.0.0-beta.12", 36 | "tns-core-modules": "2.4.0", 37 | "url": "0.10.3" 38 | }, 39 | "devDependencies": { 40 | "@types/jasmine": "^2.5.35", 41 | "babel-traverse": "6.18.0", 42 | "babel-types": "6.18.0", 43 | "babylon": "6.13.1", 44 | "lazy": "1.0.11", 45 | "nativescript-dev-typescript": "^0.3.2", 46 | "typescript": "^2.0.10", 47 | "zone.js": "~0.6.21" 48 | } 49 | } -------------------------------------------------------------------------------- /src/references.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "lib": [ 11 | "dom" 12 | ], 13 | "sourceMap": true, 14 | "pretty": true, 15 | "allowUnreachableCode": true, 16 | "allowUnusedLabels": false, 17 | "noImplicitAny": false, 18 | "noImplicitReturns": false, 19 | "noImplicitUseStrict": false, 20 | "noFallthroughCasesInSwitch": true, 21 | "typeRoots": [ 22 | "node_modules/@types", 23 | "node_modules" 24 | ], 25 | "types": [ 26 | "jasmine" 27 | ] 28 | }, 29 | "exclude": [ 30 | "node_modules", 31 | "platforms" 32 | ], 33 | "compileOnSave": false 34 | } 35 | -------------------------------------------------------------------------------- /test-main.js: -------------------------------------------------------------------------------- 1 | debugger; 2 | if (!Object.hasOwnProperty('name')) { 3 | Object.defineProperty(Function.prototype, 'name', { 4 | get: function() { 5 | var matches = this.toString().match(/^\s*function\s*(\S*)\s*\(/); 6 | var name = matches && matches.length > 1 ? matches[1] : ""; 7 | Object.defineProperty(this, 'name', {value: name}); 8 | return name; 9 | } 10 | }); 11 | } 12 | 13 | // Turn on full stack traces in errors to help debugging 14 | Error.stackTraceLimit = Infinity; 15 | 16 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; 17 | 18 | // Cancel Karma's synchronous start, 19 | // we will call `__karma__.start()` later, once all the specs are loaded. 20 | __karma__.loaded = function() {}; 21 | 22 | // Load our SystemJS configuration. 23 | System.config({ 24 | baseURL: '/base/' 25 | }); 26 | 27 | System.config({ 28 | defaultJSExtensions: true, 29 | paths: { 30 | 'application': 'node_modules/tns-core-modules/application/application.ios.js', 31 | 'ui-dialogs/*': 'node_modules/tns-core-modules/ui/dialogs/*.js' 32 | }, 33 | map: { 34 | '@angular': 'node_modules/@angular', 35 | 'rxjs': 'node_modules/rxjs' 36 | }, 37 | packages: { 38 | '@angular/core': { 39 | main: 'index.js', 40 | defaultExtension: 'js' 41 | }, 42 | '@angular/compiler': { 43 | main: 'index.js', 44 | defaultExtension: 'js' 45 | }, 46 | '@angular/common': { 47 | main: 'index.js', 48 | defaultExtension: 'js' 49 | }, 50 | '@angular/http': { 51 | main: 'index.js', 52 | defaultExtension: 'js' 53 | }, 54 | '@angular/platform-browser': { 55 | main: 'index.js', 56 | defaultExtension: 'js' 57 | }, 58 | '@angular/platform-browser-dynamic': { 59 | main: 'index.js', 60 | defaultExtension: 'js' 61 | }, 62 | '@angular/router-deprecated': { 63 | main: 'index.js', 64 | defaultExtension: 'js' 65 | }, 66 | '@angular/router': { 67 | main: 'index.js', 68 | defaultExtension: 'js' 69 | }, 70 | 'rxjs': { 71 | defaultExtension: 'js' 72 | } 73 | } 74 | }); 75 | 76 | Promise.all([ 77 | System.import('@angular/core/testing'), 78 | System.import('@angular/platform-browser-dynamic/testing') 79 | ]).then(function (providers) { 80 | debugger; 81 | var testing = providers[0]; 82 | var testingBrowser = providers[1]; 83 | 84 | testing.setBaseTestProviders(testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, 85 | testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS); 86 | 87 | }).then(function() { 88 | return Promise.all( 89 | Object.keys(window.__karma__.files) // All files served by Karma. 90 | .filter(onlySpecFiles) 91 | .map(file2moduleName) 92 | .map(function(path) { 93 | return System.import(path).then(function(module) { 94 | if (module.hasOwnProperty('main')) { 95 | module.main(); 96 | } else { 97 | throw new Error('Module ' + path + ' does not implement main() method.'); 98 | } 99 | }); 100 | })); 101 | }) 102 | .then(function() { 103 | __karma__.start(); 104 | }, function(error) { 105 | console.error(error.stack || error); 106 | __karma__.start(); 107 | }); 108 | 109 | function onlySpecFiles(path) { 110 | // check for individual files, if not given, always matches to all 111 | var patternMatched = __karma__.config.files ? 112 | path.match(new RegExp(__karma__.config.files)) : true; 113 | 114 | return patternMatched && /[\.|_]spec\.js$/.test(path); 115 | } 116 | 117 | // Normalize paths to module names. 118 | function file2moduleName(filePath) { 119 | return filePath.replace(/\\/g, '/') 120 | .replace(/^\/base\//, '') 121 | .replace(/\.js$/, ''); 122 | } 123 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "lib": [ 11 | "dom" 12 | ], 13 | "sourceMap": true, 14 | "pretty": true, 15 | "allowUnreachableCode": true, 16 | "allowUnusedLabels": false, 17 | "noImplicitAny": false, 18 | "noImplicitReturns": false, 19 | "noImplicitUseStrict": false, 20 | "noFallthroughCasesInSwitch": true, 21 | "typeRoots": [ 22 | "node_modules/@types", 23 | "node_modules" 24 | ], 25 | "types": [ 26 | "jasmine" 27 | ] 28 | }, 29 | "exclude": [ 30 | "src", 31 | "node_modules", 32 | "platforms" 33 | ], 34 | "compileOnSave": false 35 | } 36 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "modules", 3 | "out": "doc", 4 | "theme": "default", 5 | "ignoreCompilerErrors": "true", 6 | "experimentalDecorators": "true", 7 | "emitDecoratorMetadata": "true", 8 | "target": "ES5", 9 | "moduleResolution": "node", 10 | "preserveConstEnums": "true", 11 | "stripInternal": "true", 12 | "suppressExcessPropertyErrors": "true", 13 | "suppressImplicitAnyIndexErrors": "true", 14 | "module": "commonjs" 15 | } --------------------------------------------------------------------------------