Serenity 8.2.1 Release Notes (2024-01-20)

This release marks a significant milestone as we transition away from jQuery towards a React-like JSX programming model based on jsx-dom.

As the name suggests, unlike React, jsx-dom does not utilize VDOM (virtual DOM). This shift makes it more seamless for us to migrate from our existing widget model, which was designed over 10 years ago around jQuery UI widgets.

While VDOM has its own set of advantages, they do not outweigh the issues that may arise when dealing with code or external components that manipulate the DOM directly.

We have chosen jsx-dom as our primary programming model, but it is still possible to use React, Preact, Vue, or any other framework in various parts of your applications. For example, in our latest dashboard page, the Chat widget is written using Preact, while other widgets on the page use jsx-dom.

In this version, we are removing jQuery UI from StartSharp/Serene templates. The datepicker is replaced with Flatpickr in StartSharp, and other features like dialogs, draggable elements, and sortable components are either rewritten or replaced with alternatives that do not have a jQuery/UI dependency, such as Bootstrap modals, SortableJS, etc.

You can still include jQuery UI in your project, and Serenity will detect and use jQuery UI for dialogs, tabs, datepicker, and play nicely with event/cleanup mechanisms in jQuery UI. However, it is not recommended, as we plan to remove integration points in a future version.

While we haven't removed the jQuery dependency itself, as we still have some components that need to be replaced with vanilla versions that do not use jQuery, namely:

  • Select2: Used for dropdowns, lookups, autocomplete, etc. We are evaluating Tomselect and other alternatives, or we will rewrite it without jQuery dependency, just as we did before with SleekGrid.
  • Autonumeric: Used for numeric/decimal editors. Its latest version does not depend on jQuery, so we may be able to switch unless there are significant breaking changes.
  • Validate: We are considering rewriting it or replacing it with HTML form validation API and Bootstrap.
  • Colorbox: Used only to zoom upload thumbnails. Should be easier to replace.
  • MaskedInput: We may remove it and let you choose one you like.

Even though we have not yet removed jQuery and the above components from appsettings.bundles.json, all the dependencies in our core packages, common features, and pro features are isolated. They can work with no jQuery loaded on the page. Of course, lookup editors, etc., won't function properly, as they depend on Select2 for proper operation.

Adjusting Constructor Arguments for Widget Integration with JSX

Most of our widgets, such as grids and editors, typically have a constructor as shown below:

export class SomeGrid extends EntityGrid {
    constructor(container: JQuery, opt: SomeOptions) {
    }
}

The first argument is a container or target element on which the widget will be created. This argument is of type jQuery instance containing a single target element, as our widgets do not support creating multiple instances or using multiple target elements.

The second argument is an options object that varies based on the widget's requirements.

For all widgets, except dialogs, the first jQuery-type argument is mandatory. Dialogs auto-create their div element and append it to the document body, so their constructors look like:

export class SomeDialog extends EntityDialog {
    constructor(opt?: SomeOptions) {
        //...
    }
}

We usually do not call these constructors directly but use Widget.create or similar methods to automatically create a suitable element for the widget. For example, a StringEditor needs an <input type="text" /> target element, while a dialog requires a div.

This structure is not very compatible with the JSX/React component model, as both functional and class components expect the first argument to be a props object, not a jQuery instance. The underlying JSX runtimes pass the attributes set in JSX to the target component in the first argument.

Another difference is that while our widgets can attach themselves to existing elements and render only the contents of the target element (aside from adding a few classes), JSX class-based components are expected to return their element(s) or a document fragment from their Render method or as the result of a functional component.

As we are removing the jQuery dependency and aiming for easy integration of our widgets with the JSX-based component model, we need to begin by modifying constructors. Please apply the following changes in order by using search and replace in Regex mode for all .cshtml, .ts, and .tsx files under the Modules directory after updating Serenity packages and replacing Serenity.Scripts with Serenity.Corelib.

Replacing constructors with a jQuery container argument

This update addresses constructors that resemble:

