Developing Creating a new module - happy-geeks/wiser GitHub Wiki
Each Wiser module has a mostly separate code base and can be run by itself. All modules in Wiser are loaded in iframes. This is done to make sure that CSS and javascript from modules cannot break the UI of Wiser or other modules. Just like the main user interface of Wiser, all these modules use the Wiser API for any and all database actions and things like authentication. Some modules need to communicatie with the main UI of Wiser, to show popups or open another module for example, that can be done via javascript by accessing the parent frame and using the postMessage
method. Example:
window.parent?.postMessage({
action: "OpenModule",
actionData: {
moduleId: 123
}
});
Wiser has several built in modules. Before making a new module, always make sure that you can't use one of the default modules for the functionality that you want. The most important one here is the module DynamicItems
. This module can be used in many different ways and can be customized in many different ways. For more information, see this article.
If you are sure that you can't use the DynamicItems
module (or another built-in module) for what you need, you can develop your own. This can also be done in a few different ways. First of all, you should think about whether this module is very specific for a certain project, or if it's more generic and could also be useful for other projects. If it's a generic module, it can be added to the FrontEnd
project in the Wiser repository itself. If the module is specific for a single project, that module should be added to that project instead of the Wiser project. Either way, the module can and will be loaded in an iframe in Wiser.
At the moment, Wiser still uses a mix of jQuery and Vue. The main UI of Wiser and some of the more recent modules use the Vue framework. Most modules still use the jQuery framework. Wiser 2 was made with jQuery and these modules that still use jQuery come from Wiser 2. We use the Kendo UI library for most of the UI components in Wiser. Kendo UI does have a Vue version, but when we started working on Wiser 3, they did not have all components available for Vue yet. By now they probably do, so new modules can be made with Vue instead of jQuery.
Generic modules that are useful for multiple projects should be added to this Wiser repository. In the project FrontEnd
, you'll find a directory called Modules
. Add a subdirectory there with the name of your module. In this new directory, add more subdirectories for separating the files of the module. These will be directories for controllers, views etc. Most modules will need at least the directories Controllers
, Scripts
, Scss
and Views
, but more can be added when needed, such as Services
, Interfaces
etc.
Below are templates/examples for the minimum files that your module will need to work in Wiser. You can then extend that to add the functionality you need:
When you created these directories, you can start setting up the files for the module. Start by adding a new controller in the Controllers
directory of your module. Give this an appropriate name. This controller should use the Area
attribute with the name of your module as it's value and a Route
attribute with Modules/MyModule
. It should inject IBaseService
in the constructor and have an Index
method. That Index
method should generate a ViewModel via the IBaseService.CreateBaseViewModel
. This method accepts any class that inherits BaseViewModel
. This model contains properties for all settings that any Wiser module needs. The CreateBaseViewModel
method will generate a new instance of this class with these default properties all setup correctly. There is also a BaseModuleViewModel
class (located in FrontEnd.Modules.Base.Models
), this one adds a property called BodyCssClass
that you can use in your controller to add any dynamic/custom CSS classes to the <body>
HTML element of your module. You can also create your own ViewModel and make it inherit either BaseModuleViewModel
or BaseViewModel
, if you need more settings for your module.
Here's an example of a controller for a simple module:
using FrontEnd.Core.Interfaces;
using FrontEnd.Modules.Base.Models;
using Microsoft.AspNetCore.Mvc;
namespace FrontEnd.Modules.Dashboard.Controllers;
[Area("MyModule"), Route("Modules/MyModule")]
public class MyModuleController : Controller
{
private readonly IBaseService baseService;
public MyModuleController(IBaseService baseService)
{
this.baseService = baseService;
}
public IActionResult Index()
{
var viewModel = baseService.CreateBaseViewModel<BaseModuleViewModel>();
return View(viewModel);
}
}
Here is an example of a controller with custom settings that are loaded via the URL querystring:
using FrontEnd.Core.Interfaces;
using FrontEnd.Modules.ContentBox.Models;
using Microsoft.AspNetCore.Mvc;
namespace FrontEnd.Modules.ContentBox.Controllers;
[Area("ContentBox"), Route("Modules/ContentBox")]
public class ContentBoxController : Controller
{
private readonly IBaseService baseService;
public ContentBoxController(IBaseService baseService)
{
this.baseService = baseService;
}
public IActionResult Index([FromQuery]ContentBoxViewModel viewModel)
{
viewModel ??= new ContentBoxViewModel();
var defaultModel = baseService.CreateBaseViewModel();
viewModel.Settings = defaultModel.Settings;
viewModel.WiserVersion = defaultModel.WiserVersion;
viewModel.SubDomain = defaultModel.SubDomain;
viewModel.IsTestEnvironment = defaultModel.IsTestEnvironment;
viewModel.Wiser1BaseUrl = defaultModel.Wiser1BaseUrl;
viewModel.ApiAuthenticationUrl = defaultModel.ApiAuthenticationUrl;
viewModel.ApiRoot = defaultModel.ApiRoot;
viewModel.LoadPartnerStyle = defaultModel.LoadPartnerStyle;
return View(viewModel);
}
}
Next, you should add an SCSS file for your module. Add this in the subdirectory Scss
and name it Main.scss
. Then at the top of this SCSS file, add the following:
@import "../../Base/Scss/base.scss";
If you make a module with jQuery, add this below the previous line:
/* This is to improve the user experience, ít's set to `display: none` in the `Index.cshtml` and then back to `block` here, so that users don't see flickering in the UI. With this, they only see the UI once everything is loaded. */
#fullForm {
display: block;
}
Below that, you can add any custom (s)css for your module.
Now it's time to set up the javascript for your module. Add a main.js
file in the Scripts
subdirectory. At the top of this script, add imports for Utils.js, any Kendo scripts you need if you use Kendo UI components, and the SCSS file you just made. Then create a class with the name of your module and a constructor and some other functions to load the Wiser settings, setup authentication etc. The main.js
file is the starting point of your module. You can of course make multiple javascript files if you want to split up your code. You can copy one of the following templates to your module and use that as a starting point (don't forget to replace MyModule
with the actual name of your module).
To use Vue in your module (which is preferred), use the following template:
"use strict";
import { TrackJS } from "trackjs";
import { createApp, ref, isProxy, toRaw } from "vue";
import axios from "axios";
import {AUTH_LOGOUT, AUTH_REQUEST} from "../../../Core/Scripts/store/mutation-types";
import store from "../../../Core/Scripts/store";
// Import services from the shared folder that you need.
import UsersService from "../../../Core/Scripts/shared/users.service";
// Import any components that you need.
import {DropDownList} from "@progress/kendo-vue-dropdowns";
import "../Scss/Main.scss";
(() => {
class Main {
constructor() {
this.vueApp = null;
this.appSettings = null;
// Initialize services from the shared folder that you need.
this.usersService = new UsersService(this);
// Fire event on page ready for direct actions
document.addEventListener("DOMContentLoaded", () => {
this.onPageReady();
});
}
/**
* Do things that need to wait until the DOM has been fully loaded.
*/
onPageReady() {
const configElement = document.getElementById("vue-config");
this.appSettings = JSON.parse(configElement.innerHTML);
if (this.appSettings.trackJsToken) {
TrackJS.install({
token: this.appSettings.trackJsToken
});
}
this.api = axios.create({
baseURL: this.appSettings.apiBase
});
this.api.defaults.headers.common["Authorization"] = `Bearer ${localStorage.getItem("accessToken")}`;
let stopRetrying = false;
this.api.interceptors.response.use(undefined, async (error) => {
return new Promise(async (resolve, reject) => {
// Automatically re-authenticate with refresh token if login token expired or logout if that doesn't work or it is otherwise invalid.
if (error.response.status === 401 && !stopRetrying) {
// If we ever get an unauthorized, logout the user.
if (error.response.config.url === "/connect/token") {
this.vueApp.$store.dispatch(AUTH_LOGOUT);
} else {
// Re-authenticate with the refresh token.
await this.vueApp.$store.dispatch(AUTH_REQUEST, {gotUnauthorized: true});
error.config.headers.Authorization = `Bearer ${localStorage.getItem("accessToken")}`;
// Retry the original request.
this.api.request(error.config).then(response => {
stopRetrying = false;
resolve(response);
}).catch((newError) => {
stopRetrying = true;
reject(newError);
})
}
}
reject(error);
});
});
this.initVue();
}
initVue() {
this.vueApp = createApp({
data: () => {
return {
appSettings: this.appSettings,
// Any other settings or data that your module might need.
}
},
// Add any code that needs to run when the Vue app is created/mounted. See https://vuejs.org/api/options-lifecycle.html for more information.
async created() {
},
async mounted() {
},
computed: {
// Add any computer properties here. See https://vuejs.org/guide/essentials/computed.html for more information.
},
components: {
// Add any components here. See https://vuejs.org/guide/essentials/component-basics for more information.
},
methods: {
// Add any custom methods that your module needs.
}
});
// Let Vue know about our store.
this.vueApp.use(store);
// Mount our app to the main HTML element.
this.vueApp = this.vueApp.mount("#app");
}
}
window.main = new Main();
})();
To use jQuery for your module (only do this when you need something that is not available in Vue), use this template:
import { TrackJS } from "trackjs";
import { Wiser } from "../../Base/Scripts/Utils.js";
// Add any kendo components or other scripts that you need here.
// These are just some examples, remove the ones you don't need:
require("@progress/kendo-ui/js/kendo.notification.js");
require("@progress/kendo-ui/js/kendo.numerictextbox.js");
require("@progress/kendo-ui/js/kendo.dropdownlist.js");
require("@progress/kendo-ui/js/kendo.tabstrip.js");
require("@progress/kendo-ui/js/cultures/kendo.culture.nl-NL.js");
require("@progress/kendo-ui/js/messages/kendo.messages.nl-NL.js");
import "../Scss/Main.scss";
(() => {
/**
* Main class.
*/
class Main {
/**
* Initializes a new instance of Main.
*/
constructor() {
// Set the Kendo culture.
kendo.culture("nl-NL");
// Default settings
this.settings = {
tenantId: 0,
username: "Onbekend"
};
// Add logged in user access token to default authorization headers for all jQuery ajax requests.
$.ajaxSetup({
headers: { "Authorization": `Bearer ${localStorage.getItem("accessToken")}` }
});
// Fire event on page ready for direct actions
$(document).ready(() => {
this.onPageReady();
});
}
/**
* Event that will be fired when the page is ready.
*/
async onPageReady() {
this.mainLoader = $("#mainLoader");
this.isLoadedInIframe = window.parent && window.parent.main && window.parent.main.vueApp;
// Setup processing.
document.addEventListener("processing.Busy", this.toggleMainLoader.bind(this, true));
document.addEventListener("processing.Idle", this.toggleMainLoader.bind(this, false));
// Setup any settings from the body element data. These settings are added via the Wiser backend and they take preference.
Object.assign(this.settings, $("body").data());
if (this.settings.trackJsToken) {
TrackJS.install({
token: this.settings.trackJsToken
});
}
const user = JSON.parse(localStorage.getItem("userData"));
this.settings.oldStyleUserId = user.oldStyleUserId;
this.settings.adminAccountLoggedIn = !!user.adminAccountName;
const userData = await Wiser.getLoggedInUserData(this.settings.wiserApiRoot);
this.settings.userId = userData.encryptedId;
this.settings.tenantId = userData.encryptedTenantId;
this.settings.plainUserId = userData.id;
this.settings.zeroEncrypted = userData.zeroEncrypted;
this.settings.mainDomain = userData.mainDomain;
this.setupBindings();
await this.initializeComponents();
this.toggleMainLoader(false);
}
/**
* Setup all basis bindings for this module.
* Specific bindings (for buttons in certain pop-ups for example) will be set when they are needed.
*/
setupBindings() {
document.addEventListener("moduleClosing", (event) => {
// You can do anything here that needs to happen before closing the module.
event.detail();
});
// Add any custom event bindings that you need here.
}
/**
* Shows or hides the main (full screen) loader.
* @param {boolean} show True to show the loader, false to hide it.
*/
toggleMainLoader(show) {
this.mainLoader.toggleClass("loading", show);
}
/**
* Initialize components that are used in the module.
*/
initializeComponents() {
}
}
// Initialize the Search class and make one instance of it globally available.
window.main = new Main();
})();
The last of the required files is a view. In the View
subdirectory, add another subdirectory with the name of your module. Inside that, add a file called Index.cshtml
. Use one of the following templates to start with for this file.
To use Vue in your module (which is preferred), use the following template:
@using System.Web
@model FrontEnd.Modules.ContentBox.Models.ContentBoxViewModel
@addTagHelper FrontEnd.Core.TagHelpers.WebpackScriptTagHelper, FrontEnd
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Replace "MyModule" with the name of your module. -->
<title>Wiser 3 - MyModule</title>
</head>
<body>
<div id="app" v-cloak>
<!-- Add the HTML for your module here. -->
</div>
<script id="vue-config" type="application/json">
{
"apiBase": "@Model.Settings.ApiBaseUrl",
"apiRoot": "@Model.ApiRoot",
"subDomain": "@Model.SubDomain" ,
"isTestEnvironment": @Model.IsTestEnvironment.ToString().ToLowerInvariant(),
"wiser1BaseUrl": "@Model.Wiser1BaseUrl",
"loadPartnerStyle": @Model.LoadPartnerStyle.ToString().ToLowerInvariant(),
"apiClientId": "@Model.Settings.ApiClientId",
"apiClientSecret": "@Model.Settings.ApiClientSecret",
"trackJsToken": "@Model.Settings.TrackJsToken",
"wiserItemId": @Model.WiserItemId,
"propertyName": "@Model.PropertyName",
"languageCode": "@Model.LanguageCode",
"encryptedUserId": "@Html.Raw(Model.UserId)"
}
</script>
<webpack-script file-name="runtime.js"></webpack-script>
<webpack-script file-name="vendors.js"></webpack-script>
<!-- Replace "MyModule" with the name of your module. -->
<webpack-script file-name="MyModule.js"></webpack-script>
</body>
</html>
To use jQuery for your module (only do this when you need something that is not available in Vue), use this template:
@model FrontEnd.Core.Models.BaseViewModel
@addTagHelper FrontEnd.Core.TagHelpers.WebpackScriptTagHelper, FrontEnd
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta charset="utf-8" />
<!-- Replace "MyModule" with the name of your module. -->
<title>Wiser 3 - MyModule</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- If you don't use Kendo components, you can remove these 2 lines. -->
<link href="//kendo.cdn.telerik.com/2022.2.621/styles/kendo.common-material.min.css" rel="stylesheet" />
<link href="//kendo.cdn.telerik.com/2022.2.621/styles/kendo.material.min.css" rel="stylesheet" />
<script>
window.wiserVersion = "@Model.WiserVersion?.ToString()";
</script>
<style>
/* This is to improve the user experience, ít's set back to `display: block` in the `Main.scss`, so that users don't see flickering in the UI. With this, they only see the UI once everything is loaded. */
#fullForm {
display: none;
}
</style>
</head>
<body data-module-id="706"
data-wiser-api-root="@Model.ApiRoot"
data-is-test-environment="@Model.IsTestEnvironment.ToString().ToLowerInvariant()"
data-sub-domain="@Model.SubDomain"
data-wiser-api-authentication-url="@Model.ApiAuthenticationUrl"
data-api-client-id="@Model.Settings.ApiClientId"
data-api-client-secret="@Model.Settings.ApiClientSecret"
data-track-js-token="@Model.Settings.TrackJsToken">
<form id="fullForm">
<!-- Replace "my-module" with the name of your module. -->
<div id="wiser" class="my-module">
<!-- Add the HTML for your module here. -->
</div>
</form>
<!-- This is the loader that is shown while the page is loading. -->
<div id="mainLoader" class="fullscreen-loader loading">
<div class="loader-icon">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<webpack-script file-name="runtime.js"></webpack-script>
<webpack-script file-name="vendors.js"></webpack-script>
<webpack-script file-name="Processing.js"></webpack-script>
<webpack-script file-name="kendo-ui.js"></webpack-script>
<!-- Replace "MyModule" with the name of your module. -->
<webpack-script file-name="MyModule.js"></webpack-script>
</body>
</html>
Lastly, you need to open the file webpack.config.js
and add your main.js
to the entry property in this file, so that webpack knows to build your module.