2 |
8 |
9 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types": [
8 | "jasmine",
9 | "jasminewd2",
10 | "node"
11 | ]
12 | }
13 | }
--------------------------------------------------------------------------------
/src/lib/src/inst_editor.ng.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/test/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "module": "commonjs",
6 | "types": [
7 | "jasmine",
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "test.ts",
13 | "polyfills.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/src/demo-app/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "app",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "app",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es5",
12 | "downlevelIteration": true,
13 | "typeRoots": [
14 | "node_modules/@types"
15 | ],
16 | "lib": [
17 | "es2017",
18 | "dom"
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/demo-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DynamicForm
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | export * from './public_api';
18 |
--------------------------------------------------------------------------------
/src/lib/public_api.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | export * from './src/index';
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dynamic Form
2 |
3 | ## How to build
4 |
5 | 1. Check it out from GitHub.
6 | * There is no reason to fork it.
7 | 2. Create a new local repository and copy the files from this repo into it.
8 | 3. Download npm dependencies.
9 |
10 | ``` shell
11 | git clone https://github.com/google/dynamic-form.git
12 | cd dynamic-form
13 | npm install
14 | ```
15 |
16 | 4. Build the library.
17 |
18 | ``` shell
19 | npm run build:lib
20 | ```
21 |
22 | 5. If you want to run the demo-app
23 |
24 | ``` shell
25 | npm start
26 | ```
27 |
28 | 6. If you want to run test cases.
29 |
30 | ``` shell
31 | npm run test
32 | ```
33 |
34 | 7. If you want to run e2e test cases.
35 |
36 | ``` shell
37 | npm run e2e
38 | ```
39 |
--------------------------------------------------------------------------------
/src/lib/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | export * from './dynamic_form_module';
19 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | export const environment = {
18 | production: true
19 | };
20 |
--------------------------------------------------------------------------------
/src/demo-app/app/app.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component} from '@angular/core';
18 |
19 | @Component({
20 | preserveWhitespaces: true,
21 | selector: 'my-app',
22 | templateUrl: './app.ng.html',
23 | })
24 | export class AppComponent {
25 | }
26 |
--------------------------------------------------------------------------------
/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import {browser, by, element} from 'protractor';
17 |
18 | export class AppPage {
19 | navigateTo() {
20 | return browser.get('/');
21 | }
22 |
23 | getParagraphText() {
24 | return element(by.css('my-app h1')).getText();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import {AppPage} from './app.po';
17 |
18 | describe('workspace-project App', () => {
19 | let page: AppPage;
20 |
21 | beforeEach(() => {
22 | page = new AppPage();
23 | });
24 |
25 | it('should display welcome message', () => {
26 | page.navigateTo();
27 | expect(page.getParagraphText()).toEqual('Examples for Dynamic Form Field');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/demo-app/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {enableProdMode} from '@angular/core';
18 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
19 |
20 | import {environment} from '../environments/environment';
21 |
22 | import {AppModule} from './app/app.module';
23 |
24 | if (environment.production) {
25 | enableProdMode();
26 | }
27 |
28 | platformBrowserDynamic().bootstrapModule(AppModule).catch(
29 | err => console.log(err));
30 |
--------------------------------------------------------------------------------
/src/lib/src/notes.md:
--------------------------------------------------------------------------------
1 | #EntityMetaDataRespository
2 | Containing the meta data for all Entities used by frontend web UI
3 |
4 |
5 |
6 | # Value Representation
7 | A value has 3 type representation
8 | + UI Value: Used by UI and FormControl. For example, LookupValue is represented as a Lookup Object from Lookup Src
9 | + Prop Value: Used by entity.prop. This value is a typed, object style value. But it may be different from UI object
10 | + string value: used to specified default value and entity context specification
11 |
12 |
13 | #TODO
14 | + support an array of value for
15 | + support multi select
16 | + add namespace support to separate the name to avoid entity name conflict
17 | + currency format in view
18 |
19 |
20 |
21 |
22 |
23 |
24 | #property editor (DynamicFieldComponent)
25 | + option to show placeholder mode (auto, float, always) or no (for list layout)
26 | + angular material date picker for date
27 |
28 |
29 | #TODO big item: new component
30 | + UI to define property
31 | + UI to manage lookup and values
32 | + generate the data model from Discovery BUILD RULE and Discovery URL
33 |
34 |
--------------------------------------------------------------------------------
/src/lib/tsconfig-build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "declaration": true,
5 | "stripInternal": false,
6 | "experimentalDecorators": true,
7 | "noUnusedParameters": false,
8 | "strictNullChecks": false,
9 | "importHelpers": true,
10 | "newLine": "lf",
11 | "module": "es2015",
12 | "moduleResolution": "node",
13 | "outDir": "../dist/packages/dynamic-form",
14 | "rootDir": ".",
15 | "sourceMap": true,
16 | "inlineSources": true,
17 | "target": "es2015",
18 | "lib": ["es2015", "dom"],
19 | "skipLibCheck": true,
20 | "types": [],
21 | "baseUrl": ".",
22 | "paths": {
23 | "dynamic-form": ["../dist/packages/dynamic-form"]
24 | },
25 | "typeRoots": [
26 | "./node_modules/@types/"
27 | ]
28 | },
29 | "files": [
30 | "public_api.ts"
31 | ],
32 | "angularCompilerOptions": {
33 | "annotateForClosureCompiler": true,
34 | "strictMetadataEmit": true,
35 | "flatModuleOutFile": "index.js",
36 | "flatModuleId": "dynamic-form",
37 | "skipTemplateCodegen": true
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/demo-app/app/book/book.ng.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | This is the page for the Book example.
4 |
This example demonstrates the basic usage for the Dynamic Form Field library.
5 |
6 |
7 |
description field has "fieldWidth:2"
8 |
9 |
10 | vertical layout with "vertical" css class
11 |
12 |
13 |
14 |
31 |
--------------------------------------------------------------------------------
/src/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dynamic-form",
3 | "version": "0.1.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "url"
7 | },
8 | "keywords": [
9 | "angular"
10 | ],
11 | "license": "Apache 2",
12 | "main": "./bundles/dynamic-form.umd.js",
13 | "es2015": "./esm2015/dynamic-form.js",
14 | "module": "./esm5/dynamic-form.es5.js",
15 | "typings": "./dynamic-form.d.ts",
16 | "peerDependencies": {
17 | "@angular/animations": "^6.0.0",
18 | "@angular/cdk": "^6.0.1",
19 | "@angular/common": "^6.0.0",
20 | "@angular/compiler": "^6.0.0",
21 | "@angular/core": "^6.0.0",
22 | "@angular/forms": "^6.0.0",
23 | "@angular/http": "^6.0.0",
24 | "@angular/material": "^6.0.1",
25 | "@angular/platform-browser": "^6.0.0",
26 | "@angular/platform-browser-dynamic": "^6.0.0",
27 | "@angular/router": "^6.0.0",
28 | "core-js": "^2.5.4",
29 | "hammerjs": "^2.0.8",
30 | "moment": "^2.22.1",
31 | "rxjs": "^6.0.0",
32 | "zone.js": "^0.8.26"
33 | },
34 | "ngPackage": {
35 | "lib": {
36 | "entryFile": "index.ts"
37 | },
38 | "dest": "../../dist/dynamic-form"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/demo-app/material-theme.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | @import '~@angular/material/theming';
18 |
19 | // Angular Material theme definition.
20 |
21 | // Include non-theme styles for core.
22 | @include mat-core();
23 |
24 | $primary: mat-palette($mat-deep-purple);
25 | $accent: mat-palette($mat-pink, A200, A100, A400);
26 | $warn: mat-palette($mat-red);
27 |
28 | $theme: mat-light-theme($primary, $accent, $warn);
29 |
30 | // Include all theme-styles for the components based on the current theme.
31 | @include angular-material-theme($theme);
32 |
33 | @import '../lib/src/style.scss';
34 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Dynamic Form
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/src/demo-app/app/app_routing.module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {NgModule} from '@angular/core';
18 | import {RouterModule, Routes} from '@angular/router';
19 |
20 | import {BookComponent} from './book/book';
21 | import {BookLookupComponent} from './book_lookup/book_lookup';
22 |
23 | const routes: Routes = [
24 | {path: '', redirectTo: '/book', pathMatch: 'full'},
25 | {path: 'book', component: BookComponent},
26 | {path: 'book_lookup', component: BookLookupComponent},
27 | ];
28 |
29 | @NgModule({imports: [RouterModule.forRoot(routes)], exports: [RouterModule]})
30 | export class AppRoutingModule {
31 | }
32 |
--------------------------------------------------------------------------------
/test/material-theme.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | @import 'angular2_material/src/lib/core/theming/theming';
18 | @import 'angular2_material/src/lib/core/theming/palette';
19 | @import 'angular2_material/src/lib/core/theming/all-theme';
20 |
21 | // Angular Material theme definition.
22 |
23 | // Include non-theme styles for core.
24 | @include mat-core();
25 |
26 | $primary: mat-palette($mat-indigo);
27 | $accent: mat-palette($mat-pink, A200, A100, A400);
28 | $warn: mat-palette($mat-red);
29 |
30 | $theme: mat-light-theme($primary, $accent, $warn);
31 |
32 | // Include all theme-styles for the components based on the current theme.
33 | @include angular-material-theme($theme);
34 |
--------------------------------------------------------------------------------
/test/test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // This file is required by karma.conf.js and loads recursively all the .spec
18 | // and framework files
19 |
20 | import 'zone.js/dist/zone-testing';
21 | import {getTestBed} from '@angular/core/testing';
22 | import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
23 |
24 | declare const require: any;
25 |
26 | // First, initialize the Angular testing environment.
27 | getTestBed().initTestEnvironment(
28 | BrowserDynamicTestingModule, platformBrowserDynamicTesting());
29 | // Then we find all the tests.
30 | const context = require.context('./', true, /\.spec\.ts$/);
31 | // And load the modules.
32 | context.keys().map(context);
33 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // This file can be replaced during build by using the `fileReplacements` array.
18 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
19 | // The list of file replacements can be found in `angular.json`.
20 |
21 | export const environment = {
22 | production: false
23 | };
24 |
25 | /*
26 | * In development mode, to ignore zone related error stack frames such as
27 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can
28 | * import the following file, but please comment it out in production mode
29 | * because it will have performance impact when throw error
30 | */
31 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
32 |
--------------------------------------------------------------------------------
/src/lib/src/prop_viewer.ng.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{prop.label}}:
6 |
7 |
8 |
9 | {{value | date:format}}
10 |
11 |
12 | {{value | date:format}}
13 |
14 |
15 | {{value | number:format}}
16 |
17 |
18 | {{value}}
19 |
20 |
21 |
22 |
23 |
24 |
25 | {{prop.label}}:
26 |
27 |
28 | {{value?.description}}
29 |
30 |
31 |
32 |
33 |
34 | {{prop.label}}:
35 |
36 |
37 | {{value?.description}}
38 |
39 |
40 |
41 |
42 |
43 | {{prop.label}}:
44 |
45 |
46 | {{value?'Yes':'No'}}
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // Protractor configuration file, see link for more information
18 | // https://github.com/angular/protractor/blob/master/lib/config.ts
19 |
20 | const { SpecReporter } = require('jasmine-spec-reporter');
21 |
22 | exports.config = {
23 | allScriptsTimeout: 11000,
24 | specs: [
25 | './src/**/*.e2e-spec.ts'
26 | ],
27 | capabilities: {
28 | 'browserName': 'chrome'
29 | },
30 | directConnect: true,
31 | baseUrl: 'http://localhost:4200/',
32 | framework: 'jasmine',
33 | jasmineNodeOpts: {
34 | showColors: true,
35 | defaultTimeoutInterval: 30000,
36 | print: function() {}
37 | },
38 | onPrepare() {
39 | require('ts-node').register({
40 | project: require('path').join(__dirname, './tsconfig.e2e.json')
41 | });
42 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/src/demo-app/app/book_lookup/book_lookup.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | inst-viewer {
18 | background-color: lightblue;
19 | margin: 5px;
20 | }
21 | inst-viewer::ng-deep gdf-prop-viewer {
22 | min-width: 100px;
23 | margin: 5px 10px 5px 0px;
24 | }
25 |
26 | span.propname {
27 | min-width:100px;
28 | margin: 5px 10px 5px 0px;
29 |
30 | }
31 | inst-viewer::ng-deep gdf-prop-viewer.description, span.propname.description {
32 | width: 300px;
33 | text-overflow: ellipsis;
34 | overflow: hidden;
35 | }
36 |
37 | div.book {
38 | display: flex;
39 | }
40 | div.book.head {
41 | margin: 5px;
42 | }
43 |
44 | th, td {
45 | border: 1px solid black;
46 | }
47 | th, td {
48 | padding: 0px 5px 0px 5px;
49 | }
50 |
51 |
52 | section.bookgrid {
53 | display: grid;
54 | grid-template-columns: 1fr 2fr repeat(6, 1fr);
55 | }
56 | section.bookgrid > div {
57 | border: 2px solid #ffa94d;
58 | }
59 | section.bookgrid > div.propname {
60 | font-weight: bold;
61 | }
62 |
--------------------------------------------------------------------------------
/src/demo-app/app/book/book_sample.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /**
18 | * This is the sample definition of Book class.
19 | * In production, this is usually defined by back-end services.
20 | */
21 | export class Book {
22 | name: string;
23 | description: string;
24 | author: string;
25 | price: Price;
26 | isAvailable: boolean;
27 |
28 | constructor(
29 | name?: string, description?: string, author?: string, price?: Price,
30 | isAvailabe?: boolean) {
31 | this.name = name ? name : '';
32 | this.description = description ? description : '';
33 | this.author = author ? author : '';
34 | this.price = price ? price : {amount: 0.00, currency: 'USD'};
35 | this.isAvailable = isAvailabe ? isAvailabe : false;
36 | }
37 | }
38 |
39 | /**
40 | * Here we have a separate interface Price to demonstrate a little complex
41 | * scenario, where the structure of entity and instance are not the same.
42 | */
43 | export interface Price {
44 | amount: number;
45 | currency: string;
46 | }
47 |
--------------------------------------------------------------------------------
/src/demo-app/app/app.module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import 'hammerjs';
18 |
19 | import {APP_BASE_HREF} from '@angular/common';
20 | import {NgModule} from '@angular/core';
21 | import {FormsModule} from '@angular/forms';
22 | import {MatButtonModule} from '@angular/material';
23 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
24 |
25 | import {DynamicFormModule} from '../../lib/src/dynamic_form_module';
26 |
27 | import {AppComponent} from './app.component';
28 | import {AppRoutingModule} from './app_routing.module';
29 | import {BookModule} from './book/book.module';
30 | import {BookLookupModule} from './book_lookup/book_lookup.module';
31 |
32 | @NgModule({
33 | imports: [
34 | DynamicFormModule,
35 | MatButtonModule,
36 | FormsModule,
37 | AppRoutingModule,
38 | BookModule,
39 | BookLookupModule,
40 | BrowserAnimationsModule,
41 | ],
42 | providers: [{provide: APP_BASE_HREF, useValue: '/'}],
43 | declarations: [
44 | AppComponent,
45 | ],
46 | bootstrap: [
47 | AppComponent,
48 | ]
49 | })
50 | export class AppModule {
51 | }
52 |
--------------------------------------------------------------------------------
/test/karma.conf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // Karma configuration file, see link for more information
18 | // https://karma-runner.github.io/1.0/config/configuration-file.html
19 |
20 | module.exports = function (config) {
21 | config.set({
22 | basePath: '',
23 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
24 | plugins: [
25 | require('karma-jasmine'),
26 | require('karma-chrome-launcher'),
27 | require('karma-jasmine-html-reporter'),
28 | require('karma-coverage-istanbul-reporter'),
29 | require('@angular-devkit/build-angular/plugins/karma')
30 | ],
31 | client: {
32 | clearContext: false // leave Jasmine Spec Runner output visible in browser
33 | },
34 | coverageIstanbulReporter: {
35 | dir: require('path').join(__dirname, '../coverage'),
36 | reports: ['html', 'lcovonly'],
37 | fixWebpackSourcePaths: true
38 | },
39 | reporters: ['progress', 'kjhtml'],
40 | port: 9876,
41 | colors: true,
42 | logLevel: config.LOG_INFO,
43 | autoWatch: true,
44 | browsers: ['Chrome'],
45 | singleRun: false
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/src/demo-app/app/book_lookup/book_lookup_sample.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /**
18 | * This is the sample definition of BookLookup class.
19 | * In production, this is usually defined by back-end services.
20 | */
21 | export class BookLookup {
22 | name: string;
23 | description: string;
24 | author: string;
25 | price: Price;
26 | isAvailable: boolean;
27 | // Add "country" to demonstrate Lookup usage.
28 | country: string;
29 |
30 |
31 | constructor(
32 | name?: string, description?: string, author?: string, price?: Price,
33 | isAvailabe?: boolean, country?: string) {
34 | this.name = name ? name : '';
35 | this.description = description ? description : '';
36 | this.author = author ? author : '';
37 | this.price = price ? price : {amount: 0.00, currency: 'USD'};
38 | this.isAvailable = isAvailabe ? isAvailabe : false;
39 | this.country = country ? country : 'US';
40 | }
41 | }
42 |
43 | /**
44 | * Here we have a separate interface Price to demonstrate a little complex
45 | * scenario, where the structure of entity and instance are not the same.
46 | */
47 | export interface Price {
48 | amount: number;
49 | currency: string;
50 | }
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-dynamic-form",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e",
11 | "build:lib": "ng-packagr -p src/lib/package.json",
12 | "version": "sync-json -v --property version --source package.json src/lib/package.json"
13 | },
14 | "private": false,
15 | "dependencies": {
16 | "@angular/animations": "^6.0.0",
17 | "@angular/cdk": "^6.0.1",
18 | "@angular/common": "^6.0.0",
19 | "@angular/compiler": "^6.0.0",
20 | "@angular/core": "^6.0.0",
21 | "@angular/forms": "^6.0.0",
22 | "@angular/http": "^6.0.0",
23 | "@angular/material": "^6.0.1",
24 | "@angular/platform-browser": "^6.0.0",
25 | "@angular/platform-browser-dynamic": "^6.0.0",
26 | "@angular/router": "^6.0.0",
27 | "core-js": "^2.5.4",
28 | "hammerjs": "^2.0.8",
29 | "moment": "^2.22.1",
30 | "rxjs": "^6.0.0",
31 | "zone.js": "^0.8.26"
32 | },
33 | "devDependencies": {
34 | "@angular/compiler-cli": "^6.0.0",
35 | "@angular-devkit/build-angular": "~0.6.0",
36 | "typescript": "~2.7.2",
37 | "@angular/cli": "~6.0.0",
38 | "@angular/language-service": "^6.0.0",
39 | "@types/jasmine": "~2.8.6",
40 | "@types/jasminewd2": "~2.0.3",
41 | "@types/node": "~8.9.4",
42 | "codelyzer": "~4.2.1",
43 | "jasmine-core": "~2.99.1",
44 | "jasmine-spec-reporter": "~4.2.1",
45 | "karma": "~1.7.1",
46 | "karma-chrome-launcher": "~2.2.0",
47 | "karma-coverage-istanbul-reporter": "~1.4.2",
48 | "karma-jasmine": "~1.1.1",
49 | "karma-jasmine-html-reporter": "^0.2.2",
50 | "protractor": "~5.3.0",
51 | "ts-node": "~5.0.1",
52 | "tslint": "~5.9.1",
53 | "ng-packagr": "^2.4.4"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/demo-app/app/book/book.module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {NgModule} from '@angular/core';
18 | import {FormsModule} from '@angular/forms';
19 | import {MatButtonModule} from '@angular/material';
20 | import {RouterModule} from '@angular/router';
21 | import {DynamicFormModule} from '../../../lib/src/dynamic_form_module';
22 | import {PropValueSetterGetters} from '../../../lib/src/inst_service';
23 | import {Entity} from '../../../lib/src/meta_datamodel';
24 | import {EntityMetaDataRepository} from '../../../lib/src/repositories';
25 |
26 | import {BookComponent} from './book';
27 | import {BOOK_ENTITY, BookValueSetterGetter} from './book_metadata';
28 |
29 | @NgModule({
30 | imports: [
31 | DynamicFormModule,
32 | MatButtonModule,
33 | FormsModule,
34 | RouterModule,
35 | ],
36 | providers: [
37 | BookValueSetterGetter,
38 | ],
39 | declarations: [
40 | BookComponent,
41 | ],
42 | bootstrap: [
43 | BookComponent,
44 | ]
45 | })
46 | export class BookModule {
47 | constructor(
48 | entityMetaDataRepository: EntityMetaDataRepository,
49 | propValueSetterGetters: PropValueSetterGetters,
50 | bookValueSetterGetter: BookValueSetterGetter,
51 | ) {
52 | // Register Book entity to the entity repository.
53 | // You only need to do this ONCE in your application.
54 | entityMetaDataRepository.registerMetaData(
55 | new Entity(BOOK_ENTITY.name, BOOK_ENTITY.props, BOOK_ENTITY.contexts));
56 | // Register Book entity setter and getter to repository.
57 | // You only need to do this ONCE in your application.
58 | propValueSetterGetters.registerSetterGetter(bookValueSetterGetter);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/lib/src/inst_viewer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | import {Component, Input, OnInit} from '@angular/core';
19 | import {Entity, Prop} from './meta_datamodel';
20 | import {EntityMetaDataRepository} from './repositories';
21 |
22 | /**
23 | * Viewer component for an instance of an Entity
24 | */
25 | @Component({
26 | preserveWhitespaces: true,
27 | selector: 'inst-viewer',
28 | templateUrl: 'inst_viewer.ng.html',
29 | })
30 | export class InstViewerComponent implements OnInit {
31 | /**
32 | * The name of the entity to be editted.
33 | */
34 | @Input() entityName: string;
35 |
36 | /**
37 | * Instance of Entity to be editted
38 | */
39 | @Input() inst: {};
40 |
41 | /**
42 | * A list of properties to be edited. It is optional.
43 | *
44 | * If no value is supplied, all edittabled propperties will be edited.
45 | */
46 | @Input() propNames: string[];
47 |
48 |
49 | /**
50 | * Show label for properties
51 | */
52 | @Input() showLabel = true;
53 |
54 | /**
55 | * Calculated Entity
56 | */
57 | entity: Entity;
58 |
59 | /**
60 | * Calculated Properties to be edited
61 | */
62 | props: Prop[];
63 |
64 | constructor(private readonly entityMetaDataRepository:
65 | EntityMetaDataRepository) {}
66 |
67 | ngOnInit() {
68 | this.entity = this.entityMetaDataRepository.getEntity(this.entityName);
69 |
70 | this.props = [];
71 | if (this.propNames && this.propNames.length > 0) {
72 | for (const propName of this.propNames) {
73 | this.props.push(this.entity.findProp(propName));
74 | }
75 | } else {
76 | for (const prop of this.entity.props) {
77 | if (prop.showInViewer) {
78 | this.props.push(prop);
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/demo-app/app/book_lookup/book_lookup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, OnInit, QueryList, ViewChildren} from '@angular/core';
18 | import {DefaultInstPopulater} from '../../../lib/src/inst_service';
19 | import {Entity} from '../../../lib/src/meta_datamodel';
20 | import {EntityMetaDataRepository} from '../../../lib/src/repositories';
21 |
22 | import {BOOK_LOOKUP_ENTITY_NAME} from './book_lookup_metadata';
23 | import {BookLookup} from './book_lookup_sample';
24 |
25 | /**
26 | * This is the Book example component.
27 | * Contains logic to display and update a Book instance.
28 | */
29 | @Component({
30 | selector: 'book_lookup',
31 | templateUrl: './book_lookup.ng.html',
32 | styleUrls: ['./book_lookup.css']
33 | })
34 | export class BookLookupComponent {
35 | /** BookLookup instance to be edited. */
36 | inst: BookLookup;
37 |
38 | /**
39 | * BookLookup entity properties.
40 | * These need to match each property name defined in the entity definition
41 | * (./book_lookup_metadata.ts).
42 | */
43 | props = [
44 | 'name', 'description', 'author', 'amount', 'currency', 'isAvailable',
45 | 'country'
46 | ];
47 |
48 | insts: BookLookup[] = [];
49 | constructor(
50 | private readonly entityMetaDataRepository: EntityMetaDataRepository,
51 | private readonly defaultInstPopulater: DefaultInstPopulater) {
52 | for (let i = 0; i < 5; i++) {
53 | // name?: string, description?: string, author?: string, price?: Price,
54 | // isAvailabe?: boolean, country?: string
55 | this.insts.push(new BookLookup(
56 | 'name' + i,
57 | i + 'here are some long description. Very long description, Very long description ',
58 | 'James Bond', {amount: 30, currency: 'USD'},
59 | i % 2 === 0 ? true : false, 'US'));
60 | }
61 | this.inst = this.insts[0];
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/test/example_lookupsrc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {AnyType, BaseLookupValue, NULL_VALUE} from '../src/lib/src/meta_datamodel';
18 | import {assert} from '../src/lib/src/repositories';
19 | import {LookupSource} from '../src/lib/src/repositories';
20 |
21 | export class ExampleLookupValue extends BaseLookupValue {
22 | code: string;
23 | }
24 |
25 | /**
26 | * A lookup source for test purpose
27 | */
28 | export class ExampleLookupSrc implements LookupSource {
29 | static readonly NAME = 'example';
30 |
31 | private readonly currencies = new Array();
32 | private readonly countries = new Array();
33 |
34 | constructor() {
35 | this.currencies.push({code: 'USD', description: 'US Dollar'});
36 | this.currencies.push({code: 'CAD', description: 'CANDIAN Dollar'});
37 | this.currencies.push({code: 'CNY', description: 'Chinese Yuan'});
38 |
39 | this.countries.push({code: 'US', description: 'United States'});
40 | this.countries.push({code: 'CA', description: 'Canada'});
41 | this.countries.push({code: 'CN', description: 'China'});
42 | }
43 |
44 |
45 | lookupValueToPropValue(lookupValue: ExampleLookupValue): AnyType {
46 | if (!lookupValue) {
47 | return null;
48 | }
49 | return lookupValue.code;
50 | }
51 |
52 |
53 | propValueToLookupValue(lookupCode: string, value: AnyType):
54 | ExampleLookupValue {
55 | if (lookupCode === 'currencies') {
56 | return assert(this.currencies.find(v => v.code === value));
57 | } else {
58 | return assert(this.countries.find(v => v.code === value));
59 | }
60 | }
61 |
62 | lookupValueToString(lookupValue: ExampleLookupValue): string {
63 | if (!lookupValue) {
64 | return NULL_VALUE;
65 | }
66 | return lookupValue.code;
67 | }
68 |
69 | getLookupValues(lookupCode: string): ExampleLookupValue[] {
70 | if (lookupCode === 'currencies') {
71 | return this.currencies;
72 | } else {
73 | return this.countries;
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/demo-app/app/book/book.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, OnInit, QueryList, ViewChildren} from '@angular/core';
18 |
19 | import {DefaultInstPopulater} from '../../../lib/src/inst_service';
20 | import {Entity} from '../../../lib/src/meta_datamodel';
21 | import {DynamicFieldPropertyComponent} from '../../../lib/src/prop_component';
22 | import {EntityMetaDataRepository} from '../../../lib/src/repositories';
23 |
24 | import {BOOK_ENTITY_NAME} from './book_metadata';
25 | import {Book} from './book_sample';
26 |
27 | /**
28 | * This is the Book example component.
29 | * Contains logic to display and update a Book instance.
30 | */
31 | @Component({
32 | selector: 'book',
33 | templateUrl: './book.ng.html',
34 | })
35 | export class BookComponent {
36 | @ViewChildren(DynamicFieldPropertyComponent)
37 | bookPropComps: QueryList;
38 |
39 | /** Book instance to be edited. */
40 | inst: Book;
41 |
42 | /**
43 | * Book entity properties.
44 | * These need to match the entity definition (in ./book_metadata.ts).
45 | */
46 | props =
47 | ['name', 'description', 'author', 'amount', 'currency', 'isAvailable'];
48 |
49 | constructor(
50 | private readonly entityMetaDataRepository: EntityMetaDataRepository,
51 | private readonly defaultInstPopulater: DefaultInstPopulater) {}
52 |
53 | ngOnInit(): void {
54 | // Gets the registered Book entity from entity repository.
55 | const bookEntity =
56 | this.entityMetaDataRepository.getEntity(BOOK_ENTITY_NAME);
57 | // Creates a default Book instance.
58 | this.inst = this.createDefaultBook(bookEntity);
59 | }
60 |
61 | /**
62 | * Uses default instance populater to create a default Book instance.
63 | */
64 | private createDefaultBook(entity: Entity) {
65 | const book = new Book();
66 | this.defaultInstPopulater.populateInstance(book, entity);
67 | return book;
68 | }
69 |
70 | /**
71 | * Submits form control values and saves to instance.
72 | * User would see the value updated on the template.
73 | */
74 | submit() {
75 | for (const component of this.bookPropComps.toArray()) {
76 | component.pushValueToInstance();
77 | }
78 | alert('Instance value updated!');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/lib/src/style.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | // Define some convenient class which can be used by client
17 | $red: #a2180b;
18 | $prop-gap: 20px;
19 |
20 | // Copied from matrial source code
21 | $mat-form-field-default-infix-width: 180px !default;
22 |
23 | select {
24 | width: $mat-form-field-default-infix-width;
25 | }
26 |
27 | // ----------------------Property editor ----------------
28 | gdf-prop {
29 | display: inline-block;
30 | margin-right: $prop-gap;
31 |
32 | //control show and hide
33 | &.hide {
34 | display: none;
35 | }
36 |
37 | div.mat-form-field-label.required span.required {
38 | color: $red;
39 | }
40 |
41 | mat-form-field.prop {
42 |
43 | //for required property
44 | &.required mat-label span.required {
45 | color: $red;
46 | }
47 |
48 | //default width
49 | .mat-form-field-infix {
50 | width: $mat-form-field-default-infix-width;
51 | }
52 |
53 | //twice width
54 | &.field-width-2 .mat-form-field-infix {
55 | width: $mat-form-field-default-infix-width * 2 + $prop-gap;
56 | }
57 |
58 | //trip width
59 | &.field-width-3 .mat-form-field-infix {
60 | width: $mat-form-field-default-infix-width * 3 + $prop-gap * 2;
61 | }
62 | }
63 | //control with for toggle
64 | div.mat-form-field-label.prop {
65 | display: block;
66 | position: static;
67 | width: $mat-form-field-default-infix-width;
68 | }
69 | }
70 |
71 | // property editor is listed by default horizontally, we give a class
72 | //convenient class so that it can be listed vertically
73 | .vertical gdf-prop {
74 | display: block;
75 | }
76 |
77 | //----------------------- property viewer
78 | // Label is viewed with two rows: label and value
79 | .full > gdf-prop-viewer > span.prop {
80 | display: flex;
81 | flex-direction: column;
82 | align-items: flex-start;
83 | }
84 |
85 | gdf-prop-viewer {
86 | display: block;
87 | margin-right: $prop-gap;
88 | }
89 |
90 | //-- property viewer is layed out horizontally
91 | .horizontal {
92 | display: flex;
93 | flex-wrap: wrap;
94 | }
95 |
96 | .horizontal gdf-prop-viewer {
97 | display: block;
98 | }
99 |
100 | .sr-only {
101 | position: absolute;
102 | left: -10000px;
103 | top: auto;
104 | width: 1px;
105 | height: 1px;
106 | overflow: hidden;
107 | }
108 |
109 | .mat-form-field-required-marker {
110 | display: none;
111 | }
112 |
113 | .mat-placeholder-required {
114 | display: none;
115 | }
116 |
--------------------------------------------------------------------------------
/test/prop_by_name.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component} from '@angular/core';
18 | import {ComponentFixture, TestBed} from '@angular/core/testing';
19 | import {By} from '@angular/platform-browser';
20 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
21 |
22 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
23 | import {Entity, Prop} from '../src/lib/src/meta_datamodel';
24 | import {EntityMetaDataRepository} from '../src/lib/src/repositories';
25 |
26 | /**
27 | * Host component to test prop_component.ts
28 | */
29 | @Component({
30 | template: `
31 |
36 | `
37 | })
38 | export class TestHostComponent {
39 | // tslint:disable-next-line:no-any can be any, test only
40 | inst: {[index: string]: any};
41 | constructor() {}
42 | }
43 |
44 | describe('BooleanInput', () => {
45 | let comp: TestHostComponent;
46 | let fixture: ComponentFixture;
47 |
48 | const entity = new Entity('test', [new Prop({
49 | name: 'prop1',
50 | type: 'text',
51 | controlType: 'text',
52 | dataType: 'STRING',
53 | label: 'first Property',
54 | })]);
55 |
56 | // configure
57 | beforeEach(() => {
58 | TestBed.configureTestingModule({
59 | imports: [DynamicFormModule, NoopAnimationsModule],
60 | declarations: [TestHostComponent],
61 | });
62 |
63 | // initialize meta data
64 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
65 | fixture = TestBed.createComponent(TestHostComponent);
66 |
67 | comp = fixture.componentInstance;
68 | comp.inst = {prop1: 'property 1'};
69 | fixture.detectChanges();
70 | });
71 |
72 |
73 | it('propEditor', () => {
74 | // query for the title
by CSS element selector
75 | const de = fixture.debugElement.query(By.css('gdf-prop input'));
76 | expect(de).not.toBeNull();
77 | const el = de.nativeElement as HTMLInputElement;
78 | expect(el.value).toEqual('property 1');
79 | });
80 |
81 | it('propViewer', () => {
82 | // query for the title
by CSS element selector
83 | const de = fixture.debugElement.query(By.css('gdf-prop-viewer span.value'));
84 | expect(de).not.toBeNull();
85 | const el = de.nativeElement as HTMLElement;
86 | const textContent = el.textContent || '';
87 | expect(textContent.trim()).toEqual('property 1');
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-use-before-declare": true,
76 | "no-var-keyword": true,
77 | "object-literal-sort-keys": false,
78 | "one-line": [
79 | true,
80 | "check-open-brace",
81 | "check-catch",
82 | "check-else",
83 | "check-whitespace"
84 | ],
85 | "prefer-const": true,
86 | "quotemark": [
87 | true,
88 | "single"
89 | ],
90 | "radix": true,
91 | "semicolon": [
92 | true,
93 | "always"
94 | ],
95 | "triple-equals": [
96 | true,
97 | "allow-null-check"
98 | ],
99 | "typedef-whitespace": [
100 | true,
101 | {
102 | "call-signature": "nospace",
103 | "index-signature": "nospace",
104 | "parameter": "nospace",
105 | "property-declaration": "nospace",
106 | "variable-declaration": "nospace"
107 | }
108 | ],
109 | "unified-signatures": true,
110 | "variable-name": false,
111 | "whitespace": [
112 | true,
113 | "check-branch",
114 | "check-decl",
115 | "check-operator",
116 | "check-separator",
117 | "check-type"
118 | ],
119 | "no-output-on-prefix": true,
120 | "use-input-property-decorator": true,
121 | "use-output-property-decorator": true,
122 | "use-host-property-decorator": true,
123 | "no-input-rename": true,
124 | "no-output-rename": true,
125 | "use-life-cycle-interface": true,
126 | "use-pipe-transform-interface": true,
127 | "component-class-suffix": true,
128 | "directive-class-suffix": true
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/test/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Web Animations `@angular/platform-browser/animations`
51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
53 | **/
54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
55 |
56 | /**
57 | * By default, zone.js will patch all possible macroTask and DomEvents
58 | * user can disable parts of macroTask/DomEvents patch by setting following flags
59 | */
60 |
61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
64 |
65 | /*
66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
68 | */
69 | // (window as any).__Zone_enable_cross_context_check = true;
70 |
71 | /***************************************************************************************************
72 | * Zone JS is required by default for Angular itself.
73 | */
74 | import 'zone.js/dist/zone'; // Included with Angular CLI.
75 |
76 |
77 |
78 | /***************************************************************************************************
79 | * APPLICATION IMPORTS
80 | */
81 |
--------------------------------------------------------------------------------
/src/demo-app/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Web Animations `@angular/platform-browser/animations`
51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
53 | **/
54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
55 |
56 | /**
57 | * By default, zone.js will patch all possible macroTask and DomEvents
58 | * user can disable parts of macroTask/DomEvents patch by setting following flags
59 | */
60 |
61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
64 |
65 | /*
66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
68 | */
69 | // (window as any).__Zone_enable_cross_context_check = true;
70 |
71 | /***************************************************************************************************
72 | * Zone JS is required by default for Angular itself.
73 | */
74 | import 'zone.js/dist/zone'; // Included with Angular CLI.
75 |
76 |
77 |
78 | /***************************************************************************************************
79 | * APPLICATION IMPORTS
80 | */
81 |
--------------------------------------------------------------------------------
/test/prop_component_boolean.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {ComponentFixture, TestBed} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {MatSlideToggle} from '@angular/material';
21 | import {By} from '@angular/platform-browser';
22 |
23 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
24 | import {Entity, Prop} from '../src/lib/src/meta_datamodel';
25 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
26 | import {EntityMetaDataRepository} from '../src/lib/src/repositories';
27 |
28 | /**
29 | * Host component to test prop_component.ts
30 | */
31 | @Component({
32 | template: `
33 |
by CSS element selector
81 | const de = fixture.debugElement.query(By.css('mat-slide-toggle'));
82 | expect(de).not.toBeNull();
83 | const toggler = de.componentInstance as MatSlideToggle;
84 | expect(toggler.checked).toBeTruthy();
85 | });
86 |
87 | it('toggle', () => {
88 | // query for the title
by CSS element selector
89 | const de = fixture.debugElement.query(By.css('mat-slide-toggle'));
90 |
91 | const toggler = de.componentInstance as MatSlideToggle;
92 | fixture.debugElement.query(By.css('input')).nativeElement.click();
93 | expect(toggler.checked).not.toBeTruthy();
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/test/prop_component_valuechange.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {By} from '@angular/platform-browser';
21 |
22 |
23 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
24 | import {Entity, Prop} from '../src/lib/src/meta_datamodel';
25 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
26 | import {EntityMetaDataRepository} from '../src/lib/src/repositories';
27 |
28 |
29 | /**
30 | * Host component to test prop_component.ts
31 | */
32 | @Component({
33 | preserveWhitespaces: true,
34 | template: `
35 |
38 | `
39 | })
40 | export class TestHostComponent {
41 | prop: Prop;
42 | // tslint:disable-next-line:no-any property value can be anything
43 | inst: {[index: string]: any};
44 | @ViewChild(DynamicFieldPropertyComponent)
45 | propComp: DynamicFieldPropertyComponent;
46 |
47 | newvalue: string|undefined;
48 | valueChanged(value: string) {
49 | this.newvalue = value;
50 | }
51 |
52 | constructor() {}
53 | }
54 |
55 | describe('ValueChange', () => {
56 | let comp: TestHostComponent;
57 | let fixture: ComponentFixture;
58 |
59 | const entity = new Entity('test', [new Prop({
60 | name: 'prop1',
61 | type: 'text',
62 | controlType: 'number',
63 | dataType: 'NUMBER',
64 | label: 'first Property',
65 | })]);
66 |
67 | // configure
68 | beforeEach(() => {
69 | TestBed.configureTestingModule({
70 | imports: [DynamicFormModule, NoopAnimationsModule],
71 | declarations: [TestHostComponent],
72 | });
73 |
74 | // initialize meta data
75 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
76 |
77 | fixture = TestBed.createComponent(TestHostComponent);
78 | comp = fixture.componentInstance;
79 | comp.prop = entity.props[0];
80 | comp.inst = {prop1: 3};
81 | fixture.detectChanges();
82 | });
83 |
84 | it('value change is invoked', fakeAsync(() => {
85 | expect(comp.newvalue).toBeUndefined();
86 | setInputValue('input', '5');
87 | expect(comp.propComp.control.value).toEqual('5');
88 | expect(comp.newvalue).toEqual('5');
89 | }));
90 |
91 | function setInputValue(selector: string, value: string) {
92 | fixture.detectChanges();
93 | tick();
94 | const input = fixture.debugElement.query(By.css(selector)).nativeElement;
95 | input.value = value;
96 | input.dispatchEvent(new Event('input'));
97 | tick();
98 | }
99 | });
100 |
--------------------------------------------------------------------------------
/test/default_inst_populater.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {MatOption, MatSelect} from '@angular/material';
21 | import {By} from '@angular/platform-browser';
22 |
23 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
24 | import {DefaultInstPopulater} from '../src/lib/src/inst_service';
25 | import {DisableContext, Entity, NOTNULL_VALUE, NULL_VALUE, Prop, RequiredContext, RestrictLookupContext} from '../src/lib/src/meta_datamodel';
26 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
27 | import {EntityMetaDataRepository, LookupSources} from '../src/lib/src/repositories';
28 |
29 | import {ExampleLookupSrc, ExampleLookupValue} from './example_lookupsrc';
30 |
31 | describe('DefaultInstPopulater', () => {
32 | const entity = new Entity(
33 | 'test',
34 | [
35 | new Prop({
36 | name: 'prop1',
37 | type: 'select',
38 | controlType: 'text',
39 | dataType: 'STRING',
40 | label: 'first Property',
41 | lookupSrc: ExampleLookupSrc.NAME,
42 | lookupName: 'countries',
43 | defaultValue: 'US',
44 | }),
45 | new Prop({
46 | name: 'prop2',
47 | type: 'select',
48 | controlType: 'text',
49 | dataType: 'STRING',
50 | label: 'second Property',
51 | defaultValue: 'second',
52 | }),
53 | new Prop({
54 | name: 'prop3',
55 | type: 'checkbox',
56 | controlType: 'boolean',
57 | dataType: 'BOOLEAN',
58 | label: 'third Property',
59 | defaultValue: 'true',
60 | }),
61 | new Prop({
62 | name: 'prop4',
63 | type: 'text',
64 | controlType: 'number',
65 | dataType: 'NUMBER',
66 | label: 'fourth Property',
67 | defaultValue: '3',
68 | }),
69 | new Prop({
70 | name: 'prop5',
71 | type: 'checkbox',
72 | controlType: 'boolean',
73 | dataType: 'BOOLEAN',
74 | label: 'fifth Property',
75 | }),
76 | ],
77 | []);
78 |
79 | // configure
80 | beforeEach(() => {
81 | TestBed.configureTestingModule({imports: [DynamicFormModule, NoopAnimationsModule]});
82 |
83 | // initialize meta data
84 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
85 | TestBed.get(LookupSources)
86 | .registerLookupSource(ExampleLookupSrc.NAME, new ExampleLookupSrc());
87 | });
88 |
89 |
90 |
91 | it('test DefaultInstPopulater', () => {
92 | const defaultInstPouplater =
93 | TestBed.get(DefaultInstPopulater) as DefaultInstPopulater;
94 |
95 | const inst: {[index: string]: {}} = {};
96 | defaultInstPouplater.populateInstance(inst, entity);
97 | expect(inst.prop1).toBe('US');
98 | expect(inst.prop2).toEqual('second');
99 | expect(inst.prop3).toBeTruthy();
100 | expect(inst.prop5).toBeFalsy();
101 | expect(inst.prop4).toBe(3);
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/src/demo-app/app/book_lookup/book_lookup.ng.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | This is an example HTML page for Book (Lookup version) entity.
4 |
5 |
This example demonstrates the
6 | Lookup
7 | usage for the Dynamic Form Field library.
8 |
9 | Note: If you go back to the basic book example, you would notice that for property "Currency", it's just a free
10 | text input.
11 |
However, in real world, there are limited kinds of currency, for example USD, EUR, etc. In this case, you would need a drop
12 | down to list all kinds of currency. And here our Lookup comes into play.
13 |
Also, you gonna find out there is a new field "Country", which demonstrates the usage of Lookup as an enhancement.
14 |
15 |
16 |
17 |
18 |
19 |
20 | Five instances to to be show as table. This is preferred since it
21 | support accessbility well.
22 |
23 |
24 |
25 |
{{propName}}
26 |
Action
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Five instance to to be show css grid. May nto be good for accessbility
45 |
46 |
47 |
{{propName}}
48 |
Action
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
Five instance to to be show as list view
60 |
61 |
62 | {{propName}}
63 | Action
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
Single Instance with label
74 |
75 |
77 |
78 |
79 |
80 |
81 |
82 |
88 |
--------------------------------------------------------------------------------
/src/lib/src/inst_editor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | import {Component, Input, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
19 |
20 | import {EntityContextDirective} from './entity_directives';
21 | import {DefaultInstPopulater} from './inst_service';
22 | import {Entity, Prop} from './meta_datamodel';
23 | import {DynamicFieldPropertyComponent} from './prop_component';
24 | import {EntityMetaDataRepository} from './repositories';
25 |
26 | /**
27 | * Editor component for an instance of an Entity
28 | *
29 | *
30 | */
31 | @Component({
32 | preserveWhitespaces: true,
33 | selector: 'inst-editor',
34 | templateUrl: 'inst_editor.ng.html',
35 | })
36 | export class InstEditorComponent implements OnInit {
37 | /**
38 | * The name of the entity to be editted.
39 | */
40 | @Input() entityName: string;
41 |
42 | /**
43 | * Instance of Entity to be editted
44 | */
45 | private internalInst: {};
46 |
47 | /**
48 | * A list of properties to be edited. It is optional.
49 | *
50 | * If no value is supplied, all edittabled propperties will be edited.
51 | */
52 | @Input() propNames: string[];
53 |
54 | @ViewChildren(DynamicFieldPropertyComponent)
55 | propComps: QueryList;
56 |
57 | //TODO @ViewChild(EntityContextDirective, {static:false})
58 | @ViewChild(EntityContextDirective)
59 | entityContextDirective: EntityContextDirective;
60 |
61 | /**
62 | * Calculated Entity
63 | */
64 | entity: Entity;
65 |
66 | /**
67 | * Calculated Properties to be edited
68 | */
69 | props: Prop[];
70 |
71 | constructor(
72 | private readonly entityMetaDataRepository: EntityMetaDataRepository,
73 | private readonly defaultInstPopulater: DefaultInstPopulater) {}
74 |
75 | ngOnInit() {
76 | this.entity = this.entityMetaDataRepository.getEntity(this.entityName);
77 |
78 | if (!this.internalInst) {
79 | this.internalInst =
80 | this.defaultInstPopulater.populateInstance({}, this.entity);
81 | }
82 |
83 | this.props = [];
84 | if (this.propNames && this.propNames.length > 0) {
85 | for (const propName of this.propNames) {
86 | this.props.push(this.entity.findProp(propName));
87 | }
88 | } else {
89 | for (const prop of this.entity.props) {
90 | if (prop.showInEditor) {
91 | this.props.push(prop);
92 | }
93 | }
94 | }
95 | }
96 |
97 | /**
98 | * Instance of Entity to be editted
99 | */
100 | @Input()
101 | get inst() {
102 | return this.internalInst;
103 | }
104 | set inst(newInst: {}) {
105 | this.internalInst = newInst;
106 | this.reset();
107 | }
108 |
109 | /**
110 | * Collect values from UI and push it to Instance
111 | */
112 | pushValueToInstance() {
113 | for (const dfComp of this.propComps.toArray()) {
114 | dfComp.pushValueToInstance();
115 | }
116 | }
117 |
118 | /**
119 | * Reset UI values
120 | */
121 | reset() {
122 | setTimeout(() => {
123 | for (const dfComp of this.propComps.toArray()) {
124 | dfComp.resetValue();
125 | }
126 |
127 | /*
128 | Some context is enforced only at the very beginning of inst binding.
129 | We need to call the directive to enforce the value again.
130 | */
131 | this.entityContextDirective.reset();
132 | });
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "dynamic-form": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "prefix": "app",
11 | "schematics": {},
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/dynamic-form",
17 | "index": "src/demo-app/index.html",
18 | "main": "src/demo-app/main.ts",
19 | "polyfills": "src/demo-app/polyfills.ts",
20 | "tsConfig": "src/demo-app/tsconfig.app.json",
21 | "assets": [
22 | "src/demo-app/favicon.ico"
23 | ],
24 | "styles": [
25 | "src/demo-app/material-theme.scss",
26 | "src/lib/src/style.scss"
27 | ],
28 | "scripts": []
29 | },
30 | "configurations": {
31 | "production": {
32 | "fileReplacements": [
33 | {
34 | "replace": "src/environments/environment.ts",
35 | "with": "src/environments/environment.prod.ts"
36 | }
37 | ],
38 | "optimization": true,
39 | "outputHashing": "all",
40 | "sourceMap": false,
41 | "extractCss": true,
42 | "namedChunks": false,
43 | "aot": true,
44 | "extractLicenses": true,
45 | "vendorChunk": false,
46 | "buildOptimizer": true
47 | }
48 | }
49 | },
50 | "serve": {
51 | "builder": "@angular-devkit/build-angular:dev-server",
52 | "options": {
53 | "browserTarget": "dynamic-form:build"
54 | },
55 | "configurations": {
56 | "production": {
57 | "browserTarget": "dynamic-form:build:production"
58 | }
59 | }
60 | },
61 | "extract-i18n": {
62 | "builder": "@angular-devkit/build-angular:extract-i18n",
63 | "options": {
64 | "browserTarget": "dynamic-form:build"
65 | }
66 | },
67 | "test": {
68 | "builder": "@angular-devkit/build-angular:karma",
69 | "options": {
70 | "main": "test/test.ts",
71 | "polyfills": "test/polyfills.ts",
72 | "tsConfig": "test/tsconfig.spec.json",
73 | "karmaConfig": "test/karma.conf.js",
74 | "styles": [
75 | "src/demo-app/material-theme.scss",
76 | "src/lib/src/style.scss"
77 | ],
78 | "scripts": [],
79 | "assets": [
80 | "src/demo-app/favicon.ico"
81 | ]
82 | }
83 | },
84 | "lint": {
85 | "builder": "@angular-devkit/build-angular:tslint",
86 | "options": {
87 | "tsConfig": [
88 | "src/demo-app/tsconfig.app.json",
89 | "test/tsconfig.spec.json"
90 | ],
91 | "exclude": [
92 | "**/node_modules/**"
93 | ]
94 | }
95 | }
96 | }
97 | },
98 | "dynamic-form-e2e": {
99 | "root": "e2e/",
100 | "projectType": "application",
101 | "architect": {
102 | "e2e": {
103 | "builder": "@angular-devkit/build-angular:protractor",
104 | "options": {
105 | "protractorConfig": "e2e/protractor.conf.js",
106 | "devServerTarget": "dynamic-form:serve"
107 | }
108 | },
109 | "lint": {
110 | "builder": "@angular-devkit/build-angular:tslint",
111 | "options": {
112 | "tsConfig": "e2e/tsconfig.e2e.json",
113 | "exclude": [
114 | "**/node_modules/**"
115 | ]
116 | }
117 | }
118 | }
119 | }
120 | },
121 | "defaultProject": "dynamic-form"
122 | }
123 |
--------------------------------------------------------------------------------
/src/demo-app/app/book/book_metadata.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /**
18 | * Definition for Book entity.
19 | */
20 | import {Injectable} from '@angular/core';
21 |
22 | import {SimpleValueSetterGetter} from '../../../lib/src/inst_service';
23 | import {AnyType, Entity, Prop, ShowHideContext} from '../../../lib/src/meta_datamodel';
24 |
25 | import {Book} from './book_sample';
26 |
27 | export const BOOK_ENTITY_NAME = 'book';
28 |
29 | export const BOOK_ENTITY = {
30 | name: 'book',
31 | props: [
32 | new Prop({
33 | name: 'name',
34 | type: 'text',
35 | dataType: 'STRING',
36 | label: 'Book Name',
37 | minLength: 2,
38 | isRequired: true,
39 | editable: true,
40 | viewable: true,
41 | }),
42 | new Prop({
43 | name: 'description',
44 | type: 'text',
45 | dataType: 'STRING',
46 | label: 'Description',
47 | minLength: 2,
48 | isRequired: true,
49 | editable: true,
50 | viewable: true,
51 | fieldWidth: 2,
52 | }),
53 | new Prop({
54 | name: 'author',
55 | type: 'text',
56 | dataType: 'STRING',
57 | label: 'Author',
58 | minLength: 2,
59 | isRequired: true,
60 | editable: true,
61 | viewable: true,
62 | }),
63 | new Prop({
64 | name: 'amount',
65 | type: 'text',
66 | dataType: 'NUMBER',
67 | label: 'Price Amount',
68 | min: 0.00,
69 | isRequired: true,
70 | editable: true,
71 | viewable: true,
72 | }),
73 | new Prop({
74 | name: 'currency',
75 | type: 'text',
76 | dataType: 'STRING',
77 | label: 'Price Currency',
78 | minLength: 3,
79 | isRequired: true,
80 | editable: true,
81 | viewable: true,
82 | }),
83 | new Prop({
84 | name: 'isAvailable',
85 | type: 'checkbox',
86 | dataType: 'BOOLEAN',
87 | label: 'Available',
88 | }),
89 | ],
90 | contexts: [
91 | {
92 | srcProp: 'isAvailable',
93 | srcValue: 'true',
94 | target: 'price',
95 | show: true,
96 | type: ShowHideContext.TYPE,
97 | skipFirstTime: false,
98 | },
99 | ],
100 | };
101 |
102 | /**
103 | * Getter and setter for a Book instance.
104 | */
105 | @Injectable()
106 | export class BookValueSetterGetter extends SimpleValueSetterGetter {
107 | /**
108 | * Indicates whether can handle the passed entity or not.
109 | */
110 | canHandle(entity: Entity): boolean {
111 | return entity.name === BOOK_ENTITY_NAME;
112 | }
113 |
114 | /**
115 | * Gets the value from Book instance by Property.
116 | * This is just a simple implementation to get value from instance.
117 | * In real production, the scenario and implementation can be complex.
118 | */
119 | get(inst: Book, prop: Prop): AnyType {
120 | switch (prop.name) {
121 | // Handles property 'amount' and 'currency' differently, since they do not
122 | // match the structure of instance.
123 | case 'amount':
124 | return inst.price.amount;
125 | case 'currency':
126 | return inst.price.currency;
127 | default:
128 | // Uses the default getter for the rest properties.
129 | return super.get(inst, prop);
130 | }
131 | }
132 |
133 | /**
134 | * Sets value to instance.
135 | */
136 | set(inst: Book, prop: Prop, value: AnyType): void {
137 | if (!value) {
138 | return;
139 | }
140 |
141 | switch (prop.name) {
142 | case 'amount':
143 | const amount = Number(value);
144 | if (isNaN(amount)) {
145 | throw new Error('The price amount input is not valid!');
146 | }
147 | inst.price.amount = amount;
148 | break;
149 | case 'currency':
150 | inst.price.currency = value!.toString();
151 | break;
152 | default:
153 | super.set(inst, prop, value);
154 | break;
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/test/prop_component_num.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async as testasync, ComponentFixture, TestBed} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {By} from '@angular/platform-browser';
21 |
22 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
23 | import {Entity, Prop} from '../src/lib/src/meta_datamodel';
24 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
25 | import {EntityMetaDataRepository} from '../src/lib/src/repositories';
26 |
27 |
28 | /**
29 | * Host component to test prop_component.ts
30 | */
31 | @Component({
32 | preserveWhitespaces: true,
33 | template: `
34 |
39 | `
40 | })
41 | export class TestHostComponent {
42 | props: Prop[];
43 | // tslint:disable-next-line:no-any property value can be anything
44 | inst: {[index: string]: any};
45 | }
46 |
47 | describe('NumberInput', () => {
48 | let comp: TestHostComponent;
49 | let fixture: ComponentFixture;
50 | let de: DebugElement;
51 | let el: HTMLElement;
52 |
53 | const entity = new Entity('test', [
54 | new Prop({
55 | name: 'prop1',
56 | type: 'text',
57 | controlType: 'number',
58 | dataType: 'NUMBER',
59 | label: 'first Property',
60 | min: 3,
61 | max: 10
62 | }),
63 | ]);
64 |
65 |
66 | // configure
67 | beforeEach(() => {
68 | TestBed.configureTestingModule({
69 | imports: [DynamicFormModule, NoopAnimationsModule],
70 | declarations: [TestHostComponent],
71 | });
72 |
73 | // initialize meta data
74 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
75 |
76 | fixture = TestBed.createComponent(TestHostComponent);
77 | comp = fixture.componentInstance;
78 | comp.props = entity.props;
79 | comp.inst = {prop1: 3};
80 | fixture.detectChanges();
81 | });
82 |
83 |
84 | it('value is shown', () => {
85 | de = fixture.debugElement.query(By.css('.prop1 input'));
86 | el = de.nativeElement;
87 | expect((el as HTMLInputElement).value)
88 | .toEqual(String(comp.inst[entity.props[0].name]));
89 | });
90 |
91 | it('value is pushed to control', async () => {
92 | setInputValue('.prop1 input', '5');
93 | await fixture.whenStable();
94 | const propComp =
95 | fixture.debugElement.query(By.css('gdf-prop')).componentInstance as
96 | DynamicFieldPropertyComponent;
97 | expect(propComp.control.value).toEqual('5');
98 | });
99 |
100 | it('min', async () => {
101 | setInputValue('.prop1 input', '2');
102 | await fixture.whenStable();
103 | fixture.detectChanges();
104 | comp =
105 | fixture.debugElement.query(By.css('gdf-prop.prop1')).componentInstance;
106 | de = fixture.debugElement.query(By.css('gdf-prop.prop1 mat-error'));
107 | expect(de).not.toBeNull();
108 | el = de.nativeElement as HTMLElement;
109 | expect(el.textContent).toContain('minimal allowed value: 3');
110 | });
111 |
112 | it('max', testasync(async () => {
113 | setInputValue('.prop1 input', '12');
114 | await fixture.whenStable();
115 | fixture.detectChanges();
116 |
117 | de = fixture.debugElement.query(By.css('gdf-prop.prop1 mat-error'));
118 | expect(de).not.toBeNull();
119 | el = de.nativeElement as HTMLElement;
120 | expect(el.textContent).toContain('maximal allowed value: 10');
121 | }));
122 |
123 | function setInputValue(selector: string, value: string) {
124 | const input = fixture.debugElement.query(By.css(selector)).nativeElement;
125 | input.value = value;
126 | input.dispatchEvent(new Event('input'));
127 | }
128 | });
129 |
--------------------------------------------------------------------------------
/src/lib/src/prop_component.ng.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 | {{prop.label}}
8 | Required
9 |
10 | *
11 |
12 |
13 | {{prop.regExpErrorMsg?prop.regExpErrorMsg:"Value must match pattern: "+prop.regExp}}
14 | {{prop.name}} is a required field
15 | minimal allowed value: {{prop.min}}
16 | maximal allowed value: {{prop.max}}
17 | Must have at least {{prop.minLength}} characters
18 | Can have {{prop.maxLength}} characters at maximum
19 | Email is not valud
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{prop.label}}
27 | Required
28 |
29 | *
30 |
31 |
32 |
33 | {{ lookup.description }}
34 |
35 |
36 |
37 | {{prop.name}} is a required field
38 |
39 |
40 | Please select a value from dropdown
41 |
42 |
43 |
44 |
45 |
46 |
47 |
69 |
70 |
75 |
76 |
91 |
92 | {{prop.name}} is a required field
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/src/lib/src/entity_directives.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | import {AfterContentInit, ContentChildren, Directive, Input, OnDestroy, OnInit, QueryList} from '@angular/core';
19 | import {Subscription} from 'rxjs';
20 |
21 | import {ContextProcessors, DefaultValueConverter, PropValueSetterGetters} from './inst_service';
22 | import {AnyType, Entity, Prop} from './meta_datamodel';
23 | import {DynamicFieldPropertyComponent} from './prop_component';
24 | import {EntityMetaDataRepository, LookupSources} from './repositories';
25 |
26 |
27 | /**
28 | * Establishes a context for editing.
29 | *
30 | * It does
31 | *
32 | * 1. controlInst: Map property name to property's FormControl's value
33 | * 2. Process All Entity.Contexts and set up actions accordingly
34 | */
35 | @Directive({
36 | selector: '[gdfEntityCtx]',
37 | exportAs: 'entityContextDirective',
38 | })
39 | export class EntityContextDirective implements OnInit, AfterContentInit,
40 | OnDestroy {
41 | /**
42 | * Entity Name or Entity;
43 | */
44 | @Input('gdfEntityCtx') name: string|Entity;
45 | /**
46 | * The inst to be edited
47 | */
48 | @Input('inst') inst: {};
49 |
50 | /**
51 | * Convenient way for other components to access entity
52 | */
53 | entity: Entity;
54 |
55 | /**
56 | * key: property Name.
57 | * value: property value From FormControl
58 | */
59 | controlInst: {[index: string]: AnyType} = {};
60 |
61 | /**
62 | * All the control components
63 | */
64 | @ContentChildren(DynamicFieldPropertyComponent, {descendants: true})
65 | propComps: QueryList;
66 |
67 | private subscription = new Subscription();
68 |
69 | constructor(
70 | private readonly contextProcessors: ContextProcessors,
71 | private readonly entityMetaDataRepository: EntityMetaDataRepository,
72 | private readonly setterGetters: PropValueSetterGetters) {}
73 |
74 | ngOnInit() {
75 | if (typeof this.name === 'string') {
76 | this.entity = this.entityMetaDataRepository.getEntity(this.name);
77 | } else {
78 | this.entity = this.name;
79 | }
80 | }
81 | ngOnDestroy() {
82 | this.subscription.unsubscribe();
83 | }
84 |
85 | ngAfterContentInit() {
86 | this.setupProxiesToControl();
87 | this.setupDynamicContextProcessors();
88 | }
89 |
90 | reset() {
91 | this.subscription.unsubscribe();
92 | this.subscription = new Subscription();
93 | this.ngAfterContentInit();
94 | }
95 |
96 | /**
97 | * Sets up controlInst through which other component can access
98 | * FormControl.value
99 | */
100 | protected setupProxiesToControl() {
101 | for (const dfComp of this.propComps.toArray()) {
102 | if (dfComp.inst !== this.inst) {
103 | continue;
104 | }
105 | Object.defineProperty(this.controlInst, dfComp.prop.name, {
106 | enumerable: true,
107 | configurable: false,
108 | get: () => {
109 | return dfComp.control.value;
110 | },
111 | set: (newValue) => {
112 | dfComp.control.setValue(newValue);
113 | },
114 | });
115 | }
116 | }
117 |
118 | /**
119 | * Loops entity.contexts and set up Contextual action.
120 | */
121 | protected setupDynamicContextProcessors() {
122 | const comps = this.propComps.filter(dfComp => dfComp.inst === this.inst);
123 |
124 | for (const ctx of this.entity.contexts) {
125 | const processor = this.contextProcessors.processors.get(ctx.type);
126 | if (processor) {
127 | const subscription = processor.processContext(
128 | ctx, this.inst, this.entity,
129 | this.setterGetters.getSetterGetter(this.entity), comps);
130 | if (subscription) {
131 | this.subscription.add(subscription);
132 | }
133 | }
134 | }
135 | }
136 | }
137 |
138 | /**
139 | * Converts prop from string name to Prop Object
140 | */
141 | @Directive({
142 | selector: '[gdfPropCtx]',
143 | exportAs: 'propContextDirective',
144 | })
145 | export class PropContextDirective implements OnInit {
146 | @Input('gdfPropCtx') name: string;
147 | @Input('entity') entity: Entity;
148 | @Input('entityName') entityName: string;
149 |
150 | prop: Prop;
151 | constructor(private readonly entityMetaDataRepository:
152 | EntityMetaDataRepository) {}
153 | ngOnInit() {
154 | if (this.entityName) {
155 | this.entity = this.entityMetaDataRepository.getEntity(this.entityName);
156 | }
157 | this.prop = this.entity.findProp(this.name);
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/demo-app/app/book_lookup/book_lookup.module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {NgModule} from '@angular/core';
18 | import {FormsModule} from '@angular/forms';
19 | import {MatButtonModule, MatTabsModule} from '@angular/material';
20 | import {RouterModule} from '@angular/router';
21 |
22 | import {DynamicFormModule} from '../../../lib/src/dynamic_form_module';
23 | import {PropValueSetterGetters} from '../../../lib/src/inst_service';
24 | import {Entity} from '../../../lib/src/meta_datamodel';
25 | import {EntityMetaDataRepository, LookupSources, NameValueLookupSource, NameValueLookupValue} from '../../../lib/src/repositories';
26 |
27 | import {BookLookupComponent} from './book_lookup';
28 | import {BOOK_LOOKUP_ENTITY, BOOK_LOOKUP_SRC, BookLookupValueSetterGetter} from './book_lookup_metadata';
29 |
30 | @NgModule({
31 | imports: [
32 | DynamicFormModule,
33 | MatButtonModule,
34 | FormsModule,
35 | RouterModule,
36 | MatTabsModule,
37 | ],
38 | providers: [
39 | BookLookupValueSetterGetter,
40 | ],
41 | declarations: [
42 | BookLookupComponent,
43 | ],
44 | bootstrap: [
45 | BookLookupComponent,
46 | ]
47 | })
48 | export class BookLookupModule {
49 | constructor(
50 | entityMetaDataRepository: EntityMetaDataRepository,
51 | propValueSetterGetters: PropValueSetterGetters,
52 | bookLookupValueSetterGetter: BookLookupValueSetterGetter,
53 | lookupSources: LookupSources,
54 | ) {
55 | // Registers Book entity to the entity repository.
56 | // You only need to do this ONCE in your application.
57 | entityMetaDataRepository.registerMetaData(new Entity(
58 | BOOK_LOOKUP_ENTITY.name, BOOK_LOOKUP_ENTITY.props,
59 | BOOK_LOOKUP_ENTITY.contexts));
60 | // Registers BookLookup entity setter and getter to repository.
61 | // You only need to do this ONCE in your application.
62 | propValueSetterGetters.registerSetterGetter(bookLookupValueSetterGetter);
63 | // Registers LookupSource to repository.
64 | // You only need to do this ONCE in your application.
65 | lookupSources.registerLookupSource(
66 | BOOK_LOOKUP_SRC, this.getCustomLookupSource());
67 | }
68 |
69 | /**
70 | * Constructs the custom Lookup Source.
71 | * Here we construct the Lookup Source with two Lookups: currency, country.
72 | * And we utilize the NameValueLookupSource and NameValueLookupValue class to
73 | * do so.
74 | * In order to construct a Lookup Source, we need to have a class implements
75 | * LookupSource interface. And class NameValueLookupSource implements
76 | * LookupSource interface, is provided by the library already.
77 | * With NameValueLookupSource, you can have your own Lookup Source in a map
78 | * structure, and Lookup Value would be like this:
79 | * {value: 'US', description: 'United States'}
80 | * value is what to store,
81 | * description is what to display on UI.
82 | * You can also have your own Lookup Source class instead of utilizing
83 | * NameValueLookupSource like we do here. Just need to make sure the class
84 | * implements LookupSource interface.
85 | */
86 | private getCustomLookupSource(): NameValueLookupSource {
87 | // Defines currency Lookup values.
88 | const currencyLookupValues: NameValueLookupValue[] = [];
89 | currencyLookupValues.push(new NameValueLookupValue('U.S. Dollar', 'USD'));
90 | currencyLookupValues.push(
91 | new NameValueLookupValue('Canadian Dollar', 'CAD'));
92 | currencyLookupValues.push(new NameValueLookupValue('Chinese Yuan', 'CNY'));
93 | currencyLookupValues.push(new NameValueLookupValue('Euro', 'EUR'));
94 | currencyLookupValues.push(
95 | new NameValueLookupValue('Australian Dollar', 'AUD'));
96 |
97 | // Defines country Lookup values.
98 | const countryLookupValues: NameValueLookupValue[] = [];
99 | countryLookupValues.push(new NameValueLookupValue('United States', 'US'));
100 | countryLookupValues.push(new NameValueLookupValue('Canada', 'CA'));
101 | countryLookupValues.push(new NameValueLookupValue('China', 'CN'));
102 | countryLookupValues.push(new NameValueLookupValue('Europe', 'EU'));
103 | countryLookupValues.push(new NameValueLookupValue('Australia', 'AU'));
104 |
105 | // Creates Lookup Source and adds currencyLookupValues, countryLookupValues
106 | // to it.
107 | // Note: the name of each Lookup needs to match what you define as the
108 | // property name in the entity (./book_lookup_metadata.ts).
109 | const customLookupSource = new NameValueLookupSource();
110 | customLookupSource.lookups.set('currency', currencyLookupValues);
111 | customLookupSource.lookups.set('country', countryLookupValues);
112 |
113 | return customLookupSource;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/demo-app/app/book_lookup/book_lookup_metadata.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /**
18 | * Definition for Book entity.
19 | */
20 | import {Injectable} from '@angular/core';
21 |
22 | import {SimpleValueSetterGetter} from '../../../lib/src/inst_service';
23 | import {AnyType, BaseLookupValue, Entity, NULL_VALUE, Prop, ShowHideContext} from '../../../lib/src/meta_datamodel';
24 | import {assert} from '../../../lib/src/repositories';
25 | import {NameValueLookupSource, NameValueLookupValue} from '../../../lib/src/repositories';
26 |
27 | import {BookLookup} from './book_lookup_sample';
28 |
29 | export const BOOK_LOOKUP_ENTITY_NAME = 'book1';
30 | // Lookup source name for Book entity.
31 | export const BOOK_LOOKUP_SRC = 'book';
32 |
33 | export const BOOK_LOOKUP_ENTITY = {
34 | name: 'book1',
35 | props: [
36 | new Prop({
37 | name: 'name',
38 | type: 'text',
39 | dataType: 'STRING',
40 | label: 'Book Name',
41 | minLength: 2,
42 | isRequired: true,
43 | editable: true,
44 | viewable: true,
45 | }),
46 | new Prop({
47 | name: 'description',
48 | type: 'text',
49 | dataType: 'STRING',
50 | label: 'Description',
51 | minLength: 2,
52 | isRequired: true,
53 | editable: true,
54 | viewable: true,
55 | fieldWidth: 2,
56 | }),
57 | new Prop({
58 | name: 'author',
59 | type: 'text',
60 | dataType: 'STRING',
61 | label: 'Author',
62 | minLength: 2,
63 | isRequired: true,
64 | editable: true,
65 | viewable: true,
66 | }),
67 | new Prop({
68 | name: 'amount',
69 | type: 'text',
70 | dataType: 'NUMBER',
71 | label: 'Price Amount',
72 | min: 0.00,
73 | isRequired: true,
74 | editable: true,
75 | viewable: true,
76 | }),
77 | new Prop({
78 | name: 'currency',
79 | // Modified the type from 'text' to 'select'.
80 | type: 'select',
81 | dataType: 'STRING',
82 | label: 'Price Currency',
83 | isRequired: true,
84 | editable: true,
85 | viewable: true,
86 | // Use 'lookupName' and 'lookupSrc' to find the correct lookup.
87 | lookupName: 'currency',
88 | lookupSrc: BOOK_LOOKUP_SRC,
89 | }),
90 | // Added new property 'country' to demonstrate Lookup usage.
91 | new Prop({
92 | name: 'country',
93 | type: 'select',
94 | dataType: 'STRING',
95 | label: 'Country',
96 | isRequired: false,
97 | editable: true,
98 | viewable: true,
99 | lookupName: 'country',
100 | lookupSrc: BOOK_LOOKUP_SRC,
101 | }),
102 | new Prop({
103 | name: 'isAvailable',
104 | type: 'checkbox',
105 | dataType: 'BOOLEAN',
106 | label: 'Available',
107 | }),
108 | ],
109 | contexts: [
110 | {
111 | srcProp: 'isAvailable',
112 | srcValue: 'true',
113 | target: 'amount',
114 | show: true,
115 | type: ShowHideContext.TYPE,
116 | skipFirstTime: false,
117 | },
118 | ],
119 | };
120 |
121 | /**
122 | * Getter and setter for a BookLookup instance.
123 | * The same as Book example.
124 | */
125 | @Injectable()
126 | export class BookLookupValueSetterGetter extends
127 | SimpleValueSetterGetter {
128 | /**
129 | * Indicates whether can handle the passed entity or not.
130 | */
131 | canHandle(entity: Entity): boolean {
132 | return entity.name === BOOK_LOOKUP_ENTITY_NAME;
133 | }
134 |
135 | /**
136 | * Gets the value from Book instance by Property.
137 | * This is just a simple implementation to get value from instance.
138 | * In real production, the scenario and implementation can be complex.
139 | */
140 | get(inst: BookLookup, prop: Prop): AnyType {
141 | switch (prop.name) {
142 | // Handles property 'amount' and 'currency' differently, since they do
143 | // not match the structure of instance.
144 | case 'amount':
145 | return inst.price.amount;
146 | case 'currency':
147 | return inst.price.currency;
148 | default:
149 | // Uses the default getter for the rest properties.
150 | return super.get(inst, prop);
151 | }
152 | }
153 |
154 | /**
155 | * Sets value to instance.
156 | */
157 | set(inst: BookLookup, prop: Prop, value: AnyType): void {
158 | if (!value) {
159 | return;
160 | }
161 |
162 | switch (prop.name) {
163 | case 'amount':
164 | const amount = Number(value);
165 | // No need to validate the type since UI would give a proper value.
166 | inst.price.amount = amount;
167 | break;
168 | case 'currency':
169 | inst.price.currency = value!.toString();
170 | break;
171 | default:
172 | super.set(inst, prop, value);
173 | break;
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/lib/src/dynamic_form_module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | import {HttpClientModule} from '@angular/common/http';
19 | import {NgModule} from '@angular/core';
20 | import {FormsModule, ReactiveFormsModule} from '@angular/forms';
21 | import {MatAutocompleteModule} from '@angular/material/autocomplete';
22 | import {MatButtonModule} from '@angular/material/button';
23 | import {MatCheckboxModule} from '@angular/material/checkbox';
24 | import {MatRippleModule} from '@angular/material/core';
25 | import {MatDatepickerModule} from '@angular/material/datepicker';
26 | import {MatDialogModule} from '@angular/material/dialog';
27 | import {MatIconModule} from '@angular/material/icon';
28 | import {MatInputModule} from '@angular/material/input';
29 | import {MatRadioModule} from '@angular/material/radio';
30 | import {MatSelectModule} from '@angular/material/select';
31 | import {MatSlideToggleModule} from '@angular/material/slide-toggle';
32 | import {MatTooltipModule} from '@angular/material/tooltip';
33 | import {BrowserModule} from '@angular/platform-browser';
34 |
35 | import {DisableContext, RequiredContext, RestrictLookupContext, SegmentedClearValueContext, SegmentedRequiredContext, SegmentedShowHideContext, SetValueContext, ShowHideContext} from './/meta_datamodel';
36 | import {EntityContextDirective, PropContextDirective} from './entity_directives';
37 | import {InstEditorComponent} from './inst_editor';
38 | import {ContextProcessors, DefaultInstPopulater, DefaultValueConverter, DisableContextProcessor, PropValueSetterGetters, RequiredProcessor, RestrictedLookupProcessor, SegmentedClearValueProcessor, SegmentedRequiredProcessor, SegmentedShowHideProcessor, SetValueContextProcessor, ShowHideContextProcessor} from './inst_service';
39 | import {InstViewerComponent} from './inst_viewer';
40 | import {DynamicFieldPropertyComponent} from './prop_component';
41 | import {DynamicFieldPropertyViewerComponent} from './prop_viewer';
42 | import {EntityMetaDataRepository, LookupSources} from './repositories';
43 |
44 | @NgModule({
45 | imports: [
46 | BrowserModule,
47 | FormsModule,
48 | ReactiveFormsModule,
49 | MatButtonModule,
50 | MatCheckboxModule,
51 | MatDatepickerModule,
52 | MatDialogModule,
53 | MatIconModule,
54 | MatInputModule,
55 | MatRadioModule,
56 | MatRippleModule,
57 | MatSelectModule,
58 | MatTooltipModule,
59 | MatSlideToggleModule,
60 | MatAutocompleteModule,
61 | HttpClientModule,
62 | ],
63 | declarations: [
64 | DynamicFieldPropertyComponent,
65 | DynamicFieldPropertyViewerComponent,
66 | EntityContextDirective,
67 | PropContextDirective,
68 | InstEditorComponent,
69 | InstViewerComponent,
70 | ],
71 | providers: [
72 | {
73 | provide: EntityMetaDataRepository,
74 | useClass: EntityMetaDataRepository,
75 | },
76 | {
77 | provide: LookupSources,
78 | useClass: LookupSources,
79 | },
80 | PropValueSetterGetters,
81 | DefaultValueConverter,
82 | ContextProcessors,
83 | ShowHideContextProcessor,
84 | SetValueContextProcessor,
85 | DisableContextProcessor,
86 | RestrictedLookupProcessor,
87 | RequiredProcessor,
88 | SegmentedShowHideProcessor,
89 | SegmentedRequiredProcessor,
90 | SegmentedClearValueProcessor,
91 | DefaultInstPopulater,
92 | ],
93 | exports: [
94 | DynamicFieldPropertyComponent,
95 | DynamicFieldPropertyViewerComponent,
96 | EntityContextDirective,
97 | PropContextDirective,
98 | InstEditorComponent,
99 | InstViewerComponent,
100 | BrowserModule,
101 | FormsModule,
102 | ReactiveFormsModule,
103 | MatButtonModule,
104 | MatCheckboxModule,
105 | MatDatepickerModule,
106 | MatDialogModule,
107 | MatIconModule,
108 | MatInputModule,
109 | MatRadioModule,
110 | MatRippleModule,
111 | MatSelectModule,
112 | MatTooltipModule,
113 | MatSlideToggleModule,
114 | ],
115 |
116 | })
117 | export class DynamicFormModule {
118 | constructor(
119 | processors: ContextProcessors, showHide: ShowHideContextProcessor,
120 | setValue: SetValueContextProcessor, disable: DisableContextProcessor,
121 | restricted: RestrictedLookupProcessor, required: RequiredProcessor,
122 | segmentedShowHide: SegmentedShowHideProcessor,
123 | segmentedRequired: SegmentedRequiredProcessor,
124 | segmentedClearValue: SegmentedClearValueProcessor) {
125 | processors.registerContextProcessor(ShowHideContext.TYPE, showHide);
126 | processors.registerContextProcessor(SetValueContext.TYPE, setValue);
127 | processors.registerContextProcessor(DisableContext.TYPE, disable);
128 | processors.registerContextProcessor(RestrictLookupContext.TYPE, restricted);
129 | processors.registerContextProcessor(RequiredContext.TYPE, required);
130 | processors.registerContextProcessor(
131 | SegmentedClearValueContext.TYPE, segmentedClearValue);
132 | processors.registerContextProcessor(
133 | SegmentedRequiredContext.TYPE, segmentedRequired);
134 | processors.registerContextProcessor(
135 | SegmentedShowHideContext.TYPE, segmentedShowHide);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/test/restricted_lookup_context.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {MatOption, MatSelect} from '@angular/material';
21 | import {By} from '@angular/platform-browser';
22 |
23 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
24 | import {DisableContext, Entity, NOTNULL_VALUE, NULL_VALUE, Prop, RequiredContext, RestrictLookupContext} from '../src/lib/src/meta_datamodel';
25 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
26 | import {EntityMetaDataRepository, LookupSources} from '../src/lib/src/repositories';
27 |
28 | import {ExampleLookupSrc, ExampleLookupValue} from './example_lookupsrc';
29 |
30 | /**
31 | * Host component to test prop_component.ts
32 | */
33 | @Component({
34 | preserveWhitespaces: true,
35 | template: `
36 |
41 | `
42 | })
43 | export class TestHostComponent {
44 | props: Prop[];
45 | // tslint:disable-next-line:no-any property value can be anything
46 | inst: {[index: string]: any};
47 | }
48 |
49 | // tslint:disable-next-line:ban temporary skip test this CL will be reverted
50 | xdescribe('RestrictLookupContext', () => {
51 | let comp: TestHostComponent;
52 | let fixture: ComponentFixture;
53 |
54 | const entity = new Entity(
55 | 'test',
56 | [
57 | new Prop({
58 | name: 'prop1',
59 | type: 'select',
60 | controlType: 'text',
61 | dataType: 'STRING',
62 | label: 'first Property',
63 | lookupSrc: ExampleLookupSrc.NAME,
64 | lookupName: 'countries',
65 | }),
66 | new Prop({
67 | name: 'prop2',
68 | type: 'select',
69 | controlType: 'text',
70 | dataType: 'STRING',
71 | label: 'second Property',
72 | lookupSrc: ExampleLookupSrc.NAME,
73 | lookupName: 'currencies',
74 | }),
75 | ],
76 | [
77 | {
78 | type: RestrictLookupContext.TYPE,
79 | srcProp: 'prop1',
80 | srcValue: 'US',
81 | target: 'prop2',
82 | targetValues: ['USD', 'CNY'],
83 | skipFirstTime: false,
84 | },
85 | {
86 | type: RestrictLookupContext.TYPE,
87 | srcProp: 'prop1',
88 | srcValue: 'CA',
89 | target: 'prop2',
90 | targetValues: ['CAD', 'CNY'],
91 | skipFirstTime: false,
92 | },
93 | ]);
94 |
95 | // configure
96 | beforeEach(() => {
97 | TestBed.configureTestingModule({
98 | imports: [DynamicFormModule, NoopAnimationsModule],
99 | declarations: [TestHostComponent],
100 | });
101 |
102 | // initialize meta data
103 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
104 | TestBed.get(LookupSources)
105 | .registerLookupSource(ExampleLookupSrc.NAME, new ExampleLookupSrc());
106 | fixture = TestBed.createComponent(TestHostComponent);
107 |
108 | comp = fixture.componentInstance;
109 | comp.props = entity.props;
110 | comp.inst = {prop1: 'CA', prop2: 'CAD'};
111 | fixture.detectChanges();
112 | });
113 |
114 | it('TestLookupDefaultAndSwitch', async(() => {
115 | fixture.detectChanges();
116 | fixture.whenStable().then(() => {
117 | fixture.detectChanges();
118 | const currencySelect =
119 | fixture.debugElement
120 | .query(By.css('mat-form-field.prop2 mat-select'))
121 | .componentInstance as MatSelect;
122 | // empty, CAD, CNY
123 | expect(currencySelect.options.length).toEqual(3);
124 |
125 | const matOption = currencySelect.selected as MatOption;
126 | expect(matOption.value.code).toEqual('CAD');
127 |
128 | const dfComp = fixture.debugElement.query(By.css('gdf-prop.prop1'))
129 | .componentInstance as DynamicFieldPropertyComponent;
130 | const countries = TestBed.get(LookupSources)
131 | .getLookupSource(ExampleLookupSrc.NAME)
132 | .getLookupValues('countries');
133 | dfComp.control.setValue(
134 | countries.find((c: ExampleLookupValue) => c.code === 'US'));
135 | fixture.detectChanges();
136 |
137 | expect(currencySelect.options.length).toEqual(3);
138 | expect(currencySelect.selected).toBeUndefined();
139 | expect(currencySelect.options.find(
140 | option => option.value && option.value.code === 'CAD'))
141 | .toBeUndefined();
142 | expect(currencySelect.options.find(
143 | option => option.value && option.value.code === 'USD'))
144 | .not.toBeUndefined();
145 | });
146 | }));
147 | });
148 |
--------------------------------------------------------------------------------
/test/prop_component_push_value_toinstance.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {MatOption, MatSelect} from '@angular/material';
21 | import {By} from '@angular/platform-browser';
22 | import * as moment_ from 'moment';
23 |
24 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
25 | import {DisableContext, Entity, NOTNULL_VALUE, NULL_VALUE, Prop, RequiredContext, RestrictLookupContext} from '../src/lib/src/meta_datamodel';
26 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
27 | import {EntityMetaDataRepository, LookupSources} from '../src/lib/src/repositories';
28 |
29 | import {ExampleLookupSrc, ExampleLookupValue} from './example_lookupsrc';
30 |
31 | const moment = moment_;
32 | /**
33 | * Host component to test prop_component.ts
34 | */
35 | @Component({
36 | preserveWhitespaces: true,
37 | template: `
38 |
43 | `
44 | })
45 | export class TestHostComponent {
46 | props: Prop[];
47 | // tslint:disable-next-line:no-any property value can be anything
48 | inst: {[index: string]: any};
49 | }
50 |
51 | describe('PushValue', () => {
52 | let comp: TestHostComponent;
53 | let fixture: ComponentFixture;
54 |
55 | const date = new Date(2018, 3, 8);
56 |
57 | const entity = new Entity('test', [
58 | new Prop({
59 | name: 'prop1',
60 | type: 'select',
61 | controlType: 'text',
62 | dataType: 'STRING',
63 | label: 'first Property',
64 | lookupSrc: ExampleLookupSrc.NAME,
65 | lookupName: 'countries',
66 | }),
67 | new Prop({
68 | name: 'prop2',
69 | type: 'select',
70 | controlType: 'text',
71 | dataType: 'STRING',
72 | label: 'second Property',
73 | lookupSrc: ExampleLookupSrc.NAME,
74 | lookupName: 'currencies',
75 | }),
76 | new Prop({
77 | name: 'prop3',
78 | type: 'text',
79 | controlType: 'number',
80 | dataType: 'NUMBER',
81 | label: 'third Property',
82 | }),
83 | new Prop({
84 | name: 'prop4',
85 | type: 'text',
86 | controlType: 'number',
87 | dataType: 'STRING',
88 | label: 'fourth Property',
89 | }),
90 | new Prop({
91 | name: 'prop5',
92 | type: 'text',
93 | controlType: 'date',
94 | dataType: 'STRING',
95 | label: 'fifth Property',
96 | }),
97 | new Prop({
98 | name: 'prop6',
99 | type: 'text',
100 | controlType: 'date',
101 | dataType: 'DATE',
102 | label: 'sixth Property',
103 | }),
104 | ]);
105 |
106 |
107 | // configure
108 | beforeEach(() => {
109 | TestBed.configureTestingModule({
110 | imports: [DynamicFormModule, NoopAnimationsModule],
111 | declarations: [TestHostComponent],
112 | });
113 |
114 | // initialize meta data
115 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
116 | TestBed.get(LookupSources)
117 | .registerLookupSource(ExampleLookupSrc.NAME, new ExampleLookupSrc());
118 | fixture = TestBed.createComponent(TestHostComponent);
119 |
120 | comp = fixture.componentInstance;
121 | comp.props = entity.props;
122 |
123 | comp.inst = {
124 | prop1: 'CA',
125 | prop2: 'CAD',
126 | 'prop3': 5,
127 | 'prop4': '4',
128 | 'prop5': date.toISOString(),
129 | 'prop6': date
130 | };
131 | fixture.detectChanges();
132 | });
133 |
134 | it('push value', fakeAsync(() => {
135 | // make sure value is a object
136 | const dfComp = fixture.debugElement.query(By.css('gdf-prop.prop1'))
137 | .componentInstance as DynamicFieldPropertyComponent;
138 | expect(dfComp.control.value.code).toEqual('CA');
139 | setInputValue('.prop3 input', '6');
140 | comp.inst['prop1'] = '';
141 | comp.inst['prop2'] = '';
142 | comp.inst['prop3'] = 0;
143 | comp.inst['prop4'] = '0';
144 | comp.inst['prop5'] = '', comp.inst['prop6'] = undefined;
145 |
146 | const degugElements = fixture.debugElement.queryAll(By.css('gdf-prop'));
147 | for (const degugElement of degugElements) {
148 | (degugElement.componentInstance as DynamicFieldPropertyComponent)
149 | .pushValueToInstance();
150 | }
151 | // stringified value
152 | expect(comp.inst['prop1']).toEqual('CA');
153 | expect(comp.inst['prop2']).toEqual('CAD');
154 |
155 | // a number
156 | expect(comp.inst['prop3']).toBe(6);
157 | // a string
158 | expect(comp.inst['prop4']).toBe('4');
159 | expect(typeof comp.inst['prop5']).toBe('string');
160 |
161 | expect(comp.inst['prop5']).toBe(moment('2018-04-08').toISOString());
162 | expect(comp.inst['prop6'] instanceof Date).toBeTruthy();
163 | expect((comp.inst['prop6'] as Date).getTime()).toBe(date.getTime());
164 | }));
165 |
166 | function setInputValue(selector: string, value: string) {
167 | fixture.detectChanges();
168 | tick();
169 | const input = fixture.debugElement.query(By.css(selector)).nativeElement;
170 | input.value = value;
171 | input.dispatchEvent(new Event('input'));
172 | tick();
173 | }
174 | });
175 |
--------------------------------------------------------------------------------
/test/prop_component_date.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement} from '@angular/core';
18 | import {async as testasync, ComponentFixture, TestBed} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {By} from '@angular/platform-browser';
21 | import * as moment_ from 'moment';
22 |
23 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
24 | import {InstEditorComponent} from '../src/lib/src/inst_editor';
25 | import {Entity, Prop} from '../src/lib/src/meta_datamodel';
26 | import {EntityMetaDataRepository} from '../src/lib/src/repositories';
27 |
28 | const moment = moment_;
29 | /**
30 | * Host component to test prop_component.ts
31 | */
32 | @Component({
33 | preserveWhitespaces: true,
34 | template: `
35 |
38 | `
39 | })
40 | export class TestHostComponent {
41 | props: Prop[];
42 | // tslint:disable-next-line:no-any property value can be anything
43 | inst: {[index: string]: any};
44 | }
45 |
46 | describe('DateInput', () => {
47 | let comp: TestHostComponent;
48 | let fixture: ComponentFixture;
49 | let de: DebugElement;
50 | let el: HTMLElement;
51 |
52 | const entity = new Entity('test', [
53 | new Prop({
54 | name: 'prop1',
55 | type: 'text',
56 | controlType: 'date',
57 | dataType: 'DATE',
58 | label: 'first Property',
59 | }),
60 | new Prop({
61 | name: 'prop2',
62 | type: 'text',
63 | controlType: 'date',
64 | dataType: 'STRING',
65 | label: 'second Property',
66 | }),
67 | new Prop({
68 | name: 'prop3',
69 | type: 'text',
70 | controlType: 'datetime-local',
71 | dataType: 'DATETIME',
72 | label: 'third Property',
73 | }),
74 | new Prop({
75 | name: 'prop4',
76 | type: 'text',
77 | controlType: 'datetime-local',
78 | dataType: 'STRING',
79 | label: 'fourth Property',
80 | }),
81 | ]);
82 |
83 |
84 | // configure
85 | beforeEach(() => {
86 | TestBed.configureTestingModule({
87 | imports: [DynamicFormModule, NoopAnimationsModule],
88 | declarations: [TestHostComponent],
89 | });
90 |
91 | // initialize meta data
92 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
93 |
94 | fixture = TestBed.createComponent(TestHostComponent);
95 | comp = fixture.componentInstance;
96 | comp.props = entity.props;
97 | comp.inst = {};
98 | fixture.detectChanges();
99 | });
100 |
101 |
102 | it('DateValueSetCorrecty', testasync(async () => {
103 | const d = new Date();
104 | comp.inst['prop1'] = d;
105 | comp.inst['prop2'] = d.toISOString();
106 | await fixture.whenStable();
107 | fixture.detectChanges();
108 |
109 | de = fixture.debugElement.query(By.css('.prop1 input'));
110 | el = de.nativeElement;
111 | expect((el as HTMLInputElement).value)
112 | .toEqual(moment(d).format('YYYY-MM-DD'));
113 |
114 | de = fixture.debugElement.query(By.css('.prop2 input'));
115 | el = de.nativeElement;
116 | expect((el as HTMLInputElement).value)
117 | .toEqual(moment(d).format('YYYY-MM-DD'));
118 | }));
119 |
120 | it('EmptyDateInput', testasync(async () => {
121 | await fixture.whenStable();
122 | fixture.detectChanges();
123 |
124 | comp.inst['prop1'] = '';
125 | comp.inst['prop2'] = '';
126 |
127 | const degugElement = fixture.debugElement.query(By.css('inst-editor'));
128 | const instEditor = degugElement.componentInstance as InstEditorComponent;
129 | instEditor.pushValueToInstance();
130 |
131 | expect(comp.inst.prop1).toBeUndefined();
132 | expect(comp.inst.prop2).toBeUndefined();
133 | }));
134 |
135 | it('userErrorCase', testasync(async () => {
136 | const d = new Date();
137 | // prop1 is supposed to be Date type.
138 | comp.inst['prop1'] = d.toISOString();
139 | comp.inst['prop2'] = d.toISOString();
140 | await fixture.whenStable();
141 | fixture.detectChanges();
142 |
143 | de = fixture.debugElement.query(By.css('.prop1 input'));
144 | el = de.nativeElement;
145 | expect((el as HTMLInputElement).value)
146 | .toEqual(moment(d).format('YYYY-MM-DD'));
147 | }));
148 |
149 | it('DateTimeValueSetCorrecty', testasync(async () => {
150 | const d = new Date();
151 | comp.inst['prop3'] = d;
152 | comp.inst['prop4'] = d.toISOString();
153 | await fixture.whenStable();
154 | fixture.detectChanges();
155 |
156 | de = fixture.debugElement.query(By.css('.prop3 input'));
157 | el = de.nativeElement;
158 | expect((el as HTMLInputElement).value)
159 | .toEqual(moment(d).format('YYYY-MM-DDTHH:ss'));
160 |
161 | de = fixture.debugElement.query(By.css('.prop4 input'));
162 | el = de.nativeElement;
163 | expect((el as HTMLInputElement).value)
164 | .toEqual(moment(d).format('YYYY-MM-DDTHH:ss'));
165 | }));
166 |
167 | it('userErrorCaseDateTime', testasync(async () => {
168 | const d = new Date();
169 | // prop3 is supposed to be Date type.
170 | comp.inst['prop3'] = d.toISOString();
171 | comp.inst['prop4'] = d.toISOString();
172 | await fixture.whenStable();
173 | fixture.detectChanges();
174 |
175 | de = fixture.debugElement.query(By.css('.prop3 input'));
176 | el = de.nativeElement;
177 | expect((el as HTMLInputElement).value)
178 | .toEqual(moment(d).format('YYYY-MM-DDTHH:ss'));
179 | }));
180 | });
181 |
--------------------------------------------------------------------------------
/test/inst_editor.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async as testasync, ComponentFixture, TestBed} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {By} from '@angular/platform-browser';
21 |
22 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
23 | import {DisableContext, Entity, NOTNULL_VALUE, Prop} from '../src/lib/src/meta_datamodel';
24 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
25 | import {EntityMetaDataRepository, LookupSources} from '../src/lib/src/repositories';
26 |
27 | import {ExampleLookupSrc} from './example_lookupsrc';
28 |
29 | /**
30 | * Host component to test prop_component.ts
31 | */
32 | @Component({
33 | preserveWhitespaces: true,
34 | template: `
35 |
38 | `
39 | })
40 | export class TestHostComponent {
41 | props: Prop[];
42 | // tslint:disable-next-line:no-any property value can be anything
43 | inst: {[index: string]: any};
44 | }
45 |
46 | describe('InstEditorTest', () => {
47 | let comp: TestHostComponent;
48 | let fixture: ComponentFixture;
49 | const entity = new Entity(
50 | 'test',
51 | [
52 | new Prop({
53 | name: 'id',
54 | type: 'text',
55 | controlType: 'text',
56 | dataType: 'STRING',
57 | label: 'id property',
58 | editable: false,
59 | }),
60 | new Prop({
61 | name: 'prop1',
62 | type: 'text',
63 | controlType: 'text',
64 | dataType: 'STRING',
65 | label: 'first Property',
66 | fieldWidth: 2,
67 | }),
68 | new Prop({
69 | name: 'prop2',
70 | type: 'text',
71 | controlType: 'text',
72 | dataType: 'STRING',
73 | label: 'second Property',
74 | }),
75 | new Prop({
76 | name: 'prop3',
77 | type: 'text',
78 | controlType: 'text',
79 | dataType: 'STRING',
80 | label: 'third Property',
81 | }),
82 | new Prop({
83 | name: 'prop4',
84 | type: 'text',
85 | controlType: 'text',
86 | dataType: 'STRING',
87 | label: 'fourth Property',
88 | }),
89 |
90 | ],
91 | [{
92 | type: DisableContext.TYPE,
93 | srcs: new Map([['id', NOTNULL_VALUE]]),
94 | relation: 'and',
95 | target: 'prop4',
96 | disable: true,
97 | skipFirstTime: false,
98 | }],
99 | );
100 |
101 | // configure
102 | beforeEach(() => {
103 | TestBed.configureTestingModule({
104 | imports: [DynamicFormModule, NoopAnimationsModule],
105 | declarations: [TestHostComponent],
106 | });
107 |
108 | // initialize meta data
109 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
110 | fixture = TestBed.createComponent(TestHostComponent);
111 |
112 | comp = fixture.componentInstance;
113 | comp.props = entity.props;
114 | comp.inst = {
115 | id: '',
116 | prop1: 'value1',
117 | prop2: 'value2',
118 | prop3: 'value3',
119 | prop4: 'value4',
120 | };
121 | fixture.detectChanges();
122 | });
123 |
124 | it('basic', testasync(() => {
125 | // prop 2 and prop3 are not cleared at fist time
126 | let el = fixture.debugElement.query(By.css('mat-form-field.prop1 input'))
127 | .nativeElement as HTMLInputElement;
128 | expect(el.value).toEqual('value1');
129 |
130 | el = fixture.debugElement.query(By.css('mat-form-field.prop2 input'))
131 | .nativeElement as HTMLInputElement;
132 | expect(el.value).toEqual('value2');
133 |
134 | el = fixture.debugElement.query(By.css('mat-form-field.prop3 input'))
135 | .nativeElement as HTMLInputElement;
136 | expect(el.value).toEqual('value3');
137 |
138 | el = fixture.debugElement.query(By.css('mat-form-field.prop4 input'))
139 | .nativeElement as HTMLInputElement;
140 | expect(el.value).toEqual('value4');
141 | }));
142 |
143 | it('swapInstanceToBeEditted', testasync(async () => {
144 | comp.inst = {
145 | id: '',
146 | prop1: 'new value1',
147 | prop2: 'new value2',
148 | prop3: 'new value3',
149 | prop4: 'new value4',
150 | };
151 | fixture.detectChanges();
152 | await fixture.whenStable();
153 | fixture.detectChanges();
154 |
155 | // clear when loading
156 | const el =
157 | fixture.debugElement.query(By.css('mat-form-field.prop1 input'))
158 | .nativeElement as HTMLInputElement;
159 | expect(el.value).toEqual('new value1');
160 | }));
161 |
162 | /* If the id is not null, prop4is disabled after switch. */
163 | it('swapInstanceContext', testasync(async () => {
164 | let el = fixture.debugElement.query(By.css('mat-form-field.prop4 input'))
165 | .nativeElement;
166 | expect(el.disabled).toBeFalsy();
167 | comp.inst = {
168 | id: '1',
169 | prop1: 'new value1',
170 | prop2: 'new value2',
171 | prop3: 'new value3',
172 | prop4: 'new value4',
173 | };
174 | fixture.detectChanges();
175 | await fixture.whenStable();
176 | fixture.detectChanges();
177 |
178 | el = fixture.debugElement.query(By.css('mat-form-field.prop4 input'))
179 | .nativeElement;
180 | expect(el.disabled).toBeTruthy();
181 | }));
182 | });
183 |
--------------------------------------------------------------------------------
/src/lib/src/prop_viewer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | import {ChangeDetectorRef, Component, ElementRef, Injector, Input, OnInit,} from '@angular/core';
19 | import * as moment_ from 'moment';
20 |
21 | import {PropValueSetterGetters} from './inst_service';
22 | import {AnyType, BaseLookupValue, Entity, Prop} from './meta_datamodel';
23 | import {LookupSources} from './repositories';
24 | import {AutoCompleteLookupService, EntityMetaDataRepository} from './repositories';
25 |
26 | const moment = moment_;
27 | /**
28 | * Viewer component for property
29 | *
30 | * Example:
31 | *
32 | * + prop: target {@link Prop} to view
33 | * + inst: instance object from which property's value can be retrieved
34 | */
35 | @Component({
36 | preserveWhitespaces: true,
37 | selector: 'gdf-prop-viewer',
38 | templateUrl: 'prop_viewer.ng.html',
39 | })
40 | export class DynamicFieldPropertyViewerComponent implements OnInit {
41 | @Input() prop: Prop;
42 | @Input() inst: {};
43 | @Input() showLabel = true;
44 |
45 | /**
46 | * Developer has two approaches to specifiy prop
47 | * 1. use [prop]
48 | * 2. use [propName] and [entityName]
49 | * The second approach is convenient for some simple use case
50 | */
51 | @Input() propName: string;
52 | @Input() entityName: string;
53 |
54 |
55 | /**
56 | * css classes
57 | */
58 | propClasses = ['prop'];
59 |
60 |
61 | /**
62 | * Value for autocomplete property
63 | */
64 | _autoValue: BaseLookupValue|undefined;
65 |
66 | private autoCompleteService: AutoCompleteLookupService;
67 |
68 | format: string;
69 | private internalShow = true;
70 |
71 | constructor(
72 | private readonly entityMetaDataRepository: EntityMetaDataRepository,
73 | private readonly elRef: ElementRef,
74 | private readonly lookupSources: LookupSources,
75 | private readonly propValueSetterGetters: PropValueSetterGetters,
76 | private readonly injector: Injector,
77 | private cd: ChangeDetectorRef,
78 | ) {}
79 |
80 | get entity(): Entity {
81 | return this.prop.entity;
82 | }
83 |
84 | get show() {
85 | return this.internalShow;
86 | }
87 | set show(value: boolean) {
88 | if (value === this.internalShow) {
89 | return;
90 | }
91 | this.internalShow = value;
92 | if (value) {
93 | this.elRef.nativeElement.classList.remove('hide');
94 | } else {
95 | this.elRef.nativeElement.classList.add('hide');
96 | }
97 | }
98 |
99 | ngOnInit() {
100 | if (this.propName && this.entityName) {
101 | this.prop = this.entityMetaDataRepository.getEntity(this.entityName)
102 | .findProp(this.propName);
103 | } else {
104 | this.propName = this.prop.name;
105 | this.entityName = this.prop.entity.name;
106 | }
107 | this.propClasses.push(this.prop.name);
108 | if (this.prop.controlType) {
109 | this.propClasses.push(this.prop.controlType);
110 | }
111 | if (this.prop.fieldWidth > 1) {
112 | this.propClasses.push(`field-width-${this.prop.fieldWidth}`);
113 | }
114 | if (this.prop.controlType === 'date') {
115 | this.format = this.prop.format || 'mediumDate';
116 | }
117 | if (this.prop.controlType === 'datetime-local') {
118 | this.format = this.prop.format || 'medium';
119 | }
120 | if (this.prop.controlType === 'number') {
121 | this.format = this.prop.format || '1.0-3';
122 | }
123 | if (this.prop.type === 'autocomplete') {
124 | // run before buildControl. buildControl uses this.
125 | this.setupAutocomplete();
126 | }
127 | }
128 |
129 | get value() {
130 | // value from inst
131 | let value =
132 | this.propValueSetterGetters.getSetterGetterForProp(this.prop).get(
133 | this.inst, this.prop);
134 | if (this.prop.type === 'select' && this.prop.lookupSrc &&
135 | this.prop.lookupName && value) {
136 | value = this.lookupSources.getLookupSource(this.prop.lookupSrc)
137 | .propValueToLookupValue(this.prop.lookupName, value);
138 | }
139 | if (this.prop.type === 'autocomplete' && value &&
140 | this.autoCompleteService) {
141 | // set value at a later time. Need to go server to resolve value
142 | this.autoCompleteService.resolvePropValue(value).then(lookupValue => {
143 | if ((this._autoValue && lookupValue &&
144 | this._autoValue.description !== lookupValue.description) ||
145 | (!this._autoValue && lookupValue) ||
146 | (this._autoValue && !lookupValue)) {
147 | // retrieve new value;
148 | this._autoValue = lookupValue;
149 | this.cd.detectChanges();
150 | }
151 | });
152 | // return old value first;
153 | return this._autoValue;
154 | }
155 |
156 | value = this.toTypedValue(value);
157 | return value;
158 | }
159 |
160 | // a typed value is needed for UI to be used by format
161 | private toTypedValue(value: AnyType) {
162 | if (typeof value !== 'string') {
163 | return value;
164 | }
165 | if (value === '') {
166 | return null;
167 | }
168 | if (this.prop.controlType === 'number') {
169 | return Number(value);
170 | }
171 | /*
172 | * Why we use moment here
173 | * possible value type:
174 | * 1. ISOString
175 | * 2. Date Object
176 | * 3. String from input[type=date]
177 | * Date object can't handle the third type properly
178 | * moment can handle all of them
179 | */
180 | if (this.prop.controlType === 'date') {
181 | const m = moment(value);
182 | return m.isValid() ? m.toDate() : null;
183 | }
184 | if (this.prop.controlType === 'datetime' ||
185 | this.prop.controlType === 'datetime-local') {
186 | const m = moment(value);
187 | return m.isValid() ? m.toDate() : null;
188 | }
189 | return value;
190 | }
191 |
192 | //-----------------------autocomplete support
193 | setupAutocomplete() {
194 | let serviceName = this.prop.autoCompleteService;
195 | if (!serviceName) {
196 | serviceName = `${this.prop.entity.name}_${this.prop.name}`;
197 | }
198 | this.autoCompleteService =
199 | // TODO(jjzhang) figure out how to workaround deprecation later
200 | // tslint:disable-next-line:deprecation later
201 | this.injector.get(serviceName) as AutoCompleteLookupService;
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/test/disable_context.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {By} from '@angular/platform-browser';
21 |
22 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
23 | import {DisableContext, Entity, NOTNULL_VALUE, NULL_VALUE, Prop, RequiredContext} from '../src/lib/src/meta_datamodel';
24 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
25 | import {EntityMetaDataRepository, LookupSources} from '../src/lib/src/repositories';
26 |
27 | import {ExampleLookupSrc} from './example_lookupsrc';
28 |
29 |
30 | /**
31 | * Host component to test prop_component.ts
32 | */
33 | @Component({
34 | preserveWhitespaces: true,
35 | template: `
36 |
41 | `
42 | })
43 | export class TestHostComponent {
44 | props: Prop[];
45 | // tslint:disable-next-line:no-any property value can be anything
46 | inst: {[index: string]: any};
47 | }
48 |
49 | describe('DisableContext', () => {
50 | let comp: TestHostComponent;
51 | let fixture: ComponentFixture;
52 |
53 |
54 | const entity = new Entity(
55 | 'test',
56 | [
57 | new Prop({
58 | name: 'prop1',
59 | type: 'text',
60 | controlType: 'text',
61 | dataType: 'STRING',
62 | label: 'first Property',
63 | }),
64 | new Prop({
65 | name: 'prop2',
66 | type: 'text',
67 | controlType: 'text',
68 | dataType: 'STRING',
69 | label: 'second Property',
70 | }),
71 | new Prop({
72 | name: 'prop3',
73 | type: 'text',
74 | controlType: 'text',
75 | dataType: 'STRING',
76 | label: '3rd Property',
77 | }),
78 | new Prop({
79 | name: 'prop4',
80 | type: 'text',
81 | controlType: 'text',
82 | dataType: 'STRING',
83 | label: '4th Property',
84 | }),
85 |
86 | new Prop({
87 | name: 'prop5',
88 | type: 'text',
89 | controlType: 'text',
90 | dataType: 'STRING',
91 | label: 'fifth Property',
92 | }),
93 | new Prop({
94 | name: 'prop6',
95 | type: 'text',
96 | controlType: 'text',
97 | dataType: 'STRING',
98 | label: 'sixth Property',
99 | }),
100 | new Prop({
101 | name: 'prop7',
102 | type: 'text',
103 | controlType: 'text',
104 | dataType: 'STRING',
105 | label: 'seventh Property',
106 | }),
107 | new Prop({
108 | name: 'prop8',
109 | type: 'text',
110 | controlType: 'text',
111 | dataType: 'STRING',
112 | label: 'eighth Property',
113 | }),
114 | new Prop({
115 | name: 'prop9',
116 | type: 'text',
117 | controlType: 'text',
118 | dataType: 'STRING',
119 | label: 'ninth Property',
120 | editable: false,
121 | }),
122 | ],
123 | [
124 | {
125 | type: DisableContext.TYPE,
126 | srcs: new Map(
127 | [['prop1', 'value1'], ['prop2', 'value2']]),
128 | relation: 'and',
129 | target: 'prop3',
130 | disable: true,
131 | skipFirstTime: false,
132 | },
133 | {
134 | type: DisableContext.TYPE,
135 | srcs: new Map(
136 | [['prop4', 'value4'], ['prop5', 'value5']]),
137 | relation: 'or',
138 | target: 'prop6',
139 | disable: true,
140 | skipFirstTime: false,
141 | },
142 | ]);
143 |
144 |
145 | // configure
146 | beforeEach(() => {
147 | TestBed.configureTestingModule({
148 | imports: [DynamicFormModule, NoopAnimationsModule],
149 | declarations: [TestHostComponent],
150 | });
151 |
152 | // initialize meta data
153 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
154 | fixture = TestBed.createComponent(TestHostComponent);
155 |
156 | comp = fixture.componentInstance;
157 | comp.props = entity.props;
158 | comp.inst =
159 | {prop1: 'value1', prop2: 'value2', prop4: 'value4', prop5: 'value5'};
160 | fixture.detectChanges();
161 | });
162 |
163 |
164 | it('TestAnd', async(() => {
165 | fixture.detectChanges();
166 | fixture.whenStable().then(() => {
167 | fixture.detectChanges();
168 | const el =
169 | fixture.debugElement.query(By.css('mat-form-field.prop3 input'))
170 | .nativeElement;
171 | expect(el.disabled).toBeTruthy();
172 |
173 | setInputValueAsync('.prop2 input', '');
174 | fixture.detectChanges();
175 | expect(el.disabled).toBeFalsy();
176 | });
177 | }));
178 |
179 | it('TestOr', async(() => {
180 | fixture.detectChanges();
181 | fixture.whenStable().then(() => {
182 | fixture.detectChanges();
183 | const el =
184 | fixture.debugElement.query(By.css('mat-form-field.prop6 input'))
185 | .nativeElement;
186 | expect(el.disabled).toBeTruthy();
187 |
188 | setInputValueAsync('.prop4 input', '');
189 | fixture.detectChanges();
190 | expect(el.disabled).toBeTruthy();
191 | setInputValueAsync('.prop5 input', 'some value');
192 | fixture.detectChanges();
193 | expect(el.disabled).toBeFalsy();
194 | });
195 | }));
196 |
197 | // form control is disabled when property editable is false
198 | it('editable', async(() => {
199 | fixture.detectChanges();
200 | const el =
201 | fixture.debugElement.query(By.css('mat-form-field.prop9 input'))
202 | .nativeElement;
203 | expect(el.disabled).toBeTruthy();
204 | }));
205 |
206 | function setInputValueAsync(selector: string, value: string) {
207 | const input = fixture.debugElement.query(By.css(selector)).nativeElement;
208 | input.value = value;
209 | input.dispatchEvent(new Event('input'));
210 | }
211 | });
212 |
--------------------------------------------------------------------------------
/src/lib/src/repositories.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2018 Google LLC
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | import {Observable} from 'rxjs';
19 |
20 | import {AnyType, BaseLookupValue, Entity} from './meta_datamodel';
21 |
22 | /**
23 | * An assert function to remove undefined|null from type
24 | *
25 | * Don't use javascript/typescript/contrib:assert which uses closure
26 | * module 'goog.asserts'. Closure module can't be used easily by nodejs
27 | */
28 | export function assert(condition: T|undefined|null): T {
29 | // tslint:disable-next-line:triple-equals check both null and undefined
30 | if (condition == undefined) {
31 | throw new Error('undefined found in assert ');
32 | }
33 | return condition;
34 | }
35 | /**
36 | * A repository containing all meta data for entity
37 | */
38 | export class EntityMetaDataRepository {
39 | entities = new Map();
40 | registerMetaData(entity: Entity) {
41 | this.entities.set(entity.name, entity);
42 | }
43 | getEntity(name: string): Entity {
44 | return assert(this.entities.get(name));
45 | }
46 | hasEntity(name: string): boolean {
47 | return this.entities.has(name);
48 | }
49 | }
50 |
51 | /**
52 | * A Lookup Source has many lookup. A lookup is used by drop down.
53 | * Example lookup: country. It has many Lookup values: US, CANADA, etc.
54 | */
55 | export interface LookupSource {
56 | /**
57 | * Convert the selected Lookup Value Object to a value that can be set to
58 | * instance.
59 | * When a drop down is selected, the selected value will be Lookup Value
60 | * object. Instance property may not use that object directly. The method
61 | * transform Lookup Value object to a value suitable as property value.
62 | */
63 | lookupValueToPropValue(lookupValue: BaseLookupValue): AnyType;
64 |
65 | /**
66 | * Convert the property value to a lookup value Object.
67 | * FormControl for dropdown/select will always use Lookup Value object as
68 | * value. The returned value is set to control's value
69 | *
70 | * Why 'undefined' is one returned value here?
71 | * Some system passes a JS object as value representing an empty value.
72 | * In this case, undefined should be returned since there is no lookup
73 | * corresponding to empty value.
74 | */
75 | propValueToLookupValue(lookupName: string, value: AnyType): BaseLookupValue
76 | |undefined;
77 |
78 | /**
79 | * A stringified value.
80 | */
81 | lookupValueToString(lookupValue: BaseLookupValue): string;
82 |
83 | /**
84 | * Return a list of lookup values for a Lookup
85 | */
86 | getLookupValues(lookupName: string): BaseLookupValue[];
87 | }
88 |
89 | /**
90 | * Service to fetch a list lookup values for an autocomplete field.
91 | */
92 | export interface AutoCompleteLookupService {
93 | /**
94 | * A list of lookup values for a autocomplete field.
95 | */
96 | getLookups(): Observable;
97 |
98 | /**
99 | * The value user types to autocomplete field.
100 | */
101 | setFilter(filter: AnyType): void;
102 |
103 | /**
104 | * Resolves a property value to a Lookup Value
105 | * Promise is used here since this service could go to server to
106 | * search the value.
107 | */
108 | resolvePropValue(value: AnyType): Promise;
109 | /**
110 | * Converts a Lookup Value to property value which can be saved to instance
111 | * Runtime type for lookupValue
112 | * 1. undefined: user didn't type anything
113 | * 2. string: user typed some string, but doesn't select anything
114 | * 3. a Lookup Value Object user selected
115 | */
116 | lookupValueToPropValue(lookupValue: BaseLookupValue|undefined|
117 | string): AnyType;
118 | }
119 |
120 | /**
121 | * A repository for all Lookup Sources
122 | */
123 | export class LookupSources {
124 | private strategies = new Map();
125 |
126 | registerLookupSource(lookupSrc: string, strategy: LookupSource) {
127 | this.strategies.set(lookupSrc, strategy);
128 | }
129 | getLookupSource(lookupSrc: string): LookupSource {
130 | return assert(this.strategies.get(lookupSrc));
131 | }
132 | }
133 |
134 |
135 | export class NameValueLookupValue extends BaseLookupValue {
136 | value: string;
137 | constructor(description: string, value?: string) {
138 | super();
139 | this.description = description;
140 | if (value) {
141 | this.value = value;
142 | } else {
143 | this.value = description;
144 | }
145 | }
146 | }
147 |
148 |
149 | export class ObjectLookupValue extends BaseLookupValue {
150 | value: {};
151 | constructor(description: string, value?: {}) {
152 | super();
153 | this.description = description;
154 | if (value) {
155 | this.value = value;
156 | } else {
157 | this.value = description;
158 | }
159 | }
160 | }
161 |
162 | export class NameValueLookupSource implements LookupSource {
163 | name: string;
164 | readonly lookups = new Map();
165 | lookupValueToPropValue(lookupValue: NameValueLookupValue): AnyType {
166 | if (!lookupValue) {
167 | return null;
168 | }
169 | return lookupValue.value;
170 | }
171 |
172 |
173 | propValueToLookupValue(lookupName: string, value: AnyType): BaseLookupValue {
174 | const values = assert(this.lookups.get(lookupName));
175 | return assert(values.find(lookupValue => lookupValue.value === value));
176 | }
177 |
178 |
179 | lookupValueToString(lookupValue: NameValueLookupValue): string {
180 | return lookupValue.value;
181 | }
182 |
183 | getLookupValues(lookupName: string): BaseLookupValue[] {
184 | return assert(this.lookups.get(lookupName));
185 | }
186 | }
187 |
188 |
189 | export class ObjectLookupSource implements LookupSource {
190 | name: string;
191 | readonly lookups = new Map();
192 | lookupValueToPropValue(lookupValue: ObjectLookupValue): AnyType {
193 | if (!lookupValue) {
194 | return null;
195 | }
196 | return lookupValue.value;
197 | }
198 |
199 |
200 | propValueToLookupValue(lookupName: string, value: AnyType): BaseLookupValue {
201 | const values = assert(this.lookups.get(lookupName));
202 | return assert(values.find(lookupValue => lookupValue.value === value));
203 | }
204 |
205 |
206 | lookupValueToString(lookupValue: ObjectLookupValue): string {
207 | throw new Error('not Supported');
208 | }
209 |
210 | getLookupValues(lookupName: string): BaseLookupValue[] {
211 | return assert(this.lookups.get(lookupName));
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/test/prop_component_text.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async as testasync, ComponentFixture, TestBed} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {By} from '@angular/platform-browser';
21 |
22 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
23 | import {Entity, Prop} from '../src/lib/src/meta_datamodel';
24 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
25 | import {EntityMetaDataRepository} from '../src/lib/src/repositories';
26 |
27 |
28 | /**
29 | * Host component to test prop_component.ts
30 | */
31 | @Component({
32 | preserveWhitespaces: true,
33 | template: `
34 |
39 | `
40 | })
41 | export class TestHostComponent {
42 | props: Prop[];
43 | // tslint:disable-next-line:no-any property value can be anything
44 | inst: {[index: string]: any};
45 | }
46 |
47 | describe('TextInput', () => {
48 | let comp: TestHostComponent;
49 | let fixture: ComponentFixture;
50 | let de: DebugElement;
51 | let el: HTMLElement;
52 |
53 | const entity = new Entity('test', [
54 | new Prop({
55 | name: 'prop1',
56 | type: 'text',
57 | controlType: 'text',
58 | dataType: 'STRING',
59 | label: 'first Property',
60 | fieldWidth: 2,
61 | }),
62 | new Prop({
63 | name: 'prop2',
64 | type: 'text',
65 | controlType: 'text',
66 | dataType: 'STRING',
67 | label: 'second Property',
68 | regExp: '[a-zA-Z0-9]*',
69 | }),
70 | new Prop({
71 | name: 'prop3',
72 | type: 'text',
73 | controlType: 'text',
74 | dataType: 'STRING',
75 | label: 'third Property',
76 | regExp: '[a-zA-Z0-9]*',
77 | regExpErrorMsg: 'Only alphanumeric characters are allowed',
78 | }),
79 | new Prop({
80 | name: 'prop4',
81 | type: 'text',
82 | controlType: 'text',
83 | dataType: 'STRING',
84 | label: 'fourth Property',
85 | minLength: 3,
86 | maxLength: 10
87 | }),
88 | ]);
89 |
90 | // configure
91 | beforeEach(() => {
92 | TestBed.configureTestingModule({
93 | imports: [DynamicFormModule, NoopAnimationsModule],
94 | declarations: [TestHostComponent],
95 | });
96 |
97 | // initialize meta data
98 | TestBed.get(EntityMetaDataRepository).registerMetaData(entity);
99 |
100 | fixture = TestBed.createComponent(TestHostComponent);
101 | comp = fixture.componentInstance;
102 | comp.props = entity.props;
103 | comp.inst = {prop1: 'value1'};
104 | fixture.detectChanges();
105 | });
106 |
107 | it('Label is shown', () => {
108 | // query for the title
by CSS element selector
109 | de = fixture.debugElement.query(By.css('.prop1 mat-form-field'));
110 | el = de.nativeElement;
111 | expect(el.textContent).toContain(entity.props[0].label);
112 | });
113 |
114 | it('fieldWidth is handled', () => {
115 | // query for the title
by CSS element selector
116 | de = fixture.debugElement.query(By.css('.prop1 mat-form-field'));
117 | el = de.nativeElement;
118 | expect(el.classList.contains('field-width-2')).toBeTruthy();
119 | });
120 |
121 | it('value is shown', () => {
122 | // query for the title
by CSS element selector
123 | de = fixture.debugElement.query(By.css('.prop1 input'));
124 | el = de.nativeElement;
125 | expect((el as HTMLInputElement).value)
126 | .toEqual(comp.inst[entity.props[0].name]);
127 | });
128 |
129 | it('value is pushed to control', testasync(async () => {
130 | setInputValue('.prop1 input', 'value2');
131 | await fixture.whenStable();
132 | const propComp = fixture.debugElement.query(By.css('gdf-prop.prop1'))
133 | .componentInstance as DynamicFieldPropertyComponent;
134 | expect(propComp.control.value).toEqual('value2');
135 | expect(comp.inst.prop1).toEqual('value1');
136 | propComp.pushValueToInstance();
137 | expect(comp.inst.prop1).toEqual('value2');
138 | }));
139 |
140 | it('pattern error', testasync(async () => {
141 | setInputValue('.prop2 input', 'value2');
142 | await fixture.whenStable();
143 | fixture.detectChanges();
144 | de = fixture.debugElement.query(By.css('gdf-prop.prop2 mat-error'));
145 | expect(de).toBeNull();
146 | setInputValue('.prop2 input', '$%^&');
147 | await fixture.whenStable();
148 | fixture.detectChanges();
149 |
150 | de = fixture.debugElement.query(By.css('gdf-prop.prop2 mat-error'));
151 | expect(de).not.toBeNull();
152 | el = de.nativeElement as HTMLElement;
153 | expect(el.textContent).toContain('Value must match pattern: ');
154 | }));
155 |
156 | it('error message for pattern', testasync(async () => {
157 | setInputValue('.prop3 input', '$%^&');
158 | await fixture.whenStable();
159 | fixture.detectChanges();
160 |
161 | de = fixture.debugElement.query(By.css('gdf-prop.prop3 mat-error'));
162 | expect(de).not.toBeNull();
163 | el = de.nativeElement as HTMLElement;
164 | expect(el.textContent)
165 | .toContain('Only alphanumeric characters are allowed');
166 | }));
167 |
168 | it('minlength', testasync(async () => {
169 | setInputValue('.prop4 input', 'v1');
170 | await fixture.whenStable();
171 | fixture.detectChanges();
172 |
173 | de = fixture.debugElement.query(By.css('gdf-prop.prop4 mat-error'));
174 | expect(de).not.toBeNull();
175 | el = de.nativeElement as HTMLElement;
176 | expect(el.textContent).toContain('Must have at least 3 characters');
177 | }));
178 |
179 | it('maxlength', testasync(async () => {
180 | setInputValue('.prop4 input', '12345678912');
181 | await fixture.whenStable();
182 | fixture.detectChanges();
183 |
184 | de = fixture.debugElement.query(By.css('gdf-prop.prop4 mat-error'));
185 | expect(de).not.toBeNull();
186 | el = de.nativeElement as HTMLElement;
187 | expect(el.textContent).toContain('Can have 10 characters at maximum');
188 | }));
189 |
190 |
191 | function setInputValue(selector: string, value: string) {
192 | const input = fixture.debugElement.query(By.css(selector)).nativeElement;
193 | input.value = value;
194 | input.dispatchEvent(new Event('input'));
195 | }
196 | });
197 |
--------------------------------------------------------------------------------
/test/required_context.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import {Component, DebugElement, ViewChild} from '@angular/core';
18 | import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
19 | import {NoopAnimationsModule} from '@angular/platform-browser/animations';
20 | import {By} from '@angular/platform-browser';
21 |
22 | import {DynamicFormModule} from '../src/lib/src/dynamic_form_module';
23 | import {Entity, NOTNULL_VALUE, NULL_VALUE, Prop, RequiredContext} from '../src/lib/src/meta_datamodel';
24 | import {DynamicFieldPropertyComponent} from '../src/lib/src/prop_component';
25 | import {EntityMetaDataRepository, LookupSources} from '../src/lib/src/repositories';
26 |
27 | import {ExampleLookupSrc} from './example_lookupsrc';
28 |
29 |
30 | /**
31 | * Host component to test prop_component.ts
32 | */
33 | @Component({
34 | preserveWhitespaces: true,
35 | template: `
36 |