@progress/sitefinity-widget-designers-sdk
v0.2.9
Published
This package aims to create a parity for widget designer generation similar to the [autogenerated widget designers](https://www.progress.com/documentation/sitefinity-cms/autogenerated-field-types) in Sitefinity. Due to some limitations in Typescript and J
Downloads
2,650
Maintainers
Readme
Sitefinity widget designers SDK
This package aims to create a parity for widget designer generation similar to the autogenerated widget designers in Sitefinity. Due to some limitations in Typescript and Javascript, there are several cases where additional data must be provided (see usage guide). Full API documentation could be found in the api.md in the source.
Intallation
via NPM:
npm i @progress/sitefinity-widget-designers-sdk --save
via yarn:
yarn add @progress/sitefinity-widget-designers-sdk
The package relies on the Typescript decorators, which requires adding support for decorators to your tsconfig.json file:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Concept
The metadata, generated from this package, aims to equal the metadata generated by Sitefinity or the ASP.NET Core external renderer and replace the usage of JSON metadata files in external renderers.
Usage
Exctracting the metadata
import { EntityMetadataGenerator, MetadataModel } from "@progress/sitefinity-widget-designers-sdk";
const designerMetadata: MetadataModel = EntityMetadataGenerator.extractMetadata(widgetEntity);
Extracting default values from the metadata
The the metadata generator will go through the designer JSON definition, extract and parse the default value for each property is such is set. Normally the return type would match the type definition of the widget entity model.
import { EntityMetadataGenerator } from "@progress/sitefinity-widget-designers-sdk";
const defaultValues: { [key: string]: any } = EntityMetadataGenerator.extractDefaultValues(designerMetadata);
Parsing serialized widget model values
Sitefinity preserves the widget data on the server in stringified JSON form so when it is received, a parsing depending on the property type is necessary to match the entity model properties' types.
import { EntityMetadataGenerator } from "@progress/sitefinity-widget-designers-sdk";
const deserializedValues: { [key: string]: any } = EntityMetadataGenerator.parseValues(valuesFromServerCall, designerMetadata);
Creating a widget entity and property metadata
The widget model should be decorated using the WidgetEntity decorator.
import { WidgetEntity } from "@progress/sitefinity-widget-designers-sdk";
@WidgetEntity("SitefinitySection", "Section")
export class SectionEntity {
@Category('QuickEdit')
CssSystemGridSize: number = 12;
}
DataModel and Model property decorators
Properties that have complex models as types need their data models explicitly specified (while in C# these types and their subproperties would be generated OOB). For more details on complex objects see here
import { Model, WidgetEntity} from "@progress/sitefinity-widget-designers-sdk";
@WidgetEntity("ComplexWidget", "Complex Widget")
class EntityWithComplexProperties {
@DisplayName("Complex property")
@DataModel(ComplexPropModel)
ComplexProperty?: ComplexPropModel;
}
@Model()
class ComplexPropModel {
@DataType("string")
ChildString: string | null = null;
}
is the same as in C#
public class EntityWithComplexProperties {
[DisplayName("Complex property")]
public ComplexPropModel ComplexProperty { get; set; }
}
public class ComplexPropModel {
public string ChildString { get; set; }
}
Default values
The default value of the properties would be taken either from the DefaultValue decorator or a default value set to the property in the class declaration:
@DisplayName("Number prop")
@DefaultValue(42)
@DataType("number")
NumberProperty: number;
// DeafultValue="42" Type="number"
If a default value is set to basic types, the DataType decorator can be omitted. The following code would produce the same output:
@DisplayName("Number prop")
NumberProperty: number = 42;
// DeafultValue="42" Type="number"
Property display configuration
Sections can be arraged either manually or via the SectionsOrder class decorator.
Properties can be arranged in sections in the autogenerated designers and ordered by a given index via the ContentSection decorator.
Sections can be separed in categories: Basic, Advanced, QuickEdit. Setting the category to the property will move the property there and create a section for it in that category.
Properties can be given several types of description via the Description and DescriptionExtended decorators.
@WidgetEntity('CustomEntity', 'Custom entity')
@SectionsOrder('First section', 'Second section')
class Entity {
@ContentSection('Second section', 0)
SecondSectionFirstProperty: any;
@ContentSection('Second section', 1)
SecondSectionSecondProperty: any;
@ContentSection('First section', 0)
@Description('simple description')
FirstSectionFirstProperty: any;
@Category('Advanced')
@DescriptionExtended({
Description: '...',
InlineDescription: '...',
InstructionalNotes: '...'
})
FirstPropertyAdvancedCategoryNewSection: any;
}
Field type
The property editor field is defined via the DataType decorator. Besides the basic types such as string, number, bool/boolean, there are some predefined types in the KnownFieldTypes enum as well as the several variations in the ComplexType enum. Types that are a variation of choices should have a Choice decorator defined as well.
@DataType('string')
StringProperty: string | null = null;
// @DataType('string') for basic types the decorator can be omitted if there is a default value of the same type set
StringProperty: string = 'default value';
@DataType(KnownFieldTypes.Choices)
@Choice([{Title: 'First choice', Value: 'first'}, {Title: 'Second choice', Value: 'second'}])
ChoiceProperty: string | null = null;
// checkbox
@DataType(KnownFieldTypes.CheckBox)
YesNoProperty: boolean = false;
// choices with multiple selection
@DataType(KnownFieldTypes.Choices)
@Choice([{Title: 'First choice', Value: 'first'}, {Title: 'Second choice', Value: 'second'}], true)
MultipleChoiceProperty: string = 'first';
// chip choice
@DataType(KnownFieldTypes.ChipChoice)
@Choice({Choices: [
{Title:'Yes', Name: 'Yes', Value: 'True', Icon: null},
{Title:'No', Name: 'No', Value: false, Icon: null}
]})
ChipChoiceSingle: boolean = true;
Validation
Property values can be validated on the front end based on decorators set in the entity. Most of them can receive and custom validation failed error message.
- Required
- Readonly
- Range
- MinLength
- MaxLength
- RegularExpression:
- Url (predefined regexp)
- EmailAddress (predefined regexp)
- DecimalPlaces (for floating point numbers)
- StringLength
Associated content or media items
A property can refer to a content item of any of the types that are found or created in Sitefinity. The autogenerated designer will display a content selector for that content type. The full type name should be used - e.g. for pages: Telerik.Sitefinity.Pages.Model.PageNode
Related content and media
@Content({
Type: 'Telerik.Sitefinity.Pages.Model.PageNode',
AllowMultipleItemsSelection: false
})
SelectedPage: MixedContentContext = null;
// Media item is shorthand for Content
@MediaItem('images', true)
SelectedMedia: MixedContentContext = null;
@Content({
Type: 'Telerik.Sitefinity.Libraries.Model.Image',
AllowMultipleItemsSelection: false
})
FullDecoratorSelectedMedia: MixedContentContext = null;
Taxonomy
@TaxonomyContent({Type: 'Taxonomy_Tags'})
SelectedTags: MixedContentContext = null
// or
@TaxonomyContent({Type: 'Tags'})
ShorthandSelectedTags: MixedContentContext = null
Conditional visibility
Fields can be shown or hidden based on other property values using the ConditionalVisibility decorator.
FirstProperty: string = '';
@ConditionalVisibility({
conditions: [{
fieldName: 'FirstProperty',
operator: 'Equals',
value: 'show second property'
}]
})
SecondProperty: string = '';
Complex objects
In the autogenerated designers in Sitefinity there are several ways to define and to represent properties with complex object types.
@DataModel(PropModel)
// @DataType(ComplexType.Complex)
ComplexObjProp: PropModel = null; // => renders a section with the model
@DataModel(PropModel)
// @DataType(ComplexType.Complex)
@TableView("TableTitle")
ComplextTableProp: PropModel = null; // => renders the model as a table
@DataModel(PropModel)
@DataType(ComplexType.Enumerable)
MultipleComplexObj: PropModel[] = null; // renders editable rows for each instance of the model
@DataModel(PropModel)
// @DataType(ComplexType.Enumerable)
@TableView({ Reorderable: true, Selectable: true, MultipleSelect: true })
MultipleComplexObj: PropModel[] = null; // renders full editable table with a row for each instance of the model
...
@Model()
class PropModel {
stringProp: string = "default-value";
numberProp: number = 13;
...
}
Collections
Having a properties of collection types, it should be explicitly specified:
Array
Currently arrays are supported for objects/classes and string types. Other types would be rendered as a simple string field.
import { ComplexType, DataModel, DataType } from "@progress/sitefinity-widget-designers-sdk";
// would receive Type="enumerable"
@DataType(ComplexType.Enumerable, "string")
ValidStringCollection: string[] = null;
// would receive Type="enumerable"
@DataModel("string")
@DataType(ComplexType.Enumerable)
AlsoValidStringCollection: string[] = null;
// would receive Type="enumerable"
// with TypeChildProperties of the DataModel
@DataModel(ObjectModel)
@DataType(ComplexType.Enumerable)
ObjectCollection: ObjectModel[] = null
// invalid properties
// would receive Type=null
@DataType(ComplexType.Enumerable)
InvalidCollection: boolean[] | number[] = null;
Dictionary
Currently a collection of type dictionary is supported only when LengthDependsOn property decorator is also present and the value type is an object/class. Otherwise the field would be rendered as a simple string field. For collections of objects that don't depend on another property, we suggest using an array.
import { ComplexType, DataType, Model } from "@progress/sitefinity-widget-designers-sdk";
@DataModel(DictValue)
@DataType(ComplexType.Dictionary)
@LengthDependsOn("PropName", "PropDisplayName", "PropTitle")
ObjectDictionary: { [key: string]: DictValue } = null;
....
@Model()
class DictValue {
@DataType("number")
Int: number;
@DataType("string")
Str: string;
}
Nested complex objects, collections with multiple properties
Nesting single objects
Single objects can be nested inside of each other and will appear as section in the parent object in the desginer. The objects on root will be displayed as an expanding section.
@Model()
class SimpleObject {
ChildProp1: string = '';
ChildProp2: string = '';
}
@Model()
class ParentComplex {
@ContentSection('', 0)
ParentProp1: string = '';
@ContentSection('', 1)
@DataModel(SimpleObject)
SimpleObject: SimpleObject;
}
@WidgetEntity('NestedSimpleObjects', 'Nested simple objects')
class NestedSimpleObjects {
@DataModel(ParentComplex)
ParentComplexObject: ParentComplex;
}
Complex objects/collections as tables
The limitations and suggested best practices, stated below, are based on the resulting usibility of the generated UI.
Nesting collections of objects
Nesting collections inside of collections (table views) are not allowed and will result in a string field instead of the child collection (it can be used to manually store the JSON representation of the values of the field). The only exception is a list of string as a child of the model for the collection.
@Model()
class ObjectWithStringList {
@ContentSection('', 0)
Prop1: string = '';
@ContentSection('', 1)
@DataType(ComplexType.Enumerable, 'string')
StringList: string[] = [];
}
@WidgetEntity('ComplexObjectsWithStrings', 'ComplexObjectsWithStrings')
class ComplexTableWithStringList {
@DataType(ComplexType.Enumerable)
@DataModel(ObjectWithStringList)
ComplexObjectsList: ObjectWithStringList[] = [];
}
The currently suggested approach to achieving such structure in the designer is to assign the child colleciton to a property, which will collapse the field to a pencil button and open it in a new dialog:
@Model()
class SimpleObject {
ChildProp1: string = '';
ChildProp2: string = '';
}
@Model()
class ObjectListHolder {
@DataModel(SimpleObject)
@DataType(ComplexType.Enumerable)
ObjectCollection: SimpleObject[] = [];
}
@Model()
class FirstLevelObject {
@ContentSection('', 0)
FirstLevelField = 1;
@DataModel(ObjectListHolder)
ObjectListHolderProperty: ObjectListHolder | null = null;
}
@WidgetEntity('ComplexObjectsWithObjectsLists', 'ComplexObjectsWithObjectsLists')
class ComplexTableWithObjectList {
@DataType(ComplexType.Enumerable)
@DataModel(FirstLevelObject)
FirstLevelObjectsList: FirstLevelObject[] = [];
}
![complex objects with objects list root](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAu4AAAGRCAYAAAA6gTZDAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH6AsWEDEeFBWTvAAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAACAASURBVHja7N1/cFPngf/7jxGVgS8iJnZJK0oWJb