constructor(container: JQuery) { 
    super(element)
// with 
constructor(props: any) { 
    super(props)
  • Search for: (constructor\s*\()\s*\w+\s*:\s*JQuery\s*(\)[\s\n]*\{\s*super\s*\()\s*\w+\s*\)
  • Replace with: $1props: any$2props)

Replacing constructors with a jQuery container argument and an option

In this case, we are replacing constructor blocks like:

constructor(container: JQuery, opt?: SomeOpt) { 
    super(container, opt)
// with 
constructor(props: { element: any } & SomeOpt) { 
    super(props)
  • Search for: (constructor\s*\()\s*\w+\s*:\s*JQuery\s*,\s*\w+\s*(\??)\s*:\s*(\w+\s*\)\s*\{[\s\n]*super\s*\()\s*\w+\s*,\s*(\w+\s*\))
  • Replace with: $1props$2: { element?: any } & $3props)

Replacing constructors with no options

Next, replace constructors with no options, typically found in dialogs:

constructor() { 
    super()
// with 
constructor(props?: any) { 
    super(props)
  • Search for: (constructor\s*\()\s*\)(\s*\{[\s\n]*super\s*\()\s*\)
  • Replace with: $1props?: any)$2props)

Note that even if your widget does not require any options, it is strongly recommended to have a props argument, at least optional, and pass those props to the base class.

Otherwise, the new widget model that we will discuss may not function correctly.

Replacing code blocks that call old constructors with a container and options

In .CSHTML files, you may find code blocks creating grids, dialogs, and similar objects on a target element with some options, like this:

new SomeGrid(this.byId("#SomeElement"), { key1: 5 })
// with
new SomeGrid({ element: this.byId("#SomeElement"), ...{ key1: 5 } })
  • Search for: (new\s*\w+\s*\()\s*((this.byId|\$|jQuery)\([\w'"#]+\))\s*,(\s*\{([^}\n]*\n)*\s*\})
  • Replace with: $1{ element: $2, ...$4}

As shown above, all widgets accept an element property that allows attaching to a target element, similar to the jQuery container argument before. This is why all constructors should have a props argument and pass it to the base widget constructor.

Additionally, for blocks of style:

new SomeGrid(this.byId("#SomeElement"), opt)
// with
new SomeGrid({ element: this.byId("#SomeElement"), ...opt })
  • Search for: (new\s*\w+\s*\()\s*((this.byId|\$|jQuery)\([\w'"#]+\))\s*,\s*([\w\.\?]+)\s*\)
  • Replace with: $1{ element: $2, ...$4})

Replacing jQueryUI.DialogOptions

Since jQueryUI.DialogOptions are no longer used, we need to remove references to them:

  • Search for: getDialogOptions\s*\(\s*\)\s*:\s*JQueryUI\.DialogOptions
  • Replace with: getDialogOptions()

Fluent Object and Functions

To facilitate the transition from jQuery-based code, we have introduced a simple jQuery-like object called Fluent. Unlike jQuery, it operates on a single element, not multiple elements, and offers a limited subset of jQuery functions such as addClass, toggle, toggleClass, on, off, remove, etc.

If jQuery is loaded, Fluent's on, off, one, etc. methods will redirect to jQuery's event handling mechanism. Otherwise, Fluent will use its basic event system, similar to jQuery, supporting namespaced events and delegation, unlike addEventListener. It also triggers events via jQuery's event system, similar to Bootstrap when jQuery is present on the page.

It is strongly recommended to use Fluent.remove and Fluent.empty methods when removing or emptying elements. This ensures proper cleanup of widgets attached to the elements, freeing resources and preventing memory leaks.

The base class Widget's element will now return a Fluent element instead of a jQuery element. To facilitate code migration, it is recommended to use the .domNode property, which returns an HTML element, instead of the .element property, which returns a Fluent object where possible.

Replacing Find statements

Fluent provides a findFirst function and a findAll function as alternatives to jQuery's find function. The former returns another Fluent element, while the latter returns a simple array of elements (not Fluent). Therefore, when encountering places where jQuery's find is used, replace them with findFirst if searching for a single element, or findAll if searching for multiple elements.

  • Search for: \.closest\('.field'\).find\('sup'\)
  • Replace with: .closest('.field').findFirst('sup')

Replacing Focus statements

Fluent does not have a .focus() method, as it only includes a subset of jQuery functions. In such cases, you can use .getNode() to access the wrapped HTML element and call its methods. Note that it may also return null.

  • Search for: \.element\.focus\(\s*\)
  • Replace with: .element.getNode().focus()

Replacing jQuery By ID Selectors

// before:
new UserGrid($('#GridDiv'));
// after:
new UserGrid({ element: '#GridDiv' });

In the example above, replace the $ call with an object that provides the element option. The Widget now accepts a selector in addition to HTML element references, making the above modification sufficient.

  • Search for: (new\s+[\w\.]+\s*\()\s*\$\s*\(['"](#[^'"\)]+)['"]\s*\)
  • Replace with: $1{ element: "$2" }

Remaining jQuery and $ References

Depending on the size of your project, there may be scattered references to $ throughout the code. Here are examples demonstrating how to replace such references with Fluent or other alternatives:

// before:
$(function() {
    // some code
});
// after:
Serenity.Fluent.ready(function() {
    // some code
});

If the code is in a module file, for example, one with imports or inside a <script type="module">, you don't need to use $(function...) at all. Otherwise, replace such $(function...) calls with Serenity.Fluent.ready(function...).

// before:
$(e.target).val()
$(e.target).hasClass("target-text")
// after:
Fluent(e.target).val();
Fluent(e.target).hasClass('target-text')

In the code above, $ can be replaced with Fluent.

// before:
protected onClick(e: JQueryEventObject, row: number, cell: number): any {
// after:
protected onClick(e: MouseEvent, row: number, cell: number): any {

JQueryEventObject can be replaced with MouseEvent, Event, or similar based on context.

// before:
if (e.isDefaultPrevented()) {
// after
if (e.defaultPrevented || (e as any)?.isDefaultPrevented?.()) {

isDefaultPrevented is a jQuery-specific method, while defaultPrevented is the DOM one. We are checking both just in case.

// before:
$('<a/>').addClass('source-text')
// after:
Fluent("a").addClass('source-text')

Fluent only accepts tag names, not HTML markup.

// before:
if ($.fn['colorbox']) {
// after:
let $ = getjQuery();
if ($?.fn?.['colorbox']) {

This is only necessary if you removed jQuery types. Serenity provides a getjQuery() function to get a reference to jQuery. This makes it easier to spot places where jQuery is used later.

Remove jQuery UI Types

You should remove jQuery UI and jQuery Validation types from your package.json file:

"@types/jquery": "2.0.48",
"@types/jqueryui": "1.12.6",
"@types/jquery.validation": "1.16.7",

If you don't have any jQuery references left, you may also remove the @types/jquery package.

Also remove the corresponding types references in tsconfig.json:

types: [
    "jquery", // remove this if you removed jquery from package.json
    "jquery-ui", // remove this
    "jquery.validation" // and this
]

[BREAKING CHANGE] SlickGrid Scripts Are No Longer Shipped via Serenity.Assets

The SlickGrid scripts will now be shipped via the Serenity.SleekGrid NuGet package as static web assets instead of the Scripts/SlickGrid folder under Serenity.Assets.

Remove the following references from your appsettings.bundles.json:

"~/Serenity.Assets/Scripts/SlickGrid/slick.core.js",
"~/Serenity.Assets/Scripts/SlickGrid/slick.grid.js",
"~/Serenity.Assets/Scripts/SlickGrid/slick.groupitemmetadataprovider.js",
"~/Serenity.Assets/Scripts/SlickGrid/Plugins/slick.autotooltips.js",
"~/Serenity.Assets/Scripts/SlickGrid/Plugins/slick.headerbuttons.js",
"~/Serenity.Assets/Scripts/SlickGrid/Plugins/slick.rowselectionmodel.js",
"~/Serenity.Assets/Scripts/SlickGrid/Plugins/slick.rowmovemanager.js",

Add the following single file reference:

"~/Serenity.SleekGrid/index.global.js",

This file contains a bundle of all the files mentioned above.

Serenity.SleekGrid is referenced by the Serenity.Corelib package, so you don't need to have a direct reference in your project file.

Serenity.SleekGrid is published separately from other Serenity packages, so its version may not match other Serenity packages. As Serenity.Corelib will have the reference to the most compatible Serenity.SleekGrid package version, avoid manually adding a reference to Serenity.SleekGrid.

TypeScript Scanner and Parser is Rewritten

The TypeScript parser, based on TypeScriptAST, has been rewritten by converting the latest TypeScript source code. It passed about 12k cases in TypeScript's repository and produces the same set of tokens and AST nodes. This change addresses the weaknesses displayed by the existing parser as TypeScript itself evolved over the years.

Serenity Widgets Can Now Be Used in jsx-dom Directly

All Serenity widgets are now compatible with jsx-dom, eliminating the need to wrap them with jsxDomWidget. The jsxDomWidget helper is removed.

jQuery FileUpload is Replaced with a Serenity Uploader Class

A custom uploader class is introduced, and you should remove jquery.fileupload.js from appsettings.bundles.json.

SleekGrid Column Formatters Support Returning Element, e.g. Using JSX or Fluent

You can now return an HTML element (or jsx-dom element) from column formatters:

// old
someColumn.format = ctx => `<span><i class="${ctx.escape(faIcon("times"))}" /> ${ctx.escape()}</span>`
// new
someColumn.format = ctx => <span><i class={faIcon("times")} /> {ctx.value}</span>
// or via Fluent if don't want to convert the file to .tsx
someColumn.format = ctx => Fluent("span").addClass(faIcon("times")).append(" " + ctx.value)).getNode();
// or some simple HTML element
someColumn.format = ctx => {
    var span = document.createElement("span");
    var i = span.appendChild(document.createElement("i"));
    i.classList.add("fa");
    i.classList.add("fa-times");
    return span;
}

This change makes escaping values unnecessary and avoids typos while returning HTML markup.

Note that this should be considered experimental, and it has some performance implications, although not very much, as SlickGrid was not originally designed to support returning anything other than simple HTML strings. We now have to clean up event handlers/widgets, etc. while removing elements either via jQuery or Fluent.

There is also a new faIcon() helper that provides IntelliSense for Line Awesome (Font Awesome) icons.

New gridPageInit Helper

There is now a gridPageInit function that can be used instead of initFullHeightGridPage:

// old
initFullHeightGridPage(new SomeGrid({ element: '#GridDiv' })).init();
// new
gridPageInit(SomeGrid);

It auto-creates the grid class and uses #GridDiv element as the target by default, but you may change it and other options if desired:

gridPageInit(new SomeGrid({ element: '#MyGrid', someOption: 5 }));

[BREAKING CHANGE] Templates Are Deprecated

Templates, e.g. .Template.html or .ts.html files are deprecated. Use tsx and renderContents. Even though it is still possible to use getTemplate() method for now, it will be removed in next version.

TemplatedDialog, TemplatedPanel etc. classes might be removed or replaced with versions that does not use templates.