NOTE: Latest supported version: Sitefinity CMS 14.2.7924.0
The sample code in this repo implements a decoupled frontend SPA renderer for Sitefinity CMS. It uses the Sitefinity Layout API services to render the layout and widget content in Sitefinity MVC pages. This implementation also works with personalized pages and personalized widgets to enable per-user content personalization. The sample code uses the Angular framework.
Angular developers that wish to develop with Sitefinity CMS and the Angular framework and utilize the WYSIWYG page editor.
The WYSIWYG page editor of Sitefinity works with reusable components called widgets. Leveraging the power of the editor, developers can build custom SPA frontends for their users. This sample demonstrates how to integrate a custom front-end framework (such as Angular) in the page editor.
The whole renderer framework for the angular renderer is already built (including integration with the WYSIWYG editor), so all there is to do is just write 'Angular widgets'. Developing widgets for the Angular Renderer is just like developing plain Angular Components. There are some integration points which we will go through. For this purpose find the bellow Hello World tutorial
In order to build our own custom widget, we need to first create a folder that will hold our files. We will name our widget - ‘Hello World’ and it will be placed in the ‘hello-world' folder(under src/app/components). We then create a file in that folder called ‘hello-world.component.ts’ that will host our angular component. It will have the following content:
import { Component } from "@angular/core";
import { BaseComponent } from "../base.component";
import { HelloWorldEntity } from "./hello-world-entity";
@Component({
templateUrl: "hello-world.component.html",
selector: "app-hello"
})
export class HelloWorldComponent extends BaseComponent<HelloWorldEntity> {
}
We need to add this component to our app.module.ts file in both 'declarations' and 'entrycomponents'. Additionally add the 'app-hello' tag name to the styles.scss file to make the tag a block element.
Create a file called 'hello-world-entity.ts' with the following contents:
export class HelloWorldEntity {
Message!: string;
}
**The entity class will hold the properties that are populated through our designer. We will have a single property called 'Message'. **
We add a simple html file called - 'hello-world.component.html' with the following contents:
<h1>{{Properties.Message}}</h1>
The data entered from the designer will be under the Properties object
Second - we need to define the designer. This is done by creating a 'designer-metadata.json file' (name does not matter) and it will hold the metadata that will be read from the widget designer in order to construct the designer.
{
"Name":"HelloWorld",
"Caption":"HelloWorld",
"PropertyMetadata":[
{
"Name":"Basic",
"Sections":[
{
"Name":"Main",
"Title":null,
"Properties":[
{
"Name":"Message",
"DefaultValue":null,
"Title":"Message",
"Type":"string",
"SectionName":null,
"CategoryName":null,
"Properties":{
},
"TypeChildProperties":[
],
"Position":0
}
],
"CategoryName":"Basic"
}
]
},
{
"Name":"Advanced",
"Sections":[
{
"Name":"AdvancedMain",
"Title":null,
"Properties":[
],
"CategoryName":null
}
]
}
],
"PropertyMetadataFlat":[
{
"Name":"Message",
"DefaultValue":null,
"Title":"Message",
"Type":"string",
"SectionName":null,
"CategoryName":null,
"Properties":{
},
"TypeChildProperties":[
],
"Position":0
}
]
}
Once we have the above two files ready, we need to register the component implementation and the designer metadata with the Angular Renderer.
For the component we need to go to the file render-widget-service and to add a new entry to the TYPES_MAP object like so:
import { HelloWorldComponent } from "../components/hello-world/hello-world.component";
export const TYPES_MAP: { [key: string]: Function } = {
"SitefinityContentBlock": ContentComponent,
"SitefinitySection": SectionComponent,
"SitefinityContentList": ContentListComponent,
"HelloWorld": HelloWorldComponent
};
For the designer we need to go to the file renderer-contract and in the metadataMap object to add a new entry like so:
import helloWorldJson from '../components/hello-world/designer-metadata.json';
@Injectable()
export class RendererContractImpl implements RendererContract {
private metadataMap: { [key: string]: any } = {
"SitefinityContentBlock": sitefinityContentBlockJson,
"SitefinitySection": sitefinitySectionJson,
"SitefinityContentList": sitefinityContentListJson,
"HelloWorld": helloWorldJson
}
Finally we need to register the widget to be shown in the widget selector interface. Go to the file content-widgets.json and add a new entry after the SitefinityContentBlock registration:
{
"name": "HelloWorld",
"addWidgetName": null,
"addWidgetTitle": null,
"title": "Hello world",
"initialProperties": []
}
Notice that everywhere above we are using the 'HelloWorld' name to register our component. This is a unique identifier for out component and is used everywhere where it is referenced.
In order to minimize the cost and not host two applications (as the case with the .NET Renderer), the developer can host the production files on the file system of the CMS application under the following folder template(casing is important for the renderer folder):
/sitefinity/public/renderers/{rendererName}
/sitefinity/public/renderers/Angular
/sitefinity/public/renderers/React
The above folders can be used for development as well. Just configure the output folder for the build. After the files are deployed, reloading a page will take into account the new files.
NOTE Be sure to configure the deployUrl property in angular.json. Currently it is configured as '/sitefinity/public/renderers/Angular' in two places. Both need to be replaced if you plan on develop with the Sitefinity .NET Renderer to '/sfrenderer/renderers/Angular'
All of the components must inherit from the BaseComponent class and must define their own entity class as a first generic argument. This 'entity' class will hold the properties that will be populated through the widget designer interface. So, when defining an 'Angular Widget', you will be working with the values that are entered through the automatically generated widget designer. Once your widget inherits from the base component you will automaitcally have access to the Model property, where TEntityType is the type of the object that will be populated through the designer interface. When the widgets are rendered in 'edit mode', the widget wrapping element is decorated with some additional attributes. These attributes are added only in edit mode and are not added on the live rendering. They are needed so the page editor knows which type of widget is currently being rendered.
Each widget must have a wrapper tag. Having two tags side by side on root level of the html of the component is not supported.
The fields that appear are defined through a JSON file in the 'renderer-contract.ts' file. All the capabilities of the automatic widget designers are available for all the types of renderers delivering a universal UI. We have provided an all-properties.json file that holds all the available combinations for properties that you can use for your widgets, and you can simply copy-paste from there. Additionally, we have provided three widget implementations that each have their own designer file as a reference.
Some clarification on the schema of the json file. It holds the followin properites:
- Name – This much match the name of the widget with which it is registered in the renderer-contract.ts file and the render-widget-service.ts file.
- Caption – The user friendly name of the widget that will be presented when the designer is opened
- PropertyMetadata and PropertyMetadata flat both hold the metadata for all of the properites, with the exception that PropertyMetadata holds the metadata in a hierarchical manner. This property is used to construct widget designers. Whereas the flat collection is used for validation.
- The more used metadata properties that define each field are: Name – the name of the field, DefaultValue – the default value Title – user friendly name of the field, Type - the type of the property – see the all-properties.json file for all types of fields. SectionName: the name of the section in which this field belongs CategoryName: null for Basic or Advanced Properties: holds additional metadata properties - see the all-properties.json file for more examples.