Developing an External Module
Developers can access external module hookup scripts to debug and troubleshoot their own modules integrated with the CMS. While external modules integrate with the CMS, these modules are separate from the CMS.
For security, this model uses CORS (cross-origin resource sharing) scripting. All external modules require CORS support to integrate with the CMS.
Hookup Script
Access the hookup script for an external module by opening ExternalModules.json in [Drive]:[path-to-CMS-root-folder]\site\App_Data and copying the URL value for the respective external module. Enter the URL in an open web browser.
Review the hookup script file. Navigate to document.body.onload = async () =>
, and note the @src
attribute. When documents load in the CMS with the external module, the system takes the @src
attribute from the correlating element @id
. If developers inspect the source within the CMS with web browser development tools, they will see tags with the @src
attribute applied, along with the correlating @id
. This tagging gets invoked, and the system uses them to create the CMSHookup
object.
The constructor
passes in the external module URL, which is pure JavaScript ES6.
As for the async setup
code, this data resides in the respective external module repository. Developers can take this directly from the repository. Developers can locate the hookup script via const scriptsFinal = scripts
and use the script as a boiler template.
This script creates an iframe for the external module.
See the following sample hookup script for an example:
/**
* The script that looks like module to CMS
*/
class CMSHookup {
_moduleUri;
constructor(moduleUri) {
this._moduleUri = moduleUri.toLowerCase();
//create a iframe to hold the module
this.iframe = document.createElement('iframe');
this.iframe.style.position = "absolute";
this.iframe.style.left = "-9999px";
document.body.appendChild(this.iframe);
}
async setup() {
const scripts = [
"polyfills.js",
"main.js"
];
//get version from server
const rootUrl = this._moduleUri
const ver = "0";
const scriptsFinal = scripts
.map(s => `${s}?${ver}`)
.join(";");
this.iframe.src = `CMS/ExternalModule?root=app-root`
+ `&url=${encodeURIComponent(this._moduleUri)}`
+ `&scripts=${encodeURIComponent(scriptsFinal)}`;
}
createScriptElement(src) {
const scriptEle = document.createElement("script");
scriptEle.setAttribute("src", src);
scriptEle.setAttribute("type", "module");
this.iframe.contentWindow.document.body.appendChild(scriptEle);
}
}
//create hookup instance when body finished loading
document.body.onload = async () => {
let url = document.getElementById("TestModule")?.getAttribute("src") || "";
let pos = url.indexOf("/hookup/");
if (pos > -1) {
url = url.substring(0, pos);
}
window.modules_CMSHookup = new CMSHookup(url);
await window.modules_CMSHookup.setup();
}
IFrame
The iframe points to the external module view and passes the component URL and all required scripts. The component URL is the base URL where the iframe will be hosted. The system injects hookup instance in front of the iframe.
Additionally, keep in mind that each script appends the external module version.
Knowing the version number can help with troubleshooting external module caching issues. The scripts fetch the version number from the server via the route rootUrl/Common/ver.
Note that the final script parameter includes the scripts. All the external module views pass through this final script parameter. Keep in mind that the scripts reside within the iframe and not within the CMS. The system requires developers to pass in all style modifications separately via the external module's Styles URL value in ExternalModules.json.
Component Development
The required scripts are a result of the external module build. The actual loaded script resides within the corresponding component.
For Angular development, keep in mind checks in the following files.
The external modules system is not part of the CMS, so the system will not perform automatic Angular change detections. Be sure to inject ChangeDetectRef
into the component and perform manual change detections along with the injection.
app.component.ts
This app component defined by app.component.ts listens to the CMS events. In particular, note the componentInitalized
event. The NG reference is defined within const ngref = topWindow.NG_REF as CMSAppComponent;
, which is, in a sense, the root component for the CMS from the top window. This code lives within the context of the iframe. The system requires the reference to the top window for all CMS functionality.
Keep in mind that the code filters out what needs to be handled in the system. If no element data is available, then this filtering gets bypassed.
The NG reference resides within the app component via the following code:
const ngref = topWindow.NG_REF as CMSAppComponent;
if (!ngref)
return;
Developers can review this in the Google Chrome development tools Console tab. The tab provides developers with a listing of what the object includes (e.g., how IntelliSense works with the object). Developers can work with this directly within the Console tab or within a script editor. They can add a new window.top.ng_ref
watch in the source debugging.
In other cases where, for example, developers want to access a toolbar to insert a new utility button, they can run a query selector with the parent element to locate a particular button. Then, they can insert code to add the new utility button before the existing one. This injects a new component into the existing UI that's already initialized.
Button Creation
To create the external module button, the system uses a dynamic component creator to insert the button on, for example, an ICE field. The created button is then associated with an anchor.
After creating the button, the system adds a mutation observer, an object that allows developers to monitor changes with the correlating node. Particular changes to monitor include @class
attribute changes. If a class changes based on the eraser buttons, then the change will be matched with an anchor. This means that, if the class is hidden, the class will remain as hidden.
When the mutation observer initializes, developers can see the node. In the case of the external module button, this allows the button state to be synced with other objects.
See the following code for a custom button example:
export class AppComponent {
observableSubTeardowns = [];
constructor() {
const ngref = window.top['NG_REF']
this.observableSubTeardowns.push(ngref.componentInitialized.subscribe(evt => {
//get the whole edit form model
const allElesData = (window.top as any).NG_REF.editFormDataService.serializedModel;
if (!allElesData)
return; //no edit form yet, skip
//create button for edit form index fields
this.createButtonOnField(evt, allElesData);
}));
this.observableSubTeardowns.push(ngref.componentDestroyed.subscribe(comp => {
if (comp.constructor.name === "StructuralField") {
//remove the button from the field upon it being destroyed
evt.component.theButton?.parentElement?.removeChild(theButton);
delete evt.component.theButton;
}
}));
}
private _mutationObserver;
private createButtonOnField(evt: ViewInitModel, allElesData: any) {
if (evt.component.constructor.name === "StructuralField") {
//if text or html field, create an button next to the eraser button
if ((evt.component as any).isText() || (evt.component as any).isHtml()) {
const fieldNode = (evt.component as any).theButtonElement.nativeElement;
const toolbarNode = fieldNode?.querySelector(".titleBarButtons");
const eraserButtonNode = toolbarNode?.querySelector(".igx-fa-eraser");
if (!!eraserButtonNode) {
//create a node before the eraser button
theButton = document.createElement("button");
theButton.innerHTML = "The Button";
theButton.disabled = true;
//TODO: hook up event on the button to do something...
toolbarNode.insertBefore(theButton, eraserButtonNode);
//add classes after component created, since the host attribute will override the classes
theButton.classList.add("igx-fa");
theButton.classList.add("hidden");
theButton.style.marginRight = "6px";
this._mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
const eraserButton = mutation.target as HTMLElement;
if (eraserButton.classList.contains("hidden")) {
theButton.classList.add("hidden");
} else {
theButton.classList.remove("hidden");
}
}
});
});
//only enable the button when the details are created
const sub = (evt.component as any).detailsCreated
.subscribe(() => {
sub.unsubscribe();
if ((evt.component as any).isHtml())
this.delay(1000); //wait for tinymce to complete building
theButton.disabled = false;
});
//add the prop on the edit form field
evt.component.theButton = theButton;
}
}
}
}
}