diff --git a/examples/app/app.component.html b/examples/app/app.component.html
index 7ed19781e..af2ecf01f 100644
--- a/examples/app/app.component.html
+++ b/examples/app/app.component.html
@@ -20,10 +20,10 @@
Angular 2 Forms
-
+
-
+
@@ -36,13 +36,18 @@ DevExtreme validation features
-
-
+
+
+
+
+
-
-
+
+
+
+
@@ -51,6 +56,26 @@ DevExtreme validation features
+ dxForm
+
+
+
+
+
+
+
+
+
+
+
Editor Widgets
diff --git a/examples/app/app.component.ts b/examples/app/app.component.ts
index 1345dbdb9..68532d3ba 100644
--- a/examples/app/app.component.ts
+++ b/examples/app/app.component.ts
@@ -99,19 +99,9 @@ import{
export class AppComponent implements OnInit {
@ViewChild(DxPopoverComponent) popover: DxPopoverComponent;
text = 'Initial text';
- email: string;
+ formData = { email: '', password: '' };
emailControl: AbstractControl;
- password: string;
passwordControl: AbstractControl;
- dxValidationRules = {
- email: [
- { type: 'required', message: 'Email is required.' },
- { type: 'email', message: 'Email is invalid.' }
- ],
- password: [
- { type: 'required', message: 'Email is required.' }
- ]
- };
form: FormGroup;
boolValue: boolean;
numberValue: number;
diff --git a/metadata/StrongMetaData.json b/metadata/StrongMetaData.json
index ad28d505b..b9627bbe9 100644
--- a/metadata/StrongMetaData.json
+++ b/metadata/StrongMetaData.json
@@ -2515,7 +2515,7 @@
},
"formItem": {
"ComplexTypes": [
- "dxFormSimpleItem"
+ "FormSimpleItem"
],
"Description": "The form item configuration object. Used only when the editing mode is \"form\"."
},
@@ -2652,8 +2652,15 @@
"Description": "In a boolean column, replaces all true items with a specified text."
},
"validationRules": {
- "PrimitiveTypes": [
- "any"
+ "ComplexTypes": [
+ "CompareRule",
+ "CustomRule",
+ "EmailRule",
+ "NumericRule",
+ "PatternRule",
+ "RangeRule",
+ "RequiredRule",
+ "StringLengthRule"
],
"IsCollection": true,
"SingularName": "validationRule",
@@ -4816,8 +4823,11 @@
"Description": "An object providing data for the form."
},
"items": {
- "PrimitiveTypes": [
- "any"
+ "ComplexTypes": [
+ "FormEmptyItem",
+ "FormGroupItem",
+ "FormSimpleItem",
+ "FormTabbedItem"
],
"IsCollection": true,
"SingularName": "item",
@@ -15001,8 +15011,15 @@
"Description": "Specifies the validation group the editor will be related to."
},
"validationRules": {
- "PrimitiveTypes": [
- "any"
+ "ComplexTypes": [
+ "CompareRule",
+ "CustomRule",
+ "EmailRule",
+ "NumericRule",
+ "PatternRule",
+ "RangeRule",
+ "RequiredRule",
+ "StringLengthRule"
],
"IsCollection": true,
"SingularName": "validationRule",
@@ -17107,25 +17124,69 @@
"SingularName": "constantLine",
"Description": "Declares a collection of constant lines belonging to the argument axis.",
"Options": {
+ "color": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the color of constant lines."
+ },
+ "dashStyle": {
+ "EnumName": "DashStyle",
+ "Description": "Specifies the dash style of constant lines."
+ },
"label": {
"Description": "Configures the constant line label.",
"Options": {
+ "font": {
+ "ComplexTypes": [
+ "VizFont"
+ ],
+ "Description": "Specifies font options for constant line labels."
+ },
+ "position": {
+ "EnumName": "RelativePosition",
+ "Description": "Specifies the position of constant line labels on the chart plot."
+ },
+ "visible": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Makes constant line labels visible."
+ },
"horizontalAlignment": {
"EnumName": "HorizontalAlignment",
"Description": "Aligns constant line labels in the horizontal direction."
},
- "verticalAlignment": {
- "EnumName": "VerticalAlignment",
- "Description": "Aligns constant line labels in the vertical direction."
- },
"text": {
"PrimitiveTypes": [
"string"
],
"Description": "Specifies the text of a constant line label. By default, equals to the value of the constant line."
+ },
+ "verticalAlignment": {
+ "EnumName": "VerticalAlignment",
+ "Description": "Aligns constant line labels in the vertical direction."
}
}
},
+ "paddingLeftRight": {
+ "PrimitiveTypes": [
+ "double"
+ ],
+ "Description": "Generates a pixel-measured empty space between the left/right side of a constant line and the constant line label."
+ },
+ "paddingTopBottom": {
+ "PrimitiveTypes": [
+ "double"
+ ],
+ "Description": "Generates a pixel-measured empty space between the top/bottom side of a constant line and the constant line label."
+ },
+ "width": {
+ "PrimitiveTypes": [
+ "double"
+ ],
+ "Description": "Specifies the width of constant lines in pixels."
+ },
"value": {
"PrimitiveTypes": [
"date",
@@ -18718,25 +18779,69 @@
"SingularName": "constantLine",
"Description": "Declares a collection of constant lines belonging to the value axis.",
"Options": {
+ "color": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the color of constant lines."
+ },
+ "dashStyle": {
+ "EnumName": "DashStyle",
+ "Description": "Specifies the dash style of constant lines."
+ },
"label": {
"Description": "Configures the constant line label.",
"Options": {
+ "font": {
+ "ComplexTypes": [
+ "VizFont"
+ ],
+ "Description": "Specifies font options for constant line labels."
+ },
+ "position": {
+ "EnumName": "RelativePosition",
+ "Description": "Specifies the position of constant line labels on the chart plot."
+ },
+ "visible": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Makes constant line labels visible."
+ },
"horizontalAlignment": {
"EnumName": "HorizontalAlignment",
"Description": "Aligns constant line labels in the horizontal direction."
},
- "verticalAlignment": {
- "EnumName": "VerticalAlignment",
- "Description": "Aligns constant line labels in the vertical direction."
- },
"text": {
"PrimitiveTypes": [
"string"
],
"Description": "Specifies the text of a constant line label. By default, equals to the value of the constant line."
+ },
+ "verticalAlignment": {
+ "EnumName": "VerticalAlignment",
+ "Description": "Aligns constant line labels in the vertical direction."
}
}
},
+ "paddingLeftRight": {
+ "PrimitiveTypes": [
+ "double"
+ ],
+ "Description": "Generates a pixel-measured empty space between the left/right side of a constant line and the constant line label."
+ },
+ "paddingTopBottom": {
+ "PrimitiveTypes": [
+ "double"
+ ],
+ "Description": "Generates a pixel-measured empty space between the top/bottom side of a constant line and the constant line label."
+ },
+ "width": {
+ "PrimitiveTypes": [
+ "double"
+ ],
+ "Description": "Specifies the width of constant lines in pixels."
+ },
"value": {
"PrimitiveTypes": [
"date",
@@ -24181,8 +24286,9 @@
"Description": "Indicates whether or not animation is enabled."
},
"callSelectedRangeChanged": {
+ "IsDeprecated": true,
"EnumName": "ValueChangedCallMode",
- "Description": "Specifies when to call the onSelectedRangeChanged function."
+ "Description": "Use the callValueChanged option instead."
},
"callValueChanged": {
"EnumName": "ValueChangedCallMode",
@@ -24378,9 +24484,10 @@
}
},
"onSelectedRangeChanged": {
+ "IsDeprecated": true,
"IsFunc": true,
"IsEvent": true,
- "Description": "A handler for the selectedRangeChanged event."
+ "Description": "Use the onValueChanged option instead."
},
"onValueChanged": {
"IsFunc": true,
@@ -24930,7 +25037,7 @@
}
},
"selectedRange": {
- "Description": "Specifies the range to be selected when displaying the RangeSelector.",
+ "Description": "Use the value option instead.",
"Options": {
"endValue": {
"PrimitiveTypes": [
@@ -28393,6 +28500,387 @@
}
}
},
+ "FormEmptyItem": {
+ "Description": "This article describes configuration options of an empty form item.",
+ "Options": {
+ "colSpan": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the number of columns spanned by the item."
+ },
+ "cssClass": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies a CSS class to be applied to the form item."
+ },
+ "itemType": {
+ "EnumName": "FormItemType",
+ "Description": "Specifies the type of the current item."
+ },
+ "name": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the form item name."
+ },
+ "visible": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not the current form item is visible."
+ },
+ "visibleIndex": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the sequence number of the item in a form, group or tab."
+ }
+ }
+ },
+ "FormGroupItem": {
+ "Description": "This article describes configuration options of a group form item.",
+ "Options": {
+ "alignItemLabels": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not all group item labels are aligned."
+ },
+ "caption": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the group caption."
+ },
+ "colCount": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns in the group layout."
+ },
+ "colCountByScreen": {
+ "Description": "Specifies dependency between the screen factor and the count of columns in the group layout.",
+ "Options": {
+ "lg": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for a large screen size."
+ },
+ "md": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for a middle-sized screen."
+ },
+ "sm": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for a small-sized screen."
+ },
+ "xs": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for an extra small-sized screen."
+ }
+ }
+ },
+ "colSpan": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the number of columns spanned by the item."
+ },
+ "cssClass": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies a CSS class to be applied to the form item."
+ },
+ "items": {
+ "ComplexTypes": [
+ "FormEmptyItem",
+ "FormGroupItem",
+ "FormSimpleItem",
+ "FormTabbedItem"
+ ],
+ "IsCollection": true,
+ "SingularName": "item",
+ "Description": "Holds an array of form items displayed within the group."
+ },
+ "itemType": {
+ "EnumName": "FormItemType",
+ "Description": "Specifies the type of the current item."
+ },
+ "template": {
+ "IsTemplate": true,
+ "Description": "A template to be used for rendering a group item."
+ },
+ "visible": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not the current form item is visible."
+ },
+ "visibleIndex": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the sequence number of the item in a form, group or tab."
+ }
+ }
+ },
+ "FormSimpleItem": {
+ "Description": "This article describes configuration options of a simple form item.",
+ "Options": {
+ "colSpan": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the number of columns spanned by the item."
+ },
+ "cssClass": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies a CSS class to be applied to the form item."
+ },
+ "dataField": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the path to the formData object field bound to the current form item."
+ },
+ "editorOptions": {
+ "PrimitiveTypes": [
+ "any"
+ ],
+ "Description": "Specifies configuration options for the editor widget of the current form item."
+ },
+ "editorType": {
+ "EnumName": "FormItemEditorType",
+ "Description": "Specifies which editor widget is used to display and edit the form item value."
+ },
+ "helpText": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the help text displayed for the current form item."
+ },
+ "isRequired": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether the current form item is required."
+ },
+ "itemType": {
+ "EnumName": "FormItemType",
+ "Description": "Specifies the type of the current item."
+ },
+ "label": {
+ "Description": "Specifies options for the form item label.",
+ "Options": {
+ "alignment": {
+ "EnumName": "HorizontalAlignment",
+ "Description": "Specifies the label horizontal alignment."
+ },
+ "location": {
+ "EnumName": "FormLabelLocation",
+ "Description": "Specifies the location of a label against the editor."
+ },
+ "showColon": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not a colon is displayed at the end of the current label."
+ },
+ "text": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the label text."
+ },
+ "visible": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not the label is visible."
+ }
+ }
+ },
+ "name": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the form item name."
+ },
+ "template": {
+ "IsTemplate": true,
+ "Description": "A template to be used for rendering the form item."
+ },
+ "validationRules": {
+ "ComplexTypes": [
+ "CompareRule",
+ "CustomRule",
+ "EmailRule",
+ "NumericRule",
+ "PatternRule",
+ "RangeRule",
+ "RequiredRule",
+ "StringLengthRule"
+ ],
+ "IsCollection": true,
+ "SingularName": "validationRule",
+ "Description": "An array of validation rules to be checked for the form item editor."
+ },
+ "visible": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not the current form item is visible."
+ },
+ "visibleIndex": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the sequence number of the item in a form, group or tab."
+ }
+ }
+ },
+ "FormTabbedItem": {
+ "Description": "This article describes configuration options of a tabbed form item.",
+ "Options": {
+ "colSpan": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the number of columns spanned by the item."
+ },
+ "cssClass": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies a CSS class to be applied to the form item."
+ },
+ "itemType": {
+ "EnumName": "FormItemType",
+ "Description": "Specifies the type of the current item."
+ },
+ "tabPanelOptions": {
+ "ComplexTypes": [
+ "dxTabPanelOptions"
+ ],
+ "Description": "Holds a configuration object for the TabPanel widget used to display the current form item."
+ },
+ "tabs": {
+ "IsCollection": true,
+ "SingularName": "tab",
+ "Description": "An array of tab configuration objects.",
+ "Options": {
+ "alignItemLabels": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not labels of items displayed within the current tab are aligned."
+ },
+ "badge": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies a badge text for the tab."
+ },
+ "colCount": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns in the tab layout."
+ },
+ "colCountByScreen": {
+ "Description": "Specifies dependency between the screen factor and the count of columns in the tab layout.",
+ "Options": {
+ "lg": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for a large screen size."
+ },
+ "md": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for a middle-sized screen."
+ },
+ "sm": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for a small-sized screen."
+ },
+ "xs": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "The count of columns for an extra small-sized screen."
+ }
+ }
+ },
+ "disabled": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "A Boolean value specifying whether or not the tab can respond to user interaction."
+ },
+ "icon": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the icon to be displayed on the tab."
+ },
+ "items": {
+ "ComplexTypes": [
+ "FormEmptyItem",
+ "FormGroupItem",
+ "FormSimpleItem",
+ "FormTabbedItem"
+ ],
+ "IsCollection": true,
+ "SingularName": "item",
+ "Description": "Holds an array of form items displayed within the tab."
+ },
+ "tabTemplate": {
+ "IsTemplate": true,
+ "Description": "The template to be used for rendering the tab."
+ },
+ "template": {
+ "IsTemplate": true,
+ "Description": "The template to be used for rendering the tab content."
+ },
+ "title": {
+ "PrimitiveTypes": [
+ "string"
+ ],
+ "Description": "Specifies the tab title."
+ }
+ }
+ },
+ "visible": {
+ "PrimitiveTypes": [
+ "bool"
+ ],
+ "Description": "Specifies whether or not the current form item is visible."
+ },
+ "visibleIndex": {
+ "PrimitiveTypes": [
+ "int32"
+ ],
+ "Description": "Specifies the sequence number of the item in a form, group or tab."
+ }
+ }
+ },
"VizFont": {
"Description": "Font options.",
"Options": {
@@ -33461,6 +33949,84 @@
}
]
},
+ "FormItemType": {
+ "Items": [
+ {
+ "Name": "empty"
+ },
+ {
+ "Name": "group"
+ },
+ {
+ "Name": "simple"
+ },
+ {
+ "Name": "tabbed"
+ }
+ ]
+ },
+ "FormItemEditorType": {
+ "Items": [
+ {
+ "Name": "dxAutocomplete"
+ },
+ {
+ "Name": "dxCalendar"
+ },
+ {
+ "Name": "dxCheckBox"
+ },
+ {
+ "Name": "dxColorBox"
+ },
+ {
+ "Name": "dxDateBox"
+ },
+ {
+ "Name": "dxLookup"
+ },
+ {
+ "Name": "dxNumberBox"
+ },
+ {
+ "Name": "dxRadioGroup"
+ },
+ {
+ "Name": "dxSelectBox"
+ },
+ {
+ "Name": "dxSlider"
+ },
+ {
+ "Name": "dxSwitch"
+ },
+ {
+ "Name": "dxTagBox"
+ },
+ {
+ "Name": "dxTextArea"
+ },
+ {
+ "Name": "dxTextBox"
+ }
+ ],
+ "Renamings": {
+ "dxAutocomplete": "Autocomplete",
+ "dxCalendar": "Calendar",
+ "dxCheckBox": "CheckBox",
+ "dxColorBox": "ColorBox",
+ "dxDateBox": "DateBox",
+ "dxLookup": "Lookup",
+ "dxNumberBox": "NumberBox",
+ "dxRadioGroup": "RadioGroup",
+ "dxSelectBox": "SelectBox",
+ "dxSlider": "Slider",
+ "dxSwitch": "Switch",
+ "dxTagBox": "TagBox",
+ "dxTextArea": "TextArea",
+ "dxTextBox": "TextBox"
+ }
+ },
"VizTheme": {
"Items": [
{
diff --git a/src/core/nested-option.ts b/src/core/nested-option.ts
index ec1c53115..7006c55bb 100644
--- a/src/core/nested-option.ts
+++ b/src/core/nested-option.ts
@@ -15,8 +15,8 @@ export abstract class BaseNestedOption implements INestedOptionContainer, IColle
protected abstract get _optionPath(): string;
protected abstract _fullOptionPath(): string;
- constructor(private _element: ElementRef) {
- this._collectionContainerImpl = new CollectionNestedOptionContainerImpl(this._setOption.bind(this));
+ constructor() {
+ this._collectionContainerImpl = new CollectionNestedOptionContainerImpl(this._setOption.bind(this), this._filterItems.bind(this));
}
protected _getOption(name: string): any {
@@ -40,15 +40,14 @@ export abstract class BaseNestedOption implements INestedOptionContainer, IColle
this._hostOptionPath = optionPath;
}
- _template(...args) {
- let container = args[2];
- return container.append(this._element.nativeElement);
- }
-
setChildren(propertyName: string, items: QueryList) {
return this._collectionContainerImpl.setChildren(propertyName, items);
}
+ _filterItems(items: QueryList) {
+ return items.filter((item) => { return item !== this; });
+ }
+
get instance() {
return this._host && this._host.instance;
}
@@ -64,12 +63,17 @@ export interface ICollectionNestedOptionContainer {
export class CollectionNestedOptionContainerImpl implements ICollectionNestedOptionContainer {
private _activatedQueries = {};
- constructor(private _setOption: Function) {}
+
+ constructor(private _setOption: Function, private _filterItems?: Function) {}
+
setChildren(propertyName: string, items: QueryList) {
if (items.length) {
this._activatedQueries[propertyName] = true;
}
if (this._activatedQueries[propertyName]) {
+ if (this._filterItems) {
+ items = this._filterItems(items);
+ }
let widgetItems = items.map((item, index) => {
item._index = index;
return item._value;
@@ -112,6 +116,34 @@ export abstract class CollectionNestedOption extends BaseNestedOption implements
}
}
+export interface OptionWithTemplate extends BaseNestedOption {
+ template: any;
+}
+export function extractTemplate(option: OptionWithTemplate, element: ElementRef) {
+ if (!option.template === undefined || !element.nativeElement.hasChildNodes()) {
+ return;
+ }
+
+ let childNodes = [].slice.call(element.nativeElement.childNodes);
+ let userContent = childNodes.filter((n) => {
+ if (n.tagName) {
+ let tagNamePrefix = n.tagName.toLowerCase().substr(0, 3);
+ return !(tagNamePrefix === 'dxi' || tagNamePrefix === 'dxo');
+ } else {
+ return n.textContent.replace(/\s/g, '').length;
+ }
+ });
+ if (!userContent.length) {
+ return;
+ }
+
+ option.template = {
+ render: (options) => {
+ return options.container.append(element.nativeElement);
+ }
+ };
+}
+
export class NestedOptionHost {
private _host: INestedOptionContainer;
private _optionPath: OptionPathGetter;
diff --git a/templates/nested-component.tst b/templates/nested-component.tst
index 9343df226..e9acce9da 100644
--- a/templates/nested-component.tst
+++ b/templates/nested-component.tst
@@ -3,18 +3,20 @@
import {
Component,
NgModule,
- Host,
+ Host,<#? it.hasTemplate #>
ElementRef,
+ AfterViewInit,<#?#>
SkipSelf<#? it.properties #>,
Input<#?#><#? it.collectionNestedComponents.length #>,
ContentChildren,
+ forwardRef,
QueryList<#?#>
} from '@angular/core';
-import { NestedOptionHost } from '../../core/nested-option';
+import { NestedOptionHost<#? it.hasTemplate #>, extractTemplate<#?#> } from '../../core/nested-option';
import { <#= it.baseClass #> } from '<#= it.basePath #>';
-<#~ it.collectionNestedComponents :component:i #>import { <#= component.className #>Component } from './<#= component.path #>';
-<#~#>
+<#~ it.collectionNestedComponents :component:i #><#? component.className !== it.className #>import { <#= component.className #>Component } from './<#= component.path #>';
+<#?#><#~#>
@Component({
selector: '<#= it.selector #>',
@@ -24,7 +26,7 @@ import { <#= it.baseClass #> } from '<#= it.basePath #>';
'<#= input.name #>'<#? i < it.inputs.length-1 #>,<#?#><#~#>
]<#?#>
})
-export class <#= it.className #>Component extends <#= it.baseClass #> {<#~ it.properties :prop:i #>
+export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasTemplate #> implements AfterViewInit<#?#> {<#~ it.properties :prop:i #>
@Input()
get <#= prop.name #>() {
return this._getOption('<#= prop.name #>');
@@ -38,7 +40,7 @@ export class <#= it.className #>Component extends <#= it.baseClass #> {<#~ it.pr
}
<#~ it.collectionNestedComponents :component:i #>
- @ContentChildren(<#= component.className #>Component)
+ @ContentChildren(forwardRef(() => <#= component.className #>Component))
get <#= component.propertyName #>Children(): QueryList<<#= component.className #>Component> {
return this._getOption('<#= component.propertyName #>');
}
@@ -47,14 +49,16 @@ export class <#= it.className #>Component extends <#= it.baseClass #> {<#~ it.pr
}
<#~#>
- constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, @Host() optionHost: NestedOptionHost, element: ElementRef) {
- super(element);
-<#? it.hasTemplate #>
- this.template = this._template.bind(this);
-<#?#>
+ constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, @Host() optionHost: NestedOptionHost<#? it.hasTemplate #>, private element: ElementRef<#?#>) {
+ super();
parentOptionHost.setNestedOption(this);
optionHost.setHost(this, this._fullOptionPath.bind(this));
}
+<#? it.hasTemplate #>
+ ngAfterViewInit() {
+ extractTemplate(this, this.element);
+ }
+<#?#>
}
@NgModule({
diff --git a/tests/src/core/nested-option.spec.ts b/tests/src/core/nested-option.spec.ts
index 08c05e02c..c641b9936 100644
--- a/tests/src/core/nested-option.spec.ts
+++ b/tests/src/core/nested-option.spec.ts
@@ -60,8 +60,8 @@ export class DxoTestOptionComponent extends NestedOption {
return 'testOption';
}
- constructor(@SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost, element: ElementRef) {
- super(element);
+ constructor(@SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost) {
+ super();
this._pnoh.setNestedOption(this);
this._noh.setHost(this);
@@ -86,8 +86,8 @@ export class DxiTestCollectionOptionComponent extends CollectionNestedOption {
return 'testCollectionOption';
}
- constructor(@SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost, element: ElementRef) {
- super(element);
+ constructor(@SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost) {
+ super();
this._pnoh.setNestedOption(this);
this._noh.setHost(this, this._fullOptionPath.bind(this));
diff --git a/tests/src/ui/form.spec.ts b/tests/src/ui/form.spec.ts
new file mode 100644
index 000000000..6b37dcb35
--- /dev/null
+++ b/tests/src/ui/form.spec.ts
@@ -0,0 +1,87 @@
+/* tslint:disable:component-selector */
+
+import {
+ Component,
+ ViewChildren,
+ QueryList
+} from '@angular/core';
+
+import {
+ TestBed,
+ async
+} from '@angular/core/testing';
+
+import DxForm from 'devextreme/ui/form';
+
+import {
+ DxFormModule,
+ DxFormComponent
+} from '../../../dist';
+
+@Component({
+ selector: 'test-container-component',
+ template: ''
+})
+class TestContainerComponent {
+ formData = {
+ name: 'Unknown'
+ };
+ @ViewChildren(DxFormComponent) innerWidgets: QueryList;
+}
+
+describe('DxForm', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule(
+ {
+ declarations: [TestContainerComponent],
+ imports: [DxFormModule]
+ });
+ });
+
+ function getWidget(fixture) {
+ let widgetElement = fixture.nativeElement.querySelector('.dx-form') || fixture.nativeElement;
+ return DxForm['getInstance'](widgetElement);
+ }
+
+ // spec
+ it('should be able to accept items via nested dxi components (T459714)', async(() => {
+ TestBed.overrideComponent(TestContainerComponent, {
+ set: {
+ template: `
+
+
+
+ `
+ }
+ });
+ let fixture = TestBed.createComponent(TestContainerComponent);
+ fixture.detectChanges();
+
+ let instance = getWidget(fixture);
+ expect(instance.element().find('.dx-textbox').length).toBe(1);
+ }));
+
+ it('should be able to accept items recursively', async(() => {
+ TestBed.overrideComponent(TestContainerComponent, {
+ set: {
+ template: `
+
+
+
+
+
+
+
+
+ `
+ }
+ });
+ let fixture = TestBed.createComponent(TestContainerComponent);
+ fixture.detectChanges();
+
+ let instance = getWidget(fixture);
+ expect(instance.element().find('.dx-textbox').length).toBe(2);
+ }));
+
+});
diff --git a/tests/src/ui/list.spec.ts b/tests/src/ui/list.spec.ts
index 124b3c99a..593e1d4df 100644
--- a/tests/src/ui/list.spec.ts
+++ b/tests/src/ui/list.spec.ts
@@ -326,6 +326,24 @@ describe('DxList', () => {
expect(instance.element().find('.dx-item-content').eq(0).text()).toBe('testTemplate');
}));
+ it('should be able to define item without template', async(() => {
+ TestBed.overrideComponent(TestContainerComponent, {
+ set: {
+ template: `
+
+
+
+ `
+ }
+ });
+ let fixture = TestBed.createComponent(TestContainerComponent);
+ fixture.detectChanges();
+
+ let instance = getWidget(fixture);
+ expect(instance.element().find('.dx-item-content').eq(0).text()).toBe('TestText');
+ }));
+
+
it('should destroy angular components inside template', () => {
let destroyed = false;
@Component({
diff --git a/tools/spec/tests/metadata-generator.spec.js b/tools/spec/tests/metadata-generator.spec.js
index 88ba57e0e..95c5a3c79 100644
--- a/tools/spec/tests/metadata-generator.spec.js
+++ b/tools/spec/tests/metadata-generator.spec.js
@@ -252,19 +252,20 @@ describe("metadata-generator", function() {
.filter(args => args[0] === path).length;
};
- expect(writeToPathCount(path.join("output-path", "nested", "nested-external-property.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "complex-widget.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "another-complex-widget.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "base", "external-property-type.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "nested", "base", "external-property-type-dxi.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "nested", "base", "another-complex-widget-options.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "property.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "nested.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "nested-item-dxi.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "nested", "nested-external-property.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "collection-item.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "collection-item-dxi.json"))).toBe(1);
- expect(writeToPathCount(path.join("output-path", "nested", "external-property.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "collection-item-with-template-dxi.json"))).toBe(1);
- expect(writeToPathCount(path.join("output-path", "nested", "nested-external-property.json"))).toBe(1);
- expect(writeToPathCount(path.join("output-path", "nested", "base", "another-complex-widget-options.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "widget-reference.json"))).toBe(1);
- expect(writeToPathCount(path.join("output-path", "nested", "base", "external-property-type-dxi.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "nested", "external-property.json"))).toBe(1);
expect(writeToPathCount(path.join("output-path", "nested", "external-property-item-dxi.json"))).toBe(1);
});
@@ -382,4 +383,102 @@ describe("metadata-generator", function() {
});
+ describe("collection of complex types", function() {
+
+ beforeEach(function() {
+ setupContext({
+ Widgets: {
+ dxComplexWidget: {
+ Options: {
+ externalProperty: { // DxoExternalProperty
+ ComplexTypes: [
+ 'ExternalPropertyType',
+ 'ExternalPropertyType2'
+ ]
+ },
+ externalPropertyItems: { // DxiExternalPropertyItem
+ IsCollection: true,
+ SingularName: 'externalPropertyItem',
+ ComplexTypes: [
+ 'ExternalPropertyType',
+ 'ExternalPropertyType2'
+ ]
+ }
+ },
+ Module: 'test_widget'
+ }
+ },
+ ExtraObjects: {
+ ExternalPropertyType: {
+ Options: {
+ property: {
+ Options: {
+ nestedProperty1: {}
+ }
+ },
+ property1: {
+ }
+ }
+ },
+ ExternalPropertyType2: {
+ Options: {
+ property: {
+ Options: {
+ nestedProperty2: {}
+ }
+ },
+ property2: {
+ }
+ }
+ }
+ }
+ });
+ });
+
+ it("should write generated data to a separate file for each widget", function() {
+ expect(store.write.calls.count()).toBe(4);
+
+ let writeToPathCount = (path) => {
+ return store.write.calls
+ .allArgs()
+ .filter(args => args[0] === path).length;
+ };
+
+ expect(writeToPathCount(path.join("output-path", "complex-widget.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "nested", "external-property.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "nested", "external-property-item-dxi.json"))).toBe(1);
+ expect(writeToPathCount(path.join("output-path", "nested", "property.json"))).toBe(1);
+ });
+
+ it("should generate matadata", function() {
+ expect(Object.keys(metas).length).toBe(4);
+
+ expect(metas.DxComplexWidget).not.toBe(undefined);
+ expect(metas.DxoExternalProperty).not.toBe(undefined);
+ expect(metas.DxiExternalPropertyItem).not.toBe(undefined);
+ expect(metas.DxoProperty).not.toBe(undefined);
+ });
+
+ it("should generate nested components with merged properties", function() {
+ expect(metas.DxComplexWidget.nestedComponents.map(c => c.className)).toContain('DxoExternalProperty');
+
+ expect(metas.DxoExternalProperty.properties.map(p => p.name)).toEqual(['property', 'property1', 'property2']);
+ expect(metas.DxoExternalProperty.optionName).toBe('externalProperty');
+ });
+
+ it("should generate collection nested components with merged properties", function() {
+ expect(metas.DxComplexWidget.nestedComponents.map(c => c.className)).toContain('DxiExternalPropertyItem');
+
+ expect(metas.DxiExternalPropertyItem.properties.map(p => p.name)).toEqual(['property', 'property1', 'property2']);
+ expect(metas.DxiExternalPropertyItem.optionName).toBe('externalPropertyItems');
+ });
+
+ it("should generate nested components with merged properties of external types", function() {
+ expect(metas.DxComplexWidget.nestedComponents.map(c => c.className)).toContain('DxoProperty');
+
+ expect(metas.DxoProperty.properties.map(p => p.name)).toEqual(['nestedProperty1', 'nestedProperty2']);
+ expect(metas.DxoProperty.optionName).toBe('property');
+ });
+ });
+
});
diff --git a/tools/src/metadata-generator.ts b/tools/src/metadata-generator.ts
index 585ee1815..631a79a82 100644
--- a/tools/src/metadata-generator.ts
+++ b/tools/src/metadata-generator.ts
@@ -12,7 +12,10 @@ function trimDx(value: string) {
}
function trimPrefix(prefix: string, value: string) {
- return value.substr(prefix.length);
+ if (value.indexOf(prefix) === 0) {
+ return value.substr(prefix.length);
+ }
+ return value;
}
export interface IObjectStore {
@@ -143,37 +146,54 @@ export default class DXComponentMetadataGenerator {
this.generateNestedOptions(config, allNestedComponents);
}
+ private getExternalObjectInfo(metadata, typeName) {
+ let externalObject = metadata.ExtraObjects[typeName];
+
+ if (!externalObject) {
+ const postfix = 'Options';
+ if (typeName.endsWith(postfix)) {
+ let widgetName = typeName.substr(0, typeName.length - postfix.length);
+ externalObject = metadata.Widgets[widgetName];
+ typeName = trimPrefix('dx', typeName);
+ }
+ }
+
+ if (!externalObject) {
+ console.warn('WARN: missed complex type: ' + typeName);
+ } else {
+ return {
+ Options: externalObject.Options,
+ typeName: typeName
+ };
+ }
+ }
+
private generateComplexOptionByType(metadata, option, optionName, complexTypes) {
if (option.Options) {
return this.generateComplexOption(metadata, option.Options, optionName, complexTypes, option);
- } else if (option.ComplexTypes && option.ComplexTypes.length === 1) {
+ } else if (option.ComplexTypes && option.ComplexTypes.length > 0) {
if (complexTypes.indexOf(complexTypes[complexTypes.length - 1]) !== complexTypes.length - 1) {
return;
}
-
- let complexType = option.ComplexTypes[0];
- let externalObject = metadata.ExtraObjects[complexType];
-
- if (!externalObject) {
- const postfix = 'Options';
- if (complexType.endsWith(postfix)) {
- let widgetName = complexType.substr(0, complexType.length - postfix.length);
- externalObject = metadata.Widgets[widgetName];
- complexType = trimPrefix('dx', complexType);
+ let result = [];
+ option.ComplexTypes.forEach(complexType => {
+ let externalObjectInfo = this.getExternalObjectInfo(metadata, complexType);
+ if (externalObjectInfo) {
+ let nestedOptions = externalObjectInfo.Options,
+ nestedComplexTypes = complexTypes.concat(externalObjectInfo.typeName);
+
+ result.push.apply(result, this.generateComplexOption(metadata, nestedOptions, optionName, nestedComplexTypes, option));
+ }
+ });
+ if (option.ComplexTypes.length === 1) {
+ let externalObjectInfo = this.getExternalObjectInfo(metadata, option.ComplexTypes[0]);
+ if (externalObjectInfo) {
+ result[0].baseClass =
+ (option.IsCollection ? ITEM_COMPONENT_PREFIX : OPTION_COMPONENT_PREFIX) + externalObjectInfo.typeName;
+ result[0].basePath = inflector.dasherize(inflector.underscore(externalObjectInfo.typeName));
}
}
-
- if (externalObject) {
- let nestedOptions = externalObject.Options;
- let nestedComplexTypes = complexTypes.concat(complexType);
-
- let components = this.generateComplexOption(metadata, nestedOptions, optionName, nestedComplexTypes, option);
- components[0].baseClass = (option.IsCollection ? ITEM_COMPONENT_PREFIX : OPTION_COMPONENT_PREFIX) + complexType;
- components[0].basePath = inflector.dasherize(inflector.underscore(complexType));
- return components;
- } else {
- logger('WARN: missed complex type: ' + complexType);
- }
+ return result;
}
}