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 evaluatingTomselect
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.
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
.
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)
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)
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.
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})
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()
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.
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')
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()
// 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" }
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.
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
]
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
.
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.
All Serenity widgets are now compatible with jsx-dom, eliminating the need to wrap them with jsxDomWidget
. The jsxDomWidget
helper is removed.
A custom uploader class is introduced, and you should remove jquery.fileupload.js
from appsettings.bundles.json
.
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.
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 }));
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.