;
14 |
15 | let testComponentInputs;
16 |
17 | describe('Formly Form Component', () => {
18 | beforeEach(() => {
19 | TestBed.configureTestingModule({declarations: [TestComponent, FormlyFieldText, FormlyWrapperLabel, RepeatComponent], imports: [FormlyModule.forRoot({
20 | types: [
21 | {
22 | name: 'text',
23 | component: FormlyFieldText,
24 | },
25 | {
26 | name: 'other',
27 | component: FormlyFieldText,
28 | wrappers: ['label'],
29 | },
30 | {
31 | name: 'repeat',
32 | component: RepeatComponent,
33 | },
34 | ],
35 | wrappers: [{
36 | name: 'label',
37 | component: FormlyWrapperLabel,
38 | }],
39 | })]});
40 | });
41 |
42 | it('should initialize inputs with default values', () => {
43 | testComponentInputs = {
44 | fields: [{
45 | fieldGroup: [{
46 | key: 'name',
47 | type: 'text',
48 | }],
49 | }, {
50 | key: 'investments',
51 | type: 'repeat',
52 | fieldArray: {
53 | fieldGroup: [{
54 | key: 'investmentName',
55 | type: 'text',
56 | }],
57 | },
58 | }],
59 | form: new FormGroup({}),
60 | options: {},
61 | model: {
62 | investments: [{investmentName: 'FA'}, {}],
63 | },
64 | };
65 | createTestComponent('');
66 | testComponentInputs.form.controls.investments.removeAt(1);
67 | testComponentInputs.options.resetModel();
68 | });
69 | });
70 |
71 | @Component({selector: 'formly-form-test', template: '', entryComponents: []})
72 | class TestComponent {
73 | fields = testComponentInputs.fields;
74 | form = testComponentInputs.form;
75 | model = testComponentInputs.model || {};
76 | options = testComponentInputs.options;
77 | }
78 |
79 | @Component({
80 | selector: 'formly-repeat-section',
81 | template: `
82 |
83 |
88 |
89 |
90 |
91 | `,
92 | })
93 | export class RepeatComponent extends FieldType implements OnInit {
94 | get newOptions() {
95 | return clone(this.options);
96 | }
97 | get controls() {
98 | return this.form.controls[this.field.key]['controls'];
99 | }
100 |
101 | get fields(): FormlyFieldConfig[] {
102 | return this.field.fieldArray.fieldGroup;
103 | }
104 |
105 | remove(i) {
106 | this.form.controls[this.field.key]['controls'].splice(i, 1);
107 | this.model.splice(i, 1);
108 | }
109 |
110 | ngOnInit() {
111 | if (this.model) {
112 | this.model.map(() => {
113 | let formGroup = new FormGroup({});
114 | this.form.controls[this.field.key]['controls'].push(formGroup);
115 | });
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/app/core/components/formly.form.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnChanges, Input, SimpleChanges, ViewEncapsulation } from '@angular/core';
2 | import { AbstractControl, FormControl, FormGroup, FormArray } from '@angular/forms';
3 | import { FormlyValueChangeEvent } from './../services/formly.event.emitter';
4 | import { FormlyFieldConfig } from './formly.field.config';
5 | import { FormlyFormBuilder } from '../services/formly.form.builder';
6 | import { assignModelValue, isNullOrUndefined, isObject, reverseDeepMerge, getKey, getValueForKey, getFieldModel } from '../utils';
7 |
8 | @Component({
9 | selector: 'formly-form',
10 | template: `
11 |
12 |
17 |
18 |
19 |
20 | `
21 | })
22 | export class FormlyForm implements OnChanges {
23 | @Input() model: any = {};
24 | @Input() form: FormGroup = new FormGroup({});
25 | @Input() fields: FormlyFieldConfig[] = [];
26 | @Input() options: any;
27 | private initialModel: any;
28 |
29 | constructor(private formlyBuilder: FormlyFormBuilder) {}
30 |
31 | ngOnChanges(changes: SimpleChanges) {
32 | if (changes['fields']) {
33 | this.model = this.model || {};
34 | this.form = this.form || (new FormGroup({}));
35 | this.setOptions();
36 | this.formlyBuilder.buildForm(this.form, this.fields, this.model, this.options);
37 | this.updateInitialValue();
38 | } else if (changes['model'] && this.fields && this.fields.length > 0) {
39 | this.form.patchValue(this.model);
40 | }
41 | }
42 |
43 | fieldModel(field: FormlyFieldConfig) {
44 | if (field.key && (field.fieldGroup || field.fieldArray)) {
45 | return getFieldModel(this.model, field, true);
46 | }
47 | return this.model;
48 | }
49 |
50 | changeModel(event: FormlyValueChangeEvent) {
51 | assignModelValue(this.model, event.key, event.value);
52 | }
53 |
54 | setOptions() {
55 | this.options = this.options || {};
56 | this.options.resetModel = this.resetModel.bind(this);
57 | this.options.updateInitialValue = this.updateInitialValue.bind(this);
58 | }
59 |
60 | private resetModel(model?: any) {
61 | model = isNullOrUndefined(model) ? this.initialModel : model;
62 | this.form.patchValue(model);
63 | this.resetFormGroup(model, this.form);
64 | this.resetFormModel(model, this.model);
65 | }
66 |
67 | private resetFormModel(model: any, formModel: any, path?: (string | number)[]) {
68 | if (!isObject(model) && !Array.isArray(model)) {
69 | return;
70 | }
71 |
72 | // removes
73 | for (let key in formModel) {
74 | if (!(key in model) || isNullOrUndefined(model[key])) {
75 | if (!this.form.get((path || []).concat(key))) {
76 | // don't remove if bound to a control
77 | delete formModel[key];
78 | }
79 | }
80 | }
81 |
82 | // inserts and updates
83 | for (let key in model) {
84 | if (!isNullOrUndefined(model[key])) {
85 | if (key in formModel) {
86 | this.resetFormModel(model[key], formModel[key], (path || []).concat(key));
87 | }
88 | else {
89 | formModel[key] = model[key];
90 | }
91 | }
92 | }
93 | }
94 |
95 | private resetFormGroup(model: any, form: FormGroup, actualKey?: string) {
96 | for (let controlKey in form.controls) {
97 | let key = getKey(controlKey, actualKey);
98 | if (form.controls[controlKey] instanceof FormGroup) {
99 | this.resetFormGroup(model, form.controls[controlKey], key);
100 | }
101 | if (form.controls[controlKey] instanceof FormArray) {
102 | this.resetArray(model, form.controls[controlKey], key);
103 | }
104 | if (form.controls[controlKey] instanceof FormControl) {
105 | form.controls[controlKey].setValue(getValueForKey(model, key));
106 | }
107 | }
108 | }
109 |
110 | private resetArray(model: any, formArray: FormArray, key: string) {
111 | let newValue = getValueForKey(model, key);
112 |
113 | // removes and updates
114 | for (let i = formArray.controls.length - 1; i >= 0; i--) {
115 | if (formArray.controls[i] instanceof FormGroup) {
116 | if (newValue && !isNullOrUndefined(newValue[i])) {
117 | this.resetFormGroup(newValue[i], formArray.controls[i]);
118 | }
119 | else {
120 | formArray.removeAt(i);
121 | let value = getValueForKey(this.model, key);
122 | if (Array.isArray(value)) {
123 | value.splice(i, 1);
124 | }
125 | }
126 | }
127 | }
128 |
129 | // inserts
130 | if (Array.isArray(newValue) && formArray.controls.length < newValue.length) {
131 | let remaining = newValue.length - formArray.controls.length;
132 | let initialLength = formArray.controls.length;
133 | for (let i = 0; i < remaining; i++) {
134 | let pos = initialLength + i;
135 | getValueForKey(this.model, key).push(newValue[pos]);
136 | formArray.controls.push(new FormGroup({}));
137 | }
138 | }
139 | }
140 |
141 | private updateInitialValue() {
142 | let obj = reverseDeepMerge(this.form.value, this.model);
143 | this.initialModel = JSON.parse(JSON.stringify(obj));
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/app/core/components/formly.group.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewEncapsulation } from '@angular/core';
2 | import { AbstractControl } from '@angular/forms';
3 | import { FieldType } from '../templates/field.type';
4 | import { clone } from '../utils';
5 |
6 | @Component({
7 | selector: 'formly-group',
8 | template: `
9 |
10 | `
11 | })
12 | export class FormlyGroup extends FieldType {
13 |
14 | get newOptions() {
15 | return clone(this.options);
16 | }
17 |
18 | get formlyGroup(): AbstractControl {
19 | if (this.field.key) {
20 | return this.form.get(this.field.key);
21 | } else {
22 | return this.form;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/core/core.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, ModuleWithProviders, ANALYZE_FOR_ENTRY_COMPONENTS } from '@angular/core';
2 | import { NativeScriptModule, NativeScriptFormsModule } from "nativescript-angular";
3 | import { ReactiveFormsModule } from '@angular/forms';
4 | import { FormlyForm } from './components/formly.form';
5 | import { FormlyFieldConfig } from './components/formly.field.config';
6 | import { FormlyField } from './components/formly.field';
7 | import { FormlyAttributes } from './components/formly.attributes';
8 | import { FormlyConfig, ConfigOption, FORMLY_CONFIG_TOKEN } from './services/formly.config';
9 | import { FormlyFormBuilder } from './services/formly.form.builder';
10 | import { FormlyValidationMessages } from './services/formly.validation-messages';
11 | import { FormlyPubSub, FormlyEventEmitter } from './services/formly.event.emitter';
12 | import { Field } from './templates/field';
13 | import { FieldType } from './templates/field.type';
14 | import { FieldWrapper } from './templates/field.wrapper';
15 | import { FormlyGroup } from './components/formly.group';
16 | import { SingleFocusDispatcher } from './services/formly.single.focus.dispatcher';
17 |
18 | export {
19 | FormlyAttributes,
20 | FormlyFormBuilder,
21 | FormlyField,
22 | FormlyFieldConfig,
23 | FormlyForm,
24 | FormlyConfig,
25 | FormlyPubSub,
26 | FormlyValidationMessages,
27 | FormlyEventEmitter,
28 | SingleFocusDispatcher,
29 |
30 | Field,
31 | FieldType,
32 | FieldWrapper,
33 | };
34 |
35 | const FORMLY_DIRECTIVES = [FormlyForm, FormlyField, FormlyAttributes, FormlyGroup];
36 |
37 | @NgModule({
38 | declarations: FORMLY_DIRECTIVES,
39 | entryComponents: [FormlyGroup],
40 | exports: FORMLY_DIRECTIVES,
41 | imports: [
42 | NativeScriptModule,
43 | ReactiveFormsModule,
44 | NativeScriptFormsModule
45 | ],
46 | })
47 | export class FormlyModule {
48 | static forRoot(config: ConfigOption = {}): ModuleWithProviders {
49 | return {
50 | ngModule: FormlyModule,
51 | providers: [
52 | FormlyFormBuilder,
53 | FormlyConfig,
54 | FormlyPubSub,
55 | FormlyValidationMessages,
56 | { provide: FORMLY_CONFIG_TOKEN, useValue: config, multi: true },
57 | { provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: config, multi: true },
58 | ],
59 | };
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/core/services/formly.config.spec.ts:
--------------------------------------------------------------------------------
1 | import { FormlyConfig } from './formly.config';
2 | import { Validators } from '@angular/forms';
3 | import { Component } from '@angular/core';
4 |
5 | describe('FormlyConfig service', () => {
6 | let config: FormlyConfig;
7 | beforeEach(() => {
8 | config = new FormlyConfig([{
9 | wrappers: [{ name: 'layout', component: TestComponent }],
10 | types: [{ name: 'input' }],
11 | validators: [{ name: 'required', validation: Validators.required }],
12 | }]);
13 | });
14 |
15 | describe('wrappers', () => {
16 | it('should add wrapper', () => {
17 | config.setWrapper({ name: 'custom_wrapper', component: TestComponent });
18 |
19 | expect(config.getWrapper('layout').name).toEqual('layout');
20 | expect(config.getWrapper('custom_wrapper').name).toEqual('custom_wrapper');
21 | });
22 |
23 | it('should throw when wrapper not found', () => {
24 | const config = new FormlyConfig();
25 | expect(() => config.getWrapper('custom_wrapper')).toThrowError('[Formly Error] There is no wrapper by the name of "custom_wrapper"');
26 | });
27 | });
28 |
29 | describe('types', () => {
30 | it('should add type', () => {
31 | config.setType({ name: 'custom_input' });
32 |
33 | expect(config.getType('input').name).toEqual('input');
34 | expect(config.getType('custom_input').name).toEqual('custom_input');
35 | });
36 |
37 | it('should add type as an array', () => {
38 | config.setType([{ name: 'custom_input1' }, { name: 'custom_input2' }]);
39 |
40 | expect(config.getType('custom_input1').name).toEqual('custom_input1');
41 | expect(config.getType('custom_input2').name).toEqual('custom_input2');
42 | });
43 |
44 | it('should throw when type not found', () => {
45 | const config = new FormlyConfig();
46 | expect(() => config.getType('custom_input')).toThrowError('[Formly Error] There is no type by the name of "custom_input"');
47 | });
48 | });
49 |
50 | describe('validators', () => {
51 | it('should add validator', () => {
52 | config.setValidator({ name: 'null', validation: Validators.nullValidator });
53 |
54 | expect(config.getValidator('null').name).toEqual('null');
55 | expect(config.getValidator('required').name).toEqual('required');
56 | });
57 |
58 | it('should throw when validator not found', () => {
59 | const config = new FormlyConfig();
60 | expect(() => config.getValidator('custom_validator')).toThrowError('[Formly Error] There is no validator by the name of "custom_validator"');
61 | });
62 | });
63 | });
64 |
65 | @Component({selector: 'formly-test-cmp', template: '', entryComponents: []})
66 | class TestComponent {
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/core/services/formly.config.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Inject, OpaqueToken } from '@angular/core';
2 | import { FormlyGroup } from '../components/formly.group';
3 | import { reverseDeepMerge } from './../utils';
4 | import { FormlyFieldConfig } from '../components/formly.field.config';
5 |
6 | export const FORMLY_CONFIG_TOKEN = new OpaqueToken('FORMLY_CONFIG_TOKEN');
7 |
8 | /**
9 | * Maintains list of formly field directive types. This can be used to register new field templates.
10 | */
11 | @Injectable()
12 | export class FormlyConfig {
13 | types: {[name: string]: TypeOption} = {
14 | 'formly-group': {
15 | name: 'formly-group',
16 | component: FormlyGroup,
17 | },
18 | };
19 | validators: {[name: string]: ValidatorOption} = {};
20 | wrappers: {[name: string]: WrapperOption} = {};
21 |
22 | public templateManipulators = {
23 | preWrapper: [],
24 | postWrapper: [],
25 | };
26 |
27 | public extras = {
28 | fieldTransform: undefined,
29 | };
30 |
31 | constructor(@Inject(FORMLY_CONFIG_TOKEN) configs: ConfigOption[] = []) {
32 | configs.map(config => {
33 | if (config.types) {
34 | config.types.map(type => this.setType(type));
35 | }
36 | if (config.validators) {
37 | config.validators.map(validator => this.setValidator(validator));
38 | }
39 | if (config.wrappers) {
40 | config.wrappers.map(wrapper => this.setWrapper(wrapper));
41 | }
42 | if (config.manipulators) {
43 | config.manipulators.map(manipulator => this.setManipulator(manipulator));
44 | }
45 | });
46 | }
47 |
48 | setType(options: TypeOption | TypeOption[]) {
49 | if (Array.isArray(options)) {
50 | options.map((option) => {
51 | this.setType(option);
52 | });
53 | } else {
54 | if (!this.types[options.name]) {
55 | this.types[options.name] = {};
56 | }
57 | this.types[options.name].component = options.component;
58 | this.types[options.name].name = options.name;
59 | this.types[options.name].extends = options.extends;
60 | this.types[options.name].defaultOptions = options.defaultOptions;
61 | if (options.wrappers) {
62 | options.wrappers.map((wrapper) => {
63 | this.setTypeWrapper(options.name, wrapper);
64 | });
65 | }
66 | }
67 | }
68 |
69 | getType(name: string): TypeOption {
70 | if (!this.types[name]) {
71 | throw new Error(`[Formly Error] There is no type by the name of "${name}"`);
72 | }
73 |
74 | if (!this.types[name].component && this.types[name].extends) {
75 | this.types[name].component = this.getType(this.types[name].extends).component;
76 | }
77 |
78 | return this.types[name];
79 | }
80 |
81 | getMergedField(field: FormlyFieldConfig = {}): any {
82 | let name = field.type;
83 | if (!this.types[name]) {
84 | throw new Error(`[Formly Error] There is no type by the name of "${name}"`);
85 | }
86 |
87 | if (!this.types[name].component && this.types[name].extends) {
88 | this.types[name].component = this.getType(this.types[name].extends).component;
89 | }
90 |
91 | if (this.types[name].defaultOptions) {
92 | reverseDeepMerge(field, this.types[name].defaultOptions);
93 | }
94 |
95 | let extendDefaults = this.types[name].extends && this.getType(this.types[name].extends).defaultOptions;
96 | if (extendDefaults) {
97 | reverseDeepMerge(field, extendDefaults);
98 | }
99 |
100 | if (field && field.optionsTypes) {
101 | field.optionsTypes.map(option => {
102 | let defaultOptions = this.getType(option).defaultOptions;
103 | if (defaultOptions) {
104 | reverseDeepMerge(field, defaultOptions);
105 | }
106 | });
107 | }
108 | reverseDeepMerge(field, this.types[name]);
109 | }
110 |
111 | setWrapper(options: WrapperOption) {
112 | this.wrappers[options.name] = options;
113 | if (options.types) {
114 | options.types.map((type) => {
115 | this.setTypeWrapper(type, options.name);
116 | });
117 | }
118 | }
119 |
120 | getWrapper(name: string): WrapperOption {
121 | if (!this.wrappers[name]) {
122 | throw new Error(`[Formly Error] There is no wrapper by the name of "${name}"`);
123 | }
124 |
125 | return this.wrappers[name];
126 | }
127 |
128 | setTypeWrapper(type, name) {
129 | if (!this.types[type]) {
130 | this.types[type] = {};
131 | }
132 | if (!this.types[type].wrappers) {
133 | this.types[type].wrappers = <[string]>[];
134 | }
135 | this.types[type].wrappers.push(name);
136 | }
137 |
138 | setValidator(options: ValidatorOption) {
139 | this.validators[options.name] = options;
140 | }
141 |
142 | getValidator(name: string): ValidatorOption {
143 | if (!this.validators[name]) {
144 | throw new Error(`[Formly Error] There is no validator by the name of "${name}"`);
145 | }
146 |
147 | return this.validators[name];
148 | }
149 |
150 | setManipulator(manipulator) {
151 | new manipulator.class()[manipulator.method](this);
152 | }
153 | }
154 |
155 | export interface TypeOption {
156 | name: string;
157 | component?: any;
158 | wrappers?: string[];
159 | extends?: string;
160 | defaultOptions?: any;
161 | }
162 |
163 | export interface WrapperOption {
164 | name: string;
165 | component: any;
166 | types?: string[];
167 | }
168 |
169 | export interface ValidatorOption {
170 | name: string;
171 | validation: any;
172 | }
173 |
174 | export interface ValidationMessageOption {
175 | name: string;
176 | message: any;
177 | }
178 |
179 | export interface ManipulatorsOption {
180 | class?: Function;
181 | method?: string;
182 | }
183 |
184 | export interface ConfigOption {
185 | types?: [TypeOption];
186 | wrappers?: [WrapperOption];
187 | validators?: [ValidatorOption];
188 | validationMessages?: [ValidationMessageOption];
189 | manipulators?: [ManipulatorsOption];
190 | }
191 |
--------------------------------------------------------------------------------
/src/app/core/services/formly.event.emitter.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs/Subject';
2 |
3 | export class FormlyValueChangeEvent {
4 | constructor(public key: string, public value: any) {}
5 | }
6 |
7 | export class FormlyEventEmitter extends Subject {
8 | emit(value) {
9 | super.next(value);
10 | }
11 | }
12 |
13 | export class FormlyPubSub {
14 | emitters = {};
15 |
16 | setEmitter(key, emitter) {
17 | this.emitters[key] = emitter;
18 | }
19 |
20 | getEmitter(key) {
21 | return this.emitters[key];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/core/services/formly.form.builder.spec.ts:
--------------------------------------------------------------------------------
1 | import { FormlyFormBuilder, FormlyConfig, FormlyFieldConfig } from './../core';
2 | import { FormGroup, Validators, FormControl } from '@angular/forms';
3 | import { Component } from '@angular/core';
4 |
5 | describe('FormlyFormBuilder service', () => {
6 | let builder: FormlyFormBuilder,
7 | form: FormGroup,
8 | field: FormlyFieldConfig;
9 | beforeEach(() => {
10 | form = new FormGroup({});
11 | builder = new FormlyFormBuilder(
12 | new FormlyConfig([{
13 | types: [{ name: 'input', component: TestComponent }],
14 | wrappers: [{ name: 'label', component: TestComponent, types: ['input'] }],
15 | validators: [{ name: 'required', validation: Validators.required }],
16 | }]),
17 | );
18 | });
19 |
20 | describe('initialise default TemplateOptions', () => {
21 | it('should not set the default value if the specified key or type is undefined', () => {
22 | field = { key: 'title' };
23 | builder.buildForm(form, [field], {}, {});
24 |
25 | expect(field.templateOptions).toEqual(undefined);
26 | });
27 |
28 | it('should set the default value if the specified key and type is defined', () => {
29 | field = { key: 'title', type: 'input', templateOptions: { placeholder: 'Title' } };
30 | builder.buildForm(form, [field], {}, {});
31 |
32 | expect(field.templateOptions).toEqual({ label: '', placeholder: 'Title', focus: false });
33 | });
34 |
35 | it('should set the default value if the specified key and type is defined for fieldGroup', () => {
36 | field = {
37 | key: 'fieldgroup',
38 | fieldGroup: [{ key: 'title', type: 'input', templateOptions: { placeholder: 'Title' } }],
39 | };
40 | builder.buildForm(form, [field], {}, {});
41 | expect(field.fieldGroup[0].templateOptions).toEqual({ label: '', placeholder: 'Title', focus: false });
42 | });
43 | });
44 |
45 | describe('generate field id', () => {
46 | it('should not generate id if it is defined', () => {
47 | field = { key: 'title', id: 'title_id' };
48 | builder.buildForm(form, [field], {}, {});
49 |
50 | expect(field.id).toEqual('title_id');
51 | });
52 |
53 | it('should generate id if it is not defined', () => {
54 | field = { key: 'title' };
55 | builder.buildForm(form, [field], {}, {});
56 |
57 | expect(field.id).toEqual('formly_1__title_0');
58 | });
59 |
60 | it('should generate an unique id for each form', () => {
61 | let field1 = { key: 'title' },
62 | field2 = { key: 'title' };
63 |
64 | builder.buildForm(form, [field1], {}, {});
65 | builder.buildForm(form, [field2], {}, {});
66 |
67 | expect(field1['id']).not.toEqual(field2['id']);
68 | });
69 | });
70 |
71 | describe('form control creation and addition', () => {
72 | it('should let component create the form control', () => {
73 | let field = { key: 'title', type: 'input', component: new TestComponentThatCreatesControl() };
74 |
75 | builder.buildForm(form, [field], {}, {});
76 |
77 | let control: FormControl = form.get('title');
78 | expect(control).not.toBeNull();
79 | expect(control.value).toEqual('created by component');
80 | });
81 | });
82 |
83 | describe('merge field options', () => {
84 | it('nested property key', () => {
85 | field = { key: 'nested.title', type: 'input' };
86 | builder.buildForm(form, [field], {}, {});
87 |
88 | expect(field.key).toEqual('nested.title');
89 | expect(field.wrappers).toEqual(['label']);
90 | });
91 | });
92 |
93 | describe('initialise field validators', () => {
94 | const expectValidators = (invalidValue, validValue, errors?) => {
95 | const formControl = form.get('title');
96 | expect(typeof field.validators.validation).toBe('function');
97 |
98 | formControl.patchValue(invalidValue);
99 | expect(formControl.valid).toBeFalsy();
100 | if (errors) {
101 | expect(formControl.errors).toEqual(errors);
102 | }
103 |
104 | formControl.patchValue(validValue);
105 | expect(formControl.valid).toBeTruthy();
106 | };
107 |
108 | const expectAsyncValidators = (value) => {
109 | const formControl = form.get('title');
110 | expect(typeof field.asyncValidators.validation).toBe('function');
111 |
112 | formControl.patchValue(value);
113 | expect(formControl.status).toBe('PENDING');
114 | };
115 |
116 | beforeEach(() => {
117 | field = { key: 'title', type: 'input' };
118 | });
119 |
120 | describe('validation.show', () => {
121 | it('should show error when option `show` is true', () => {
122 | field.validators = { validation: ['required'] };
123 | field.validation = { show: true };
124 | builder.buildForm(form, [field], {}, {});
125 |
126 | expect(form.get('title').touched).toBeTruthy();
127 | });
128 |
129 | it('should not show error when option `show` is false', () => {
130 | field.validators = { validation: ['required'] };
131 | field.validation = { show: false };
132 | builder.buildForm(form, [field], {}, {});
133 |
134 | expect(form.get('title').touched).toBeFalsy();
135 | });
136 | });
137 |
138 | describe('validators', () => {
139 | describe('with validation option', () => {
140 | it(`using pre-defined type`, () => {
141 | field.validators = { validation: ['required'] };
142 | builder.buildForm(form, [field], {}, {});
143 |
144 | expectValidators(null, 'test');
145 | });
146 |
147 | it(`using custom type`, () => {
148 | field.validators = { validation: [Validators.required] };
149 | builder.buildForm(form, [field], {}, {});
150 |
151 | expectValidators(null, 'test');
152 | });
153 | });
154 |
155 | describe('without validation option', () => {
156 | it(`using function`, () => {
157 | field.validators = { required: (form) => form.value };
158 | builder.buildForm(form, [field], {}, {});
159 |
160 | expectValidators(null, 'test', {required: true});
161 | });
162 |
163 | it(`using expression property`, () => {
164 | field.validators = {
165 | required: { expression: (form) => form.value },
166 | };
167 | builder.buildForm(form, [field], {}, {});
168 |
169 | expectValidators(null, 'test', {required: true});
170 | });
171 | });
172 | });
173 |
174 | describe('asyncValidators', () => {
175 | it(`uses asyncValidator objects`, () => {
176 | field.asyncValidators = { custom: (control: FormControl) => new Promise(resolve => resolve( control.value !== 'test'))};
177 | builder.buildForm(form, [field], {}, {});
178 |
179 | expectAsyncValidators('test');
180 | });
181 |
182 | it(`uses asyncValidator objects`, () => {
183 | field.asyncValidators = { validation: [(control: FormControl) =>
184 | new Promise(resolve => resolve( control.value !== 'john' ? null : { uniqueUsername: true }))] };
185 | builder.buildForm(form, [field], {}, {});
186 |
187 | expectAsyncValidators('test');
188 | });
189 | });
190 |
191 | describe('using templateOptions', () => {
192 | const options = [
193 | { name: 'required', value: true, valid: 'test', invalid: null },
194 | { name: 'pattern', value: '[0-9]{5}', valid: '75964', invalid: 'ddd' },
195 | { name: 'minLength', value: 5, valid: '12345', invalid: '123' },
196 | { name: 'maxLength', value: 10, valid: '123', invalid: '12345678910' },
197 | { name: 'min', value: 5, valid: 6, invalid: 3 },
198 | { name: 'max', value: 10, valid: 8, invalid: 11 },
199 | ];
200 |
201 | options.map(option => {
202 | it(`${option.name}`, () => {
203 | field.templateOptions = { [option.name]: option.value };
204 | builder.buildForm(form, [field], {}, {});
205 |
206 | expectValidators(option.invalid, option.valid);
207 | });
208 | });
209 | });
210 | });
211 | });
212 |
213 | @Component({selector: 'formly-test-cmp', template: '', entryComponents: []})
214 | class TestComponent {
215 | }
216 |
217 | class TestComponentThatCreatesControl {
218 |
219 | createControl(model, field) {
220 | return new FormControl('created by component');
221 | }
222 |
223 | }
--------------------------------------------------------------------------------
/src/app/core/services/formly.form.builder.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { FormGroup, FormArray, FormControl, Validators } from '@angular/forms';
3 | import { FormlyConfig } from './formly.config';
4 | import { evalStringExpression, evalExpressionValueSetter, getFieldId, assignModelValue, isObject } from './../utils';
5 | import { FormlyFieldConfig } from '../components/formly.field.config';
6 |
7 | @Injectable()
8 | export class FormlyFormBuilder {
9 | private defaultPath;
10 | private validationOpts = ['required', 'pattern', 'minLength', 'maxLength', 'min', 'max'];
11 | private formId = 0;
12 | private model;
13 |
14 | constructor(private formlyConfig: FormlyConfig) {}
15 |
16 | buildForm(form: FormGroup, fields: FormlyFieldConfig[] = [], model, options) {
17 | this.model = model;
18 | this.formId++;
19 | let fieldTransforms = (options && options.fieldTransform) || this.formlyConfig.extras.fieldTransform;
20 | if (!Array.isArray(fieldTransforms)) {
21 | fieldTransforms = [fieldTransforms];
22 | }
23 |
24 | fieldTransforms.forEach(fieldTransform => {
25 | if (fieldTransform) {
26 | fields = fieldTransform(fields, model, form, options);
27 | if (!fields) {
28 | throw new Error('fieldTransform must return an array of fields');
29 | }
30 | }
31 | });
32 |
33 | this.registerFormControls(form, fields, model, options);
34 | }
35 |
36 | private registerFormControls(form: FormGroup, fields: FormlyFieldConfig[], model, options) {
37 | fields.map((field, index) => {
38 | field.id = getFieldId(`formly_${this.formId}`, field, index);
39 | if (field.key && field.type) {
40 | this.initFieldTemplateOptions(field);
41 | this.initFieldValidation(field);
42 | this.initFieldAsyncValidation(field);
43 |
44 |
45 | let path: any = field.key;
46 | if (typeof path === 'string') {
47 | if (field.defaultValue) {
48 | this.defaultPath = path;
49 | }
50 | path = path.split('.');
51 | }
52 |
53 | if (path.length > 1) {
54 | const rootPath = path.shift();
55 | let nestedForm = (form.get(rootPath) ? form.get(rootPath) : new FormGroup({}, field.validators ? field.validators.validation : undefined, field.asyncValidators ? field.asyncValidators.validation : undefined));
56 | if (!form.get(rootPath)) {
57 | form.addControl(rootPath, nestedForm);
58 | }
59 | if (!model[rootPath]) {
60 | model[rootPath] = isNaN(rootPath) ? {} : [];
61 | }
62 |
63 | const originalKey = field.key;
64 | // Should this reassignment not be refactored?
65 | field.key = path;
66 | this.buildForm(nestedForm, [field], model[rootPath], {});
67 | field.key = originalKey;
68 | } else {
69 |
70 | this.formlyConfig.getMergedField(field);
71 | this.initFieldExpression(field);
72 | this.initFieldValidation(field);
73 | this.initFieldAsyncValidation(field);
74 | this.addFormControl(form, field, model[path[0]] || field.defaultValue || '');
75 | if (field.defaultValue && !model[path[0]]) {
76 | let path = this.defaultPath.split('.');
77 | path = path.pop();
78 | assignModelValue(this.model, path, field.defaultValue);
79 | this.defaultPath = undefined;
80 | }
81 | }
82 | }
83 |
84 | if (field.fieldGroup) {
85 | if (field.key) {
86 | let nestedForm = form.get(field.key),
87 | nestedModel = model[field.key] || {};
88 |
89 | if (!nestedForm) {
90 | nestedForm = new FormGroup(
91 | {},
92 | field.validators ? field.validators.validation : undefined,
93 | field.asyncValidators ? field.asyncValidators.validation : undefined,
94 | );
95 | form.addControl(field.key, nestedForm);
96 | }
97 |
98 | this.buildForm(nestedForm, field.fieldGroup, nestedModel, {});
99 | } else {
100 | this.buildForm(form, field.fieldGroup, model, {});
101 | }
102 | }
103 |
104 | if (field.fieldArray && field.key) {
105 | if (!(form.get(field.key) instanceof FormArray)) {
106 | const arrayForm = new FormArray(
107 | [],
108 | field.validators ? field.validators.validation : undefined,
109 | field.asyncValidators ? field.asyncValidators.validation : undefined,
110 | );
111 | form.setControl(field.key, arrayForm);
112 | }
113 | }
114 | });
115 | }
116 |
117 | private initFieldExpression(field: FormlyFieldConfig) {
118 | if (field.expressionProperties) {
119 | for (let key in field.expressionProperties) {
120 | if (typeof field.expressionProperties[key] === 'string') {
121 | // cache built expression
122 | field.expressionProperties[key] = {
123 | expression: evalStringExpression(field.expressionProperties[key], ['model', 'formState']),
124 | expressionValueSetter: evalExpressionValueSetter(key, ['expressionValue', 'model', 'templateOptions', 'validation']),
125 | };
126 | }
127 | }
128 | }
129 |
130 | if (typeof field.hideExpression === 'string') {
131 | // cache built expression
132 | field.hideExpression = evalStringExpression(field.hideExpression, ['model', 'formState']);
133 | }
134 | }
135 |
136 | private initFieldTemplateOptions(field: FormlyFieldConfig) {
137 | field.templateOptions = (Object).assign({
138 | label: '',
139 | placeholder: '',
140 | focus: false,
141 | }, field.templateOptions);
142 | }
143 |
144 | private initFieldAsyncValidation(field: FormlyFieldConfig) {
145 | let validators = [];
146 | if (field.asyncValidators) {
147 | for (let validatorName in field.asyncValidators) {
148 | if (validatorName !== 'validation') {
149 | validators.push((control: FormControl) => {
150 | let validator = field.asyncValidators[validatorName];
151 | if (isObject(validator)) {
152 | validator = validator.expression;
153 | }
154 |
155 | return new Promise((resolve) => {
156 | return validator(control).then(result => {
157 | resolve(result ? null : {[validatorName]: true});
158 | });
159 | });
160 | });
161 | }
162 | }
163 | }
164 | if (field.asyncValidators && Array.isArray(field.asyncValidators.validation)) {
165 | field.asyncValidators.validation.map(validate => {
166 | if (typeof validate === 'string') {
167 | validators.push(this.formlyConfig.getValidator(validate).validation);
168 | } else {
169 | validators.push(validate);
170 | }
171 | });
172 | }
173 |
174 | if (validators.length) {
175 | if (field.asyncValidators && !Array.isArray(field.asyncValidators.validation)) {
176 | field.asyncValidators.validation = Validators.composeAsync([field.asyncValidators.validation, ...validators]);
177 | } else {
178 | field.asyncValidators = {
179 | validation: Validators.composeAsync(validators),
180 | };
181 | }
182 | }
183 | }
184 |
185 | private initFieldValidation(field: FormlyFieldConfig) {
186 | let validators = [];
187 | this.validationOpts.filter(opt => field.templateOptions[opt]).map((opt) => {
188 | validators.push(this.getValidation(opt, field.templateOptions[opt]));
189 | });
190 | if (field.validators) {
191 | for (let validatorName in field.validators) {
192 | if (validatorName !== 'validation') {
193 | validators.push((control: FormControl) => {
194 | let validator = field.validators[validatorName];
195 | if (isObject(validator)) {
196 | validator = validator.expression;
197 | }
198 |
199 | return validator(control) ? null : {[validatorName]: true};
200 | });
201 | }
202 | }
203 | }
204 |
205 | if (field.validators && Array.isArray(field.validators.validation)) {
206 | field.validators.validation.map(validate => {
207 | if (typeof validate === 'string') {
208 | validators.push(this.formlyConfig.getValidator(validate).validation);
209 | } else {
210 | validators.push(validate);
211 | }
212 | });
213 | }
214 |
215 | if (validators.length) {
216 | if (field.validators && !Array.isArray(field.validators.validation)) {
217 | field.validators.validation = Validators.compose([field.validators.validation, ...validators]);
218 | } else {
219 | field.validators = {
220 | validation: Validators.compose(validators),
221 | };
222 | }
223 | }
224 | }
225 |
226 | private addFormControl(form: FormGroup, field: FormlyFieldConfig, model) {
227 | /* Although the type of the key property in FormlyFieldConfig is declared to be a string,
228 | the recurstion of this FormBuilder uses an Array.
229 | This should probably be addressed somehow. */
230 | let name: string = typeof field.key === 'string' ? field.key : field.key[0];
231 | if (field.component && field.component.createControl) {
232 | form.addControl(name, field.component.createControl(model, field));
233 | } else {
234 | form.addControl(name, new FormControl(
235 | { value: model, disabled: field.templateOptions.disabled },
236 | field.validators ? field.validators.validation : undefined,
237 | field.asyncValidators ? field.asyncValidators.validation : undefined,
238 | ));
239 | }
240 | if (field.validation && field.validation.show) {
241 | form.get(field.key).markAsTouched();
242 | }
243 | }
244 |
245 | private getValidation(opt, value) {
246 | switch (opt) {
247 | case this.validationOpts[0]:
248 | return Validators[opt];
249 | case this.validationOpts[1]:
250 | case this.validationOpts[2]:
251 | case this.validationOpts[3]:
252 | return Validators[opt](value);
253 | case this.validationOpts[4]:
254 | case this.validationOpts[5]:
255 | return (changes) => {
256 | if (this.checkMinMax(opt, changes.value, value)) {
257 | return null;
258 | } else {
259 | return {[opt]: true};
260 | }
261 | };
262 | }
263 | }
264 |
265 | private checkMinMax(opt, changes, value) {
266 | if (opt === this.validationOpts[4]) {
267 | return parseInt(changes) > value;
268 | } else {
269 | return parseInt(changes) < value;
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/src/app/core/services/formly.messages.spec.ts:
--------------------------------------------------------------------------------
1 | import { FormlyValidationMessages } from './formly.validation-messages';
2 |
3 | describe('FormlyValidationMessages service', () => {
4 | let formlyMessages: FormlyValidationMessages;
5 | beforeEach(() => {
6 | formlyMessages = new FormlyValidationMessages([{
7 | validationMessages: [
8 | { name: 'required', message: 'This field is required.' },
9 | ],
10 | }]);
11 | });
12 |
13 | it('get validator error message', () => {
14 | expect(formlyMessages.getValidatorErrorMessage('required')).toEqual('This field is required.');
15 | expect(formlyMessages.getValidatorErrorMessage('maxlength')).toEqual(undefined);
16 | });
17 |
18 | it('add validator error message', () => {
19 | formlyMessages.addStringMessage('maxlength', 'Maximum Length Exceeded.');
20 | expect(formlyMessages.getValidatorErrorMessage('maxlength')).toEqual('Maximum Length Exceeded.');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/app/core/services/formly.single.focus.dispatcher.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | export type SingleFocusDispatcherListener = (key: string) => void;
3 |
4 | /**
5 | * Class to coordinate single focus based on key.
6 | * Intended to be consumed as an Angular service.
7 | * This service ensures that 'focus' is true for single field and, and also focus out on previous focused field.
8 | */
9 | @Injectable()
10 | export class SingleFocusDispatcher {
11 | private _listeners: SingleFocusDispatcherListener[] = [];
12 |
13 | /** Notify other items that focus for the given key has been set. */
14 | notify(key: string) {
15 | for (let listener of this._listeners) {
16 | listener(key);
17 | }
18 | }
19 |
20 | /** Listen for future changes to item selection. */
21 | listen(listener: SingleFocusDispatcherListener) {
22 | this._listeners.push(listener);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/core/services/formly.validation-messages.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@angular/core';
2 | import { FORMLY_CONFIG_TOKEN } from './formly.config';
3 |
4 | @Injectable()
5 | export class FormlyValidationMessages {
6 | messages = {};
7 |
8 | constructor(@Inject(FORMLY_CONFIG_TOKEN) configs = []) {
9 | configs.map(config => {
10 | if (config.validationMessages) {
11 | config.validationMessages.map(validation => this.addStringMessage(validation.name, validation.message));
12 | }
13 | });
14 | }
15 |
16 | addStringMessage(validator, message) {
17 | this.messages[validator] = message;
18 | }
19 |
20 | getMessages() {
21 | return this.messages;
22 | }
23 |
24 | getValidatorErrorMessage(prop) {
25 | return this.messages[prop];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/core/templates/field.ts:
--------------------------------------------------------------------------------
1 | import { Input } from '@angular/core';
2 | import { FormGroup, AbstractControl } from '@angular/forms';
3 | import { FormlyTemplateOptions, FormlyFieldConfig } from '../components/formly.field.config';
4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject';
5 |
6 | export abstract class Field {
7 | @Input() form: FormGroup;
8 | @Input() field: FormlyFieldConfig;
9 | @Input() model: any;
10 | @Input() options;
11 |
12 | valueChanges: BehaviorSubject;
13 |
14 | get key() { return this.field.key; }
15 | get formControl(): AbstractControl { return this.form.get(this.key); }
16 |
17 | /**
18 | * @deprecated Use `to` instead.
19 | **/
20 | get templateOptions(): FormlyTemplateOptions {
21 | console.warn(`${this.constructor['name']}: 'templateOptions' is deprecated. Use 'to' instead.`);
22 |
23 | return this.to;
24 | }
25 |
26 | get to(): FormlyTemplateOptions { return this.field.templateOptions; }
27 |
28 | get valid(): boolean { return this.formControl.touched && !this.formControl.valid; }
29 |
30 | get id(): string { return this.field.id; }
31 |
32 | get formState() { return this.options.formState || {}; }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/core/templates/field.type.ts:
--------------------------------------------------------------------------------
1 | import { Field } from './field';
2 | import { OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy, AfterViewChecked } from '@angular/core';
3 |
4 | export abstract class FieldType extends Field implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
5 |
6 | ngOnInit() {
7 | this.lifeCycleHooks('onInit');
8 | }
9 |
10 | ngOnChanges(changes) {
11 | this.lifeCycleHooks('onChanges');
12 | }
13 |
14 | ngDoCheck() {
15 | this.lifeCycleHooks('doCheck');
16 | }
17 |
18 | ngAfterContentInit() {
19 | this.lifeCycleHooks('afterContentInit');
20 | }
21 |
22 | ngAfterContentChecked() {
23 | this.lifeCycleHooks('afterContentChecked');
24 | }
25 |
26 | ngAfterViewInit() {
27 | this.lifeCycleHooks('afterViewInit');
28 | }
29 |
30 | ngAfterViewChecked() {
31 | this.lifeCycleHooks('afterViewChecked');
32 | }
33 |
34 | ngOnDestroy() {
35 | this.lifeCycleHooks('onDestroy');
36 | }
37 |
38 | private get lifecycle() {
39 | return this.field.lifecycle;
40 | }
41 |
42 | private lifeCycleHooks(type) {
43 | if (this.lifecycle && this.lifecycle[type]) {
44 | this.lifecycle[type].bind(this)(this.form, this.field, this.model, this.options);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/core/templates/field.wrapper.ts:
--------------------------------------------------------------------------------
1 | import { ViewContainerRef } from '@angular/core';
2 | import { Field } from './field';
3 |
4 | export abstract class FieldWrapper extends Field {
5 | fieldComponent: ViewContainerRef;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/core/test-utils.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, ComponentFixture } from '@angular/core/testing';
2 |
3 | export function createGenericTestComponent(html: string, type: {new (...args: any[]): T}): ComponentFixture {
4 | TestBed.overrideComponent(type, {set: {template: html}});
5 | const fixture = TestBed.createComponent(type);
6 | fixture.detectChanges();
7 | return fixture as ComponentFixture;
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/core/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { reverseDeepMerge, assignModelValue, getFieldId, getValueForKey, getKey, evalExpression, getKeyPath, getFieldModel } from './utils';
2 | import { FormlyFieldConfig } from './components/formly.field.config';
3 |
4 | describe('FormlyUtils service', () => {
5 | describe('reverseDeepMerge', () => {
6 | it('should properly reverse deep merge', () => {
7 | let foo = {foo: 'bar', obj: {}};
8 | let bar = {foo: 'foo', foobar: 'foobar', fun: () => console.log('demo'), obj: {}};
9 | reverseDeepMerge(foo, bar);
10 |
11 | expect(foo['foo']).toEqual('bar');
12 | expect(foo['foobar']).toEqual('foobar');
13 | });
14 | });
15 |
16 | describe('assignModelValue', () => {
17 | it('should properly assign model value', () => {
18 | let model = {};
19 | assignModelValue(model, 'path.to.save', 2);
20 | expect(model['path']['to']['save']).toBe(2);
21 | });
22 | });
23 |
24 | describe('getValueForKey', () => {
25 | it('should properly get value', () => {
26 | let model = {
27 | value: 2,
28 | 'looks.nested': 'foo',
29 | nested: {
30 | value: 'bar',
31 | },
32 | };
33 | expect(getValueForKey(model, 'path.to.save')).toBe(undefined);
34 | expect(getValueForKey(model, 'value')).toBe(2);
35 | expect(getValueForKey(model, 'looks.nested')).toBe(undefined);
36 | expect(getValueForKey(model, 'nested.value')).toBe('bar');
37 | });
38 | });
39 |
40 | describe('getKey', () => {
41 | it('should properly get key', () => {
42 | expect(getKey('key', 'path.to.save')).toBe('path.to.save.key');
43 | expect(getKey('key', undefined)).toBe('key');
44 | });
45 | });
46 |
47 | describe('getFieldId', () => {
48 | it('should properly get the field id if id is set in options', () => {
49 | let options: FormlyFieldConfig = {id: '1'};
50 | let id = getFieldId('formly_1', options, 2);
51 | expect(id).toBe('1');
52 | });
53 |
54 | it('should properly get the field id if id is not set in options', () => {
55 | let options: FormlyFieldConfig = {type: 'input', key: 'email'};
56 | let id = getFieldId('formly_1', options, 2);
57 | expect(id).toBe('formly_1_input_email_2');
58 | });
59 | });
60 |
61 | describe('getKeyPath', () => {
62 |
63 | it('should get an empty key path for an empty key', () => {
64 | let keyPath = getKeyPath({});
65 | expect(keyPath).toEqual([]);
66 | keyPath = getKeyPath({key: null});
67 | expect(keyPath).toEqual([]);
68 | keyPath = getKeyPath({key: ''});
69 | expect(keyPath).toEqual([]);
70 | });
71 |
72 | it('should get the correct key path of length 1 for a simple string', () => {
73 | let keyPath = getKeyPath({key: 'property'});
74 | expect(keyPath).toEqual(['property']);
75 | });
76 |
77 | it('should get the correct key path of length 2 for a simple string with an index', () => {
78 | let keyPath = getKeyPath({key: 'property[2]'});
79 | expect(keyPath).toEqual(['property', 2]);
80 | });
81 |
82 | it('should get the correct key path of length 3 for a simple nested property', () => {
83 | let keyPath = getKeyPath({key: 'property1.property2.property3'});
84 | expect(keyPath).toEqual(['property1', 'property2', 'property3']);
85 | });
86 |
87 | it('should get the correct key path of length 4 with one index for a nested property containing 1 index property', () => {
88 | let keyPath = getKeyPath({key: 'property1.property2[4].property3'});
89 | expect(keyPath).toEqual(['property1', 'property2', 4, 'property3']);
90 | });
91 |
92 | it('should get the correct key path of length 5 with one index for a complex array key', () => {
93 | let keyPath = getKeyPath({key: ['property1.property2[4].property3', 'property4']});
94 | expect(keyPath).toEqual(['property1', 'property2', 4, 'property3', 'property4']);
95 | });
96 |
97 | it('should get the correct key path if the path contains a numeric path element', () => {
98 | let keyPath = getKeyPath({key: ['property1.2.property2']});
99 | expect(keyPath).toEqual(['property1', 2, 'property2']);
100 | });
101 |
102 | it('should attach the key path to the field config', () => {
103 | let fieldConfig = {key: 'property1.property2[4].property3'};
104 | getKeyPath(fieldConfig);
105 | expect(fieldConfig['_formlyKeyPath']).toEqual(['property1', 'property2', 4, 'property3']);
106 | });
107 |
108 | });
109 |
110 | });
111 |
112 |
113 | describe ('getFieldModel', () => {
114 |
115 | it('should extract te correct simple property', () => {
116 |
117 | let config: FormlyFieldConfig = {key: 'property1'};
118 | let model: any = {property1: 3};
119 | let fieldModel: any = getFieldModel(model, config, true);
120 | expect(fieldModel).toEqual(3);
121 |
122 | });
123 |
124 |
125 | it('should extract te correct nested property', () => {
126 |
127 | let config: FormlyFieldConfig = {key: 'property1.property2[2]'};
128 | let model: any = {property1: {property2: [1, 1, 2]}};
129 | let fieldModel: any = getFieldModel(model, config, true);
130 | expect(fieldModel).toEqual(2);
131 |
132 | config = {key: 'property1.property2[2].property3'};
133 | model = {property1: {property2: [1, 1, {property3: 'test'}]}};
134 | fieldModel = getFieldModel(model, config, true);
135 | expect(fieldModel).toEqual('test');
136 |
137 | config = {key: 'property1.property2.property3'};
138 | model = {property1: {property2: {property3: 'test'}}};
139 | fieldModel = getFieldModel(model, config, true);
140 | expect(fieldModel).toEqual('test');
141 |
142 |
143 | });
144 |
145 | it('should create the necessary empty objects in a simple property path', () => {
146 |
147 | let config: FormlyFieldConfig = {key: 'property1'};
148 | let model: any = {};
149 | getFieldModel(model, config, true);
150 | expect(model).toEqual({});
151 |
152 | config = {key: 'property1', fieldGroup: []};
153 | model = {};
154 | getFieldModel(model, config, true);
155 | expect(model).toEqual({property1: {}});
156 |
157 | config = {key: 'property1', fieldArray: {}};
158 | model = {};
159 | getFieldModel(model, config, true);
160 | expect(model).toEqual({property1: []});
161 |
162 | });
163 |
164 | it('should create the necessary empty objects in a nested property path', () => {
165 |
166 | let config: FormlyFieldConfig = {key: 'property1.property2'};
167 | let model: any = {};
168 | getFieldModel(model, config, true);
169 | expect(model).toEqual({property1: {}});
170 |
171 | config = {key: 'property1.property2', fieldGroup: []};
172 | model = {};
173 | getFieldModel(model, config, true);
174 | expect(model).toEqual({property1: {property2: {}}});
175 |
176 | config = {key: 'property1.property2', fieldArray: {}};
177 | model = {};
178 | getFieldModel(model, config, true);
179 | expect(model).toEqual({property1: {property2: []}});
180 |
181 | config = {key: 'property1.property2.property3'};
182 | model = {};
183 | getFieldModel(model, config, true);
184 | expect(model).toEqual({property1: {property2: {}}});
185 |
186 | config = {key: 'property1.property2.property3', fieldGroup: []};
187 | model = {};
188 | getFieldModel(model, config, true);
189 | expect(model).toEqual({property1: {property2: {property3: {}}}});
190 |
191 | config = {key: 'property1.property2.property3', fieldArray: {}};
192 | model = {};
193 | getFieldModel(model, config, true);
194 | expect(model).toEqual({property1: {property2: {property3: []}}});
195 |
196 | config = {key: 'property1.property2[2]'};
197 | model = {};
198 | getFieldModel(model, config, true);
199 | expect(model).toEqual({property1: {property2: []}});
200 |
201 | config = {key: 'property1.property2[2]', fieldGroup: []};
202 | model = {};
203 | getFieldModel(model, config, true);
204 | expect(model).toEqual({property1: {property2: [undefined, undefined, {}]}});
205 |
206 | config = {key: 'property1.property2[2]', fieldArray: {}};
207 | model = {};
208 | getFieldModel(model, config, true);
209 | expect(model).toEqual({property1: {property2: [undefined, undefined, []]}});
210 |
211 | config = {key: 'property1.property2[2].property3', fieldGroup: []};
212 | model = {};
213 | getFieldModel(model, config, true);
214 | expect(model).toEqual({property1: {property2: [undefined, undefined, {property3: {}}]}});
215 |
216 | config = {key: 'property1.property2[2].property3', fieldArray: {}};
217 | model = {};
218 | getFieldModel(model, config, true);
219 | expect(model).toEqual({property1: {property2: [undefined, undefined, {property3: []}]}});
220 |
221 | config = {key: 'property1.property2[2].property3'};
222 | model = {};
223 | getFieldModel(model, config, true);
224 | expect(model).toEqual({property1: {property2: [undefined, undefined, {}]}});
225 |
226 | });
227 |
228 | describe('evalExpression', () => {
229 | it('should evaluate the value correctly', () => {
230 | let expression = () => { return this.model.val; };
231 | this.model = {
232 | val: 2,
233 | };
234 | expect(evalExpression(expression, this, [this.model])).toBe(2);
235 | });
236 | });
237 | });
238 |
--------------------------------------------------------------------------------
/src/app/core/utils.ts:
--------------------------------------------------------------------------------
1 | import { FormlyFieldConfig } from './core';
2 |
3 | export function getFieldId(formId: string, options: FormlyFieldConfig, index: string|number) {
4 | if (options.id) return options.id;
5 | let type = options.type;
6 | if (!type && options.template) type = 'template';
7 | return [formId, type, options.key, index].join('_');
8 | }
9 |
10 | export function getKeyPath(field: {key?: string|string[], fieldGroup?: any, fieldArray?: any}): (string|number)[] {
11 | /* We store the keyPath in the field for performance reasons. This function will be called frequently. */
12 | if (field['_formlyKeyPath'] !== undefined) {
13 | return field['_formlyKeyPath'];
14 | }
15 | let keyPath: (string|number)[] = [];
16 | if (field.key) {
17 | /* Also allow for an array key, hence the type check */
18 | let pathElements = typeof field.key === 'string' ? field.key.split('.') : field.key;
19 | for (let pathElement of pathElements) {
20 | if (typeof pathElement === 'string') {
21 | /* replace paths of the form names[2] by names.2, cfr. angular formly */
22 | pathElement = pathElement.replace(/\[(\w+)\]/g, '.$1');
23 | keyPath = keyPath.concat(pathElement.split('.'));
24 | } else {
25 | keyPath.push(pathElement);
26 | }
27 | }
28 | for (let i = 0; i < keyPath.length; i++) {
29 | let pathElement = keyPath[i];
30 | if (typeof pathElement === 'string' && stringIsInteger(pathElement)) {
31 | keyPath[i] = parseInt(pathElement);
32 | }
33 | }
34 | }
35 | field['_formlyKeyPath'] = keyPath;
36 | return keyPath;
37 | }
38 |
39 | function stringIsInteger(str: string) {
40 | return !isNullOrUndefined(str) && /^\d+$/.test(str);
41 | }
42 |
43 | export function getFieldModel(model: any, field: FormlyFieldConfig, constructEmptyObjects: boolean): any {
44 | let keyPath: (string|number)[] = getKeyPath(field);
45 | let value: any = model;
46 | for (let i = 0; i < keyPath.length; i++) {
47 | let path = keyPath[i];
48 | let pathValue = value[path];
49 | if (isNullOrUndefined(pathValue) && constructEmptyObjects) {
50 | if (i < keyPath.length - 1) {
51 | /* TODO? : It would be much nicer if we could construct object instances of the correct class, for instance by using factories. */
52 | value[path] = typeof keyPath[i + 1] === 'number' ? [] : {};
53 | } else if (field.fieldGroup) {
54 | value[path] = {};
55 | } else if (field.fieldArray) {
56 | value[path] = [];
57 | }
58 | }
59 | value = value[path];
60 | if (!value) {
61 | break;
62 | }
63 | }
64 | return value;
65 | }
66 |
67 | export function assignModelValue(model, path, value) {
68 | if (typeof path === 'string') {
69 | path = path.split('.');
70 | }
71 |
72 | if (path.length > 1) {
73 | const e = path.shift();
74 | if (!model[e]) {
75 | model[e] = isNaN(path[0]) ? {} : [];
76 | }
77 | assignModelValue(model[e], path, value);
78 | } else {
79 | model[path[0]] = value;
80 | }
81 | }
82 |
83 | export function getValueForKey(model, path) {
84 | if (typeof path === 'string') {
85 | path = path.split('.');
86 | }
87 | if (path.length > 1) {
88 | const e = path.shift();
89 | if (!model[e]) {
90 | model[e] = isNaN(path[0]) ? {} : [];
91 | }
92 | return getValueForKey(model[e], path);
93 | } else {
94 | return model[path[0]];
95 | }
96 | }
97 |
98 | export function getKey(controlKey: string, actualKey: string) {
99 | return actualKey ? actualKey + '.' + controlKey : controlKey;
100 | }
101 |
102 | export function reverseDeepMerge(dest, source = undefined) {
103 | let args = Array.prototype.slice.call(arguments);
104 | if (!args[1]) {
105 | return dest;
106 | }
107 | args.forEach((src, index) => {
108 | if (!index) {
109 | return;
110 | }
111 | for (let srcArg in src) {
112 | if (isNullOrUndefined(dest[srcArg]) || isBlankString(dest[srcArg])) {
113 | if (isFunction(src[srcArg])) {
114 | dest[srcArg] = src[srcArg];
115 | } else {
116 | dest[srcArg] = clone(src[srcArg]);
117 | }
118 | } else if (objAndSameType(dest[srcArg], src[srcArg])) {
119 | reverseDeepMerge(dest[srcArg], src[srcArg]);
120 | }
121 | }
122 | });
123 | return dest;
124 | }
125 |
126 | export function isNullOrUndefined(value) {
127 | return value === undefined || value === null;
128 | }
129 |
130 | export function isBlankString(value) {
131 | return value === '';
132 | }
133 |
134 | export function isFunction(value) {
135 | return typeof(value) === 'function';
136 | }
137 |
138 | export function objAndSameType(obj1, obj2) {
139 | return isObject(obj1) && isObject(obj2) &&
140 | Object.getPrototypeOf(obj1) === Object.getPrototypeOf(obj2);
141 | }
142 |
143 | export function isObject(x) {
144 | return x != null && typeof x === 'object';
145 | }
146 |
147 | export function clone(value) {
148 | if (!isObject(value)) {
149 | return value;
150 | }
151 | return Array.isArray(value) ? value.slice(0) : (Object).assign({}, value);
152 | }
153 |
154 | export function evalStringExpression(expression: string, argNames: string[]) {
155 | try {
156 | return Function.bind.apply(Function, [void 0].concat(argNames.concat(`return ${expression};`)))();
157 | } catch (error) {
158 | console.error(error);
159 | }
160 | }
161 |
162 | export function evalExpressionValueSetter(expression: string, argNames: string[]) {
163 | try {
164 | return Function.bind
165 | .apply(Function, [void 0].concat(argNames.concat(`${expression} = expressionValue;`)))();
166 | } catch (error) {
167 | console.error(error);
168 | }
169 | }
170 |
171 | export function evalExpression(expression: string | Function | boolean, thisArg: any, argVal: any[]): boolean {
172 | if (expression instanceof Function) {
173 | return expression.apply(thisArg, argVal);
174 | } else {
175 | return expression ? true : false;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/app/index.ts:
--------------------------------------------------------------------------------
1 | export * from './core/core'
2 | export * from './ui-bootstrap/ui-bootstrap'
3 |
--------------------------------------------------------------------------------
/src/app/main.ts:
--------------------------------------------------------------------------------
1 | import { platformNativeScriptDynamic, NativeScriptModule, NativeScriptFormsModule } from "nativescript-angular";
2 | // angular
3 | import { NgModule } from "@angular/core";
4 |
5 | import { DemoComponent } from './app.component';
6 |
7 | import { FormlyModule, FormlyBootstrapModule } from "ng-formly-nativescript";
8 |
9 |
10 | @NgModule({
11 | imports: [
12 | NativeScriptModule,
13 | NativeScriptFormsModule,
14 | FormlyModule.forRoot(),
15 | FormlyBootstrapModule
16 | ],
17 | declarations: [
18 | DemoComponent,
19 | ],
20 | bootstrap: [
21 | DemoComponent
22 | ],
23 | })
24 | class DemoModule { }
25 |
26 | platformNativeScriptDynamic().bootstrapModule(DemoModule);
27 |
--------------------------------------------------------------------------------
/src/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": false,
3 | "nativescript": {
4 | "id": "org.nativescript.helloworldng"
5 | },
6 | "name": "tns-template-hello-world-ng",
7 | "main": "main.js",
8 | "version": "2.3.3",
9 | "author": "Telerik ",
10 | "description": "Nativescript Angular Hello World template",
11 | "license": "BSD",
12 | "keywords": [
13 | "telerik",
14 | "mobile",
15 | "angular",
16 | "nativescript",
17 | "{N}",
18 | "tns",
19 | "appbuilder",
20 | "template"
21 | ],
22 | "repository": {
23 | "type": "git",
24 | "url": "git://github.com/NativeScript/template-hello-world-ng"
25 | },
26 | "homepage": "https://github.com/NativeScript/template-hello-world-ng",
27 | "android": {
28 | "v8Flags": "--expose_gc"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/formly.validation-message.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, ComponentFixture } from '@angular/core/testing';
2 | import { createGenericTestComponent } from '../core/test-utils';
3 |
4 | import { Component } from '@angular/core';
5 | import { FormControl, Validators } from '@angular/forms';
6 | import { FormlyModule, FormlyFieldConfig } from '../core/core';
7 | import { FormlyValidationMessage } from './formly.validation-message';
8 |
9 | const createTestComponent = (html: string) =>
10 | createGenericTestComponent(html, TestComponent) as ComponentFixture;
11 |
12 | function getFormlyValidationMessageElement(element: HTMLElement): HTMLDivElement {
13 | return element.querySelector('formly-validation-message');
14 | }
15 |
16 | describe('FormlyValidationMessage Component', () => {
17 | beforeEach(() => {
18 | TestBed.configureTestingModule({
19 | declarations: [FormlyValidationMessage, TestComponent],
20 | imports: [
21 | FormlyModule.forRoot({
22 | validationMessages: [
23 | { name: 'required', message: (err, field) => `${field.templateOptions.label} is required.`},
24 | { name: 'maxlength', message: 'Maximum Length Exceeded.' },
25 | ],
26 | }),
27 | ],
28 | });
29 | });
30 |
31 | it('should not render message with a valid value', () => {
32 | const fixture = createTestComponent('');
33 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement);
34 | fixture.componentInstance.formControl.setValue('12');
35 | fixture.detectChanges();
36 |
37 | expect(formlyMessageElm.textContent).not.toMatch(/Maximum Length Exceeded/);
38 | expect(formlyMessageElm.textContent).not.toMatch(/Title is required/);
39 | });
40 |
41 | describe('render validation message', () => {
42 | it('with a simple validation message', () => {
43 | const fixture = createTestComponent('');
44 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement);
45 | fixture.componentInstance.formControl.setValue('test');
46 | fixture.detectChanges();
47 |
48 | expect(formlyMessageElm.textContent).toMatch(/Maximum Length Exceeded/);
49 | expect(formlyMessageElm.textContent).not.toMatch(/Title is required/);
50 | });
51 |
52 | it('with a function validation message', () => {
53 | const fixture = createTestComponent('');
54 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement);
55 |
56 | expect(formlyMessageElm.textContent).toMatch(/Title is required/);
57 | expect(formlyMessageElm.textContent).not.toMatch(/Maximum Length Exceeded/);
58 | });
59 |
60 | it('with a validator.message property', () => {
61 | const fixture = createTestComponent('');
62 | const formlyMessageElm = getFormlyValidationMessageElement(fixture.nativeElement);
63 | fixture.componentInstance.field = (Object).assign({}, fixture.componentInstance.field, {
64 | validators: {
65 | required: {
66 | expression: (control: FormControl) => false,
67 | message: `Custom title: Should have atleast 3 Characters`,
68 | },
69 | },
70 | });
71 |
72 | fixture.detectChanges();
73 |
74 | expect(formlyMessageElm.textContent).toMatch(/Custom title: Should have atleast 3 Characters/);
75 | expect(formlyMessageElm.textContent).not.toMatch(/Maximum Length Exceeded/);
76 | expect(formlyMessageElm.textContent).not.toMatch(/Title is required/);
77 | });
78 | });
79 |
80 | });
81 |
82 | @Component({selector: 'formly-validation-message-test', template: '', entryComponents: []})
83 | class TestComponent {
84 | formControl = new FormControl(null, [Validators.required, Validators.maxLength(3)]);
85 | field: FormlyFieldConfig = {
86 | type: 'input',
87 | key: 'title',
88 | templateOptions: {
89 | label: 'Title',
90 | },
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/formly.validation-message.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { FormControl } from '@angular/forms';
3 | import { FormlyFieldConfig, FormlyValidationMessages } from '../core/core';
4 |
5 | @Component({
6 | selector: 'formly-validation-message',
7 | template: ``,
8 | })
9 | export class FormlyValidationMessage {
10 | @Input() fieldForm: FormControl;
11 | @Input() field: FormlyFieldConfig;
12 |
13 | constructor(private formlyMessages: FormlyValidationMessages) {}
14 |
15 | get errorMessage() {
16 | for (let error in this.fieldForm.errors) {
17 | if (this.fieldForm.errors.hasOwnProperty(error)) {
18 | let message = this.formlyMessages.getValidatorErrorMessage(error);
19 | ['validators', 'asyncValidators'].map(validators => {
20 | if (this.field[validators] && this.field[validators][error] && this.field[validators][error].message) {
21 | message = this.field.validators[error].message;
22 | }
23 | });
24 |
25 | if (typeof message === 'function') {
26 | return message(this.fieldForm.errors[error], this.field);
27 | }
28 |
29 | return message;
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/run/addon.ts:
--------------------------------------------------------------------------------
1 | import { FormlyConfig } from '../../core/core';
2 |
3 | export class TemplateAddons {
4 | run(fc: FormlyConfig) {
5 | fc.templateManipulators.postWrapper.push((field) => {
6 | if (field && field.templateOptions && (field.templateOptions.addonLeft || field.templateOptions.addonRight)) {
7 | return 'addons';
8 | }
9 | });
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/run/description.ts:
--------------------------------------------------------------------------------
1 | import { FormlyFieldConfig, FormlyConfig } from '../../core/core';
2 |
3 | export class TemplateDescription {
4 | run(fc: FormlyConfig) {
5 | fc.templateManipulators.postWrapper.push((field: FormlyFieldConfig) => {
6 | if (field && field.templateOptions && field.templateOptions.description) {
7 | return 'description';
8 | }
9 | });
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/run/validation.ts:
--------------------------------------------------------------------------------
1 | import { FormlyFieldConfig, FormlyConfig } from '../../core/core';
2 |
3 | export class TemplateValidation {
4 | run(fc: FormlyConfig) {
5 | fc.templateManipulators.postWrapper.push((field: FormlyFieldConfig) => {
6 | if (field && field.validators) {
7 | return 'validation-message';
8 | }
9 | });
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/types/checkbox.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewEncapsulation } from '@angular/core';
2 | import { FormControl, AbstractControl } from '@angular/forms';
3 | import { FieldType, FormlyFieldConfig } from '../../core/core';
4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject';
5 |
6 | @Component({
7 | selector: 'formly-field-checkbox',
8 | template: `
9 |
10 |
13 |
14 |
15 | `
16 | })
17 | export class FormlyFieldCheckbox extends FieldType {
18 |
19 | constructor() {
20 | super();
21 | this.valueChanges = new BehaviorSubject(false);
22 | }
23 |
24 | static createControl(model: any, field: FormlyFieldConfig): AbstractControl {
25 | return new FormControl(
26 | { checked: model ? 'true' : 'false', disabled: field.templateOptions.disabled },
27 | field.validators ? field.validators.validation : undefined,
28 | field.asyncValidators ? field.asyncValidators.validation : undefined,
29 | );
30 | }
31 |
32 | public onPropertyChanged(event: any): void {
33 | if (event.propertyName === 'checked') {
34 | this.valueChanges.next(event.value);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/types/input.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewEncapsulation } from '@angular/core';
2 | import { FieldType } from '../../core/core';
3 | import { BehaviorSubject } from 'rxjs/BehaviorSubject';
4 |
5 | @Component({
6 | selector: 'formly-field-input',
7 | template: `
8 |
9 |
10 |
11 | `
12 | })
13 | export class FormlyFieldInput extends FieldType {
14 |
15 | constructor() {
16 | super();
17 | this.valueChanges = new BehaviorSubject('');
18 | }
19 |
20 | get type() {
21 | return this.to.type || 'text';
22 | }
23 |
24 | public onPropertyChanged(event: any): void {
25 | if (event.propertyName === 'text') {
26 | this.valueChanges.next(event.value);
27 | }
28 | }
29 |
30 | ngOnInit() {
31 | // init with existing value
32 | this.valueChanges.next(this.model[this.key]);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/types/multicheckbox.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FormGroup, FormControl, AbstractControl } from '@angular/forms';
3 | import { FieldType, FormlyFieldConfig } from '../../core/core';
4 |
5 | @Component({
6 | selector: 'formly-field-multicheckbox',
7 | template: `
8 |
9 |
10 |
11 |
12 | `,
13 | })
14 | export class FormlyFieldMultiCheckbox extends FieldType {
15 | static createControl(model: any, field: FormlyFieldConfig): AbstractControl {
16 | let controlGroupConfig = field.templateOptions.options.reduce((previous, option) => {
17 | previous[option.key] = new FormControl(model ? model[option.key] : undefined);
18 | return previous;
19 | }, {});
20 |
21 | return new FormGroup(controlGroupConfig);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/types/radio.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FieldType } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-field-radio',
6 | template: `
7 |
8 |
9 |
11 |
12 |
13 |
14 | `,
15 | })
16 | export class FormlyFieldRadio extends FieldType {}
17 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/types/select.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FieldType } from '../../core/core';
3 |
4 | export class SelectOption {
5 | label: string;
6 | value?: string;
7 | group?: SelectOption[];
8 |
9 | constructor(label: string, value?: string, children?: SelectOption[]) {
10 | this.label = label;
11 | this.value = value;
12 | this.group = children;
13 | }
14 | }
15 |
16 |
17 | @Component({
18 | selector: 'formly-field-select',
19 | template: ``
20 | // template: `
21 | //
32 | // `,
33 | })
34 | export class FormlyFieldSelect extends FieldType {
35 | get labelProp(): string { return this.to['labelProp'] || 'label'; }
36 | get valueProp(): string { return this.to['valueProp'] || 'value'; }
37 | get groupProp(): string { return this.to['groupProp'] || 'group'; }
38 |
39 | get selectOptions() {
40 | let options: SelectOption[] = [];
41 | this.to.options.map((option: SelectOption) => {
42 | if (!option[this.groupProp]) {
43 | options.push(option);
44 | } else {
45 | let filteredOption: SelectOption[] = options.filter((filteredOption) => {
46 | return filteredOption.label === option[this.groupProp];
47 | });
48 | if (filteredOption[0]) {
49 | filteredOption[0].group.push({
50 | label: option[this.labelProp],
51 | value: option[this.valueProp],
52 | });
53 | }
54 | else {
55 | options.push({
56 | label: option[this.groupProp],
57 | group: [{ value: option[this.valueProp], label: option[this.labelProp] }],
58 | });
59 | }
60 | }
61 | });
62 | return options;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/types/textarea.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FieldType } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-field-textarea',
6 | template: `
7 |
9 |
10 | `,
11 | })
12 | export class FormlyFieldTextArea extends FieldType {
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/types/types.ts:
--------------------------------------------------------------------------------
1 | import { FormlyFieldCheckbox } from './checkbox';
2 | import { FormlyFieldMultiCheckbox } from './multicheckbox';
3 | import { FormlyFieldInput } from './input';
4 | import { FormlyFieldRadio } from './radio';
5 | import { FormlyFieldTextArea } from './textarea';
6 | import { FormlyFieldSelect } from './select';
7 |
8 | export {
9 | FormlyFieldCheckbox,
10 | FormlyFieldMultiCheckbox,
11 | FormlyFieldInput,
12 | FormlyFieldRadio,
13 | FormlyFieldTextArea,
14 | FormlyFieldSelect,
15 | };
16 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/ui-bootstrap.config.ts:
--------------------------------------------------------------------------------
1 | import { ConfigOption } from '../core/services/formly.config';
2 | import { FormlyWrapperAddons } from './wrappers/addons';
3 | import { TemplateDescription } from './run/description';
4 | import { TemplateValidation } from './run/validation';
5 | import { TemplateAddons } from './run/addon';
6 | import {
7 | FormlyFieldInput,
8 | FormlyFieldCheckbox,
9 | FormlyFieldRadio,
10 | FormlyFieldSelect,
11 | FormlyFieldTextArea,
12 | FormlyFieldMultiCheckbox,
13 | } from './types/types';
14 | import {
15 | FormlyWrapperLabel,
16 | FormlyWrapperSideLabel,
17 | FormlyWrapperDescription,
18 | FormlyWrapperValidationMessages,
19 | FormlyWrapperFieldset,
20 | } from './wrappers/wrappers';
21 |
22 | export const FIELD_TYPE_COMPONENTS = [
23 | // types
24 | FormlyFieldInput,
25 | FormlyFieldCheckbox,
26 | FormlyFieldRadio,
27 | FormlyFieldSelect,
28 | FormlyFieldTextArea,
29 | FormlyFieldMultiCheckbox,
30 |
31 | // wrappers
32 | FormlyWrapperLabel,
33 | FormlyWrapperSideLabel,
34 | FormlyWrapperDescription,
35 | FormlyWrapperValidationMessages,
36 | FormlyWrapperFieldset,
37 | FormlyWrapperAddons,
38 | ];
39 |
40 | export const BOOTSTRAP_FORMLY_CONFIG: ConfigOption = {
41 | types: [
42 | {
43 | name: 'input',
44 | component: FormlyFieldInput,
45 | wrappers: ['label'],
46 | },
47 | {
48 | name: 'checkbox',
49 | component: FormlyFieldCheckbox,
50 | wrappers: ['fieldset'],
51 | },
52 | {
53 | name: 'radio',
54 | component: FormlyFieldRadio,
55 | wrappers: ['fieldset', 'label'],
56 | },
57 | {
58 | name: 'select',
59 | component: FormlyFieldSelect,
60 | wrappers: ['fieldset', 'label'],
61 | },
62 | {
63 | name: 'textarea',
64 | component: FormlyFieldTextArea,
65 | wrappers: ['fieldset', 'label'],
66 | },
67 | {
68 | name: 'multicheckbox',
69 | component: FormlyFieldMultiCheckbox,
70 | wrappers: ['fieldset', 'label'],
71 | },
72 | ],
73 | wrappers: [
74 | {name: 'label', component: FormlyWrapperLabel},
75 | {name: 'sideLabel', component: FormlyWrapperSideLabel},
76 | {name: 'description', component: FormlyWrapperDescription},
77 | {name: 'validation-message', component: FormlyWrapperValidationMessages},
78 | {name: 'fieldset', component: FormlyWrapperFieldset},
79 | {name: 'addons', component: FormlyWrapperAddons},
80 | ],
81 | manipulators: [
82 | {class: TemplateDescription, method: 'run'},
83 | {class: TemplateValidation, method: 'run'},
84 | {class: TemplateAddons, method: 'run'},
85 | ],
86 | };
87 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/ui-bootstrap.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { NativeScriptModule, NativeScriptFormsModule } from "nativescript-angular";
3 | import { ReactiveFormsModule } from '@angular/forms';
4 | import { FormlyModule } from '../core/core';
5 | import { BOOTSTRAP_FORMLY_CONFIG, FIELD_TYPE_COMPONENTS } from './ui-bootstrap.config';
6 | import { FormlyValidationMessage } from './formly.validation-message';
7 |
8 | @NgModule({
9 | declarations: [...FIELD_TYPE_COMPONENTS, FormlyValidationMessage],
10 | imports: [
11 | NativeScriptModule,
12 | ReactiveFormsModule,
13 | FormlyModule.forRoot(BOOTSTRAP_FORMLY_CONFIG),
14 | ],
15 | })
16 | export class FormlyBootstrapModule {
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/ui-bootstrap.ts:
--------------------------------------------------------------------------------
1 | export * from './types/types';
2 | export * from './wrappers/wrappers';
3 | export { FormlyValidationMessage } from './formly.validation-message';
4 | export { FormlyBootstrapModule } from './ui-bootstrap.module';
5 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/wrappers/addons.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
2 | import { FieldWrapper } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-wrapper-addons',
6 | template: `
7 |
8 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 | `,
23 | encapsulation: ViewEncapsulation.None
24 | })
25 | export class FormlyWrapperAddons extends FieldWrapper {
26 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef;
27 |
28 | addonRightClick($event) {
29 | if (this.to['addonRight'].onClick) {
30 | this.to['addonRight'].onClick(this.to, this, $event);
31 | }
32 | }
33 |
34 | addonLeftClick($event) {
35 | if (this.to['addonLeft'].onClick) {
36 | this.to['addonLeft'].onClick(this.to, this, $event);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/wrappers/description.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
2 | import { FieldWrapper } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-wrapper-description',
6 | template: `
7 |
8 |
9 |
10 |
11 | `
12 | })
13 | export class FormlyWrapperDescription extends FieldWrapper {
14 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef;
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/wrappers/fieldset.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
2 | import { FieldWrapper } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-wrapper-fieldset',
6 | template: `
7 |
8 |
9 |
10 | `
11 | })
12 | export class FormlyWrapperFieldset extends FieldWrapper {
13 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef;
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/wrappers/label.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
2 | import { FieldWrapper } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-wrapper-label',
6 | template: `
7 |
8 |
9 |
10 |
11 |
12 | `
13 | })
14 | export class FormlyWrapperLabel extends FieldWrapper {
15 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/wrappers/message-validation.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
2 | import { FieldWrapper } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-wrapper-validation-messages',
6 | template: `
7 |
8 |
9 |
10 |
11 | `,
12 | encapsulation: ViewEncapsulation.None
13 | })
14 | export class FormlyWrapperValidationMessages extends FieldWrapper {
15 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef;
16 |
17 | get validationId() {
18 | return this.field.id + '-message';
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/wrappers/sideLabel.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
2 | import { FieldWrapper } from '../../core/core';
3 |
4 | @Component({
5 | selector: 'formly-wrapper-side-label',
6 | template: `
7 |
8 |
9 |
10 |
11 |
12 | `
13 | })
14 | export class FormlyWrapperSideLabel extends FieldWrapper {
15 | @ViewChild('fieldComponent', {read: ViewContainerRef}) fieldComponent: ViewContainerRef;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/ui-bootstrap/wrappers/wrappers.ts:
--------------------------------------------------------------------------------
1 | import { FormlyWrapperFieldset } from './fieldset';
2 | import { FormlyWrapperLabel } from './label';
3 | import { FormlyWrapperSideLabel } from './sideLabel';
4 | import { FormlyWrapperDescription } from './description';
5 | import { FormlyWrapperValidationMessages } from './message-validation';
6 |
7 | export {
8 | FormlyWrapperFieldset,
9 | FormlyWrapperLabel,
10 | FormlyWrapperSideLabel,
11 | FormlyWrapperDescription,
12 | FormlyWrapperValidationMessages,
13 | };
14 |
--------------------------------------------------------------------------------
/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "NativeScript Application",
3 | "license": "SEE LICENSE IN ",
4 | "readme": "NativeScript Application",
5 | "repository": "",
6 | "nativescript": {
7 | "id": "org.nativescript.demo",
8 | "tns-ios": {
9 | "version": "2.4.0"
10 | },
11 | "tns-android": {
12 | "version": "2.4.0"
13 | }
14 | },
15 | "dependencies": {
16 | "@angular/common": "~2.2.1",
17 | "@angular/compiler": "~2.2.1",
18 | "@angular/core": "~2.2.1",
19 | "@angular/forms": "~2.2.1",
20 | "@angular/http": "~2.2.1",
21 | "@angular/platform-browser": "~2.2.1",
22 | "@angular/platform-browser-dynamic": "~2.2.1",
23 | "@angular/router": "~3.2.1",
24 | "@types/jasmine": "^2.5.35",
25 | "es6-promise": "~3.1.2",
26 | "es6-shim": "^0.35.0",
27 | "nativescript-angular": "next",
28 | "nativescript-theme-core": "^1.0.2",
29 | "ng-formly-nativescript": "file:..",
30 | "parse5": "1.4.2",
31 | "punycode": "1.3.2",
32 | "querystring": "0.2.0",
33 | "reflect-metadata": "0.1.3",
34 | "rimraf": "^2.5.1",
35 | "rxjs": "5.0.0-beta.12",
36 | "tns-core-modules": "2.4.0",
37 | "url": "0.10.3"
38 | },
39 | "devDependencies": {
40 | "@types/jasmine": "^2.5.35",
41 | "babel-traverse": "6.18.0",
42 | "babel-types": "6.18.0",
43 | "babylon": "6.13.1",
44 | "lazy": "1.0.11",
45 | "nativescript-dev-typescript": "^0.3.2",
46 | "typescript": "^2.0.10",
47 | "zone.js": "~0.6.21"
48 | }
49 | }
--------------------------------------------------------------------------------
/src/references.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "declaration": false,
6 | "removeComments": true,
7 | "noLib": false,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "lib": [
11 | "dom"
12 | ],
13 | "sourceMap": true,
14 | "pretty": true,
15 | "allowUnreachableCode": true,
16 | "allowUnusedLabels": false,
17 | "noImplicitAny": false,
18 | "noImplicitReturns": false,
19 | "noImplicitUseStrict": false,
20 | "noFallthroughCasesInSwitch": true,
21 | "typeRoots": [
22 | "node_modules/@types",
23 | "node_modules"
24 | ],
25 | "types": [
26 | "jasmine"
27 | ]
28 | },
29 | "exclude": [
30 | "node_modules",
31 | "platforms"
32 | ],
33 | "compileOnSave": false
34 | }
35 |
--------------------------------------------------------------------------------
/test-main.js:
--------------------------------------------------------------------------------
1 | debugger;
2 | if (!Object.hasOwnProperty('name')) {
3 | Object.defineProperty(Function.prototype, 'name', {
4 | get: function() {
5 | var matches = this.toString().match(/^\s*function\s*(\S*)\s*\(/);
6 | var name = matches && matches.length > 1 ? matches[1] : "";
7 | Object.defineProperty(this, 'name', {value: name});
8 | return name;
9 | }
10 | });
11 | }
12 |
13 | // Turn on full stack traces in errors to help debugging
14 | Error.stackTraceLimit = Infinity;
15 |
16 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
17 |
18 | // Cancel Karma's synchronous start,
19 | // we will call `__karma__.start()` later, once all the specs are loaded.
20 | __karma__.loaded = function() {};
21 |
22 | // Load our SystemJS configuration.
23 | System.config({
24 | baseURL: '/base/'
25 | });
26 |
27 | System.config({
28 | defaultJSExtensions: true,
29 | paths: {
30 | 'application': 'node_modules/tns-core-modules/application/application.ios.js',
31 | 'ui-dialogs/*': 'node_modules/tns-core-modules/ui/dialogs/*.js'
32 | },
33 | map: {
34 | '@angular': 'node_modules/@angular',
35 | 'rxjs': 'node_modules/rxjs'
36 | },
37 | packages: {
38 | '@angular/core': {
39 | main: 'index.js',
40 | defaultExtension: 'js'
41 | },
42 | '@angular/compiler': {
43 | main: 'index.js',
44 | defaultExtension: 'js'
45 | },
46 | '@angular/common': {
47 | main: 'index.js',
48 | defaultExtension: 'js'
49 | },
50 | '@angular/http': {
51 | main: 'index.js',
52 | defaultExtension: 'js'
53 | },
54 | '@angular/platform-browser': {
55 | main: 'index.js',
56 | defaultExtension: 'js'
57 | },
58 | '@angular/platform-browser-dynamic': {
59 | main: 'index.js',
60 | defaultExtension: 'js'
61 | },
62 | '@angular/router-deprecated': {
63 | main: 'index.js',
64 | defaultExtension: 'js'
65 | },
66 | '@angular/router': {
67 | main: 'index.js',
68 | defaultExtension: 'js'
69 | },
70 | 'rxjs': {
71 | defaultExtension: 'js'
72 | }
73 | }
74 | });
75 |
76 | Promise.all([
77 | System.import('@angular/core/testing'),
78 | System.import('@angular/platform-browser-dynamic/testing')
79 | ]).then(function (providers) {
80 | debugger;
81 | var testing = providers[0];
82 | var testingBrowser = providers[1];
83 |
84 | testing.setBaseTestProviders(testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
85 | testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS);
86 |
87 | }).then(function() {
88 | return Promise.all(
89 | Object.keys(window.__karma__.files) // All files served by Karma.
90 | .filter(onlySpecFiles)
91 | .map(file2moduleName)
92 | .map(function(path) {
93 | return System.import(path).then(function(module) {
94 | if (module.hasOwnProperty('main')) {
95 | module.main();
96 | } else {
97 | throw new Error('Module ' + path + ' does not implement main() method.');
98 | }
99 | });
100 | }));
101 | })
102 | .then(function() {
103 | __karma__.start();
104 | }, function(error) {
105 | console.error(error.stack || error);
106 | __karma__.start();
107 | });
108 |
109 | function onlySpecFiles(path) {
110 | // check for individual files, if not given, always matches to all
111 | var patternMatched = __karma__.config.files ?
112 | path.match(new RegExp(__karma__.config.files)) : true;
113 |
114 | return patternMatched && /[\.|_]spec\.js$/.test(path);
115 | }
116 |
117 | // Normalize paths to module names.
118 | function file2moduleName(filePath) {
119 | return filePath.replace(/\\/g, '/')
120 | .replace(/^\/base\//, '')
121 | .replace(/\.js$/, '');
122 | }
123 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "removeComments": true,
7 | "noLib": false,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "lib": [
11 | "dom"
12 | ],
13 | "sourceMap": true,
14 | "pretty": true,
15 | "allowUnreachableCode": true,
16 | "allowUnusedLabels": false,
17 | "noImplicitAny": false,
18 | "noImplicitReturns": false,
19 | "noImplicitUseStrict": false,
20 | "noFallthroughCasesInSwitch": true,
21 | "typeRoots": [
22 | "node_modules/@types",
23 | "node_modules"
24 | ],
25 | "types": [
26 | "jasmine"
27 | ]
28 | },
29 | "exclude": [
30 | "src",
31 | "node_modules",
32 | "platforms"
33 | ],
34 | "compileOnSave": false
35 | }
36 |
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "mode": "modules",
3 | "out": "doc",
4 | "theme": "default",
5 | "ignoreCompilerErrors": "true",
6 | "experimentalDecorators": "true",
7 | "emitDecoratorMetadata": "true",
8 | "target": "ES5",
9 | "moduleResolution": "node",
10 | "preserveConstEnums": "true",
11 | "stripInternal": "true",
12 | "suppressExcessPropertyErrors": "true",
13 | "suppressImplicitAnyIndexErrors": "true",
14 | "module": "commonjs"
15 | }
--------------------------------------------------------------------------------