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; } }