david-bouyssie / TypExt

A Typescript factory of ExtJS classes
3 stars 3 forks source link
extjs typescript

TypExt

A Typescript factory of ExtJS classes.

The goal of this project is to provide a brige between TypeScript and the ExtJS framework.

ExtJS and TypeScript: a complicated story...

It is well known that TypeScript (TS) and ExtJS don't play well together because they use incompatible class systems:

https://blorkfish.wordpress.com/2013/01/28/using-extjs-with-typescript/
Unfortunately, TypeScript and ExtJs do not seem to work too well together.
This incompatibility is mainly due to the differences in object creation between the two approaches.
Where ExtJs uses config blocks and anonymous methods for object creation, TypeScript uses the closure pattern to bring an easier way to build object-oriented javascript.
Unfortunately these two approaches seem to be at odds with each other.

Despite these limitations, and also the low interest from Sencha in making progress on this issue, some volunteer developers worked on setting up available ExtJS-TS definitions.
To generate TS definitions in an automated way, these developers got the idea to use the ExtJS documentation as a data source. The strategy consists in executing the Sencha JSDuck documentation tool on the ExtJS sources in order to generate corresponding JSON files. These files are then parsed by a dedicated converter which will generate the precious TS definitions.

Here is the list of projects providing TypeScript definitions for ExtJS (and for some of them the corresponding "JSDuck to TS" converter):

While the availability of such TS definitions allows a partial comprehension of the ExtJS API by the TypeScript compiler, they don't provide a compile-time checking of ExtJS classes instantiation.
Indeed, due to incompatibilities between ExtJS and TypeScript it is not possible to use the new operator if you want to instantiate ExtJS classes.
However it is still possible to use the method Ext.create('MyClass',config) which takes as first parameter the name of the class to instantiate or its alias. For more information about Ext.create() and the differences with the new operator, have a look to these topics on Stackoverflow:
http://stackoverflow.com/questions/9481828/objects-in-extjs-ext-create-or-new-operator
http://stackoverflow.com/questions/16991133/how-to-use-ext-create-properly

If you use the ExtJS-TS definitions provided by one of the repos cited above you will be able to instantiate your ExtJS class.
However you will have no guarantee that the name of the class you want to instantiate is valid.
Let's say you want to instantiate an ExtJS panel. You may write something like this:

Ext.create('Ext.panel.Panel', {
    title: 'Hello',
    width: 200,
    html: '<p>World!</p>',
    renderTo: document.body
});

And because of the typo on the class name you will see the following error at runtime:

Uncaught Error: [Ext.create] Unrecognized class name / alias: Ext.panel.Panel

Don't you think it could be nice to catch this error at compile time?
To overcome this issue, some other devs worked on a forked version of TS compiler to emit JavaScript code in an ExtJS class style. Thus instead of emitting vanilla javascript code, the compiler will generate code compliant with the ExtJS class syntax. Here is a list of repositories proposing to these patched compilers:

Motivation for an alternative solution

So you may wonder why develop another solution if all the great definitions/tools mentionned above are available? Because none of them may satisfy your needs.

In my opinion the main drawback of the Patched Compiler approach (see fabioparra repos) is that it complicates the integration with IDE supporting the TS language. For instance there is no documentation regarding the compatibility of this solution with existing Eclipse TS plugins. Moreover it is not sure that the development of this modified compiler will be maintained for future versions of TypeScript. If you have a look at the current repo documentation, you will see that it is compatible only with TS up to 1.8 and ExtJS up to 5.x. What about TS 2.0 and ExtJS 6.x?

The second drawback is relative to Javascript performance. The TS compiler does a lot of work to generate JS code that will be as fast as possible. Emitting JS code using the ExtJS style means that a lot of things will occur at runtime and this may not be what you wish to obtain for some parts of your code.
Moreover you may want to use vanilla TypeScript classes for some purposes and ExtJS classes for other ones. With a modified compiler all your emitted classes will follow the ExtJS syntax, and this might not be what you want.

So, are we stuck? I think there is another way that has not been explored so far: use TS static methods as a factory of ExtJS objects.

TypExt: compile-time checking of your ExtJS classes instantiation

The main idea of TypExt is to provide a set of static methods replacing the ExtJS object construction usually performed via the Ext.create() method. It thus becomes possible to instantiate ExtJS classes while using the genuine TS compiler.
Note that ExtJS 5.1.1 only is currently supported for now, but I plan to extend the usage of TypExt to other ExtJS versions.

For each ExtJS class the TypExt library provides a static factory method named create() prefixed by the namespace of the class.
To avoid confusion with the ExtJS namespace, the root namespace is TypExt instead of Ext.
For instance, the factory method of the class Ext.panel.Panel is TypeExt.panel.Panel.create().
So if we translate our previous ExtJS panel construction example in the TypExt syntax it becomes:

TypExt.panel.Panel.create( {
    title: 'Hello',
    width: 200,
    html: '<p>World!</p>',
    renderTo: document.body
});

If you try to compile this little code using the TypeScript compiler you will obtain this error at compile-time:

Property 'Panel' does not exist...

We thus moved from runtime error checking to compile-time error checking. This gives us more safety when developing/maintaining large applications. Note that we can also take advantage of autocompletion on the TypExt namespace to look for sub-modules and class names.

Another feature of TypExt is that the configuration passed to the create() method is restricted to the list of arguments that are really used by the class constructor. Other available ExtJS-TS definitions provide interfaces mixing configs, properties and methods. Having an interface that correspond strictly to the constructor configuration is a real advantage if you want to be sure that you don't provide a wrong parameter. This is thus another source of safety provided by the TypExt library.

TypExt getting started

1. Required files

The TypExt project is composed of the following file hierarchy:

If you don't want to compile the TypExt library in your project it also possible to grab a pre-compiled version of the library from the output directory (compilation of the typext.ts and typext-addon.ts files).

2. Usage examples

TypExt addons

TypExt major addons are shortly described below:

1. TypExt.addon.data.IModelField

module path.to.my.module {

  import IModelField = TypExt.addon.data.IModelField;

  export interface IFile {
    id: string;
    path: string;
    name: string;
    size: number;
    lastmodified: number;
    extension: string;
  }

  export class FileModelFactory extends TypExt.addon.data.AbstractModelFactory implements IFile {
    getClassName() { return "path.to.my.module.File" }

    id: any = <IModelField>{ name: 'id', type: 'string' };
    path: any = <IModelField>{ name: 'path', type: 'string' };
    name: any = <IModelField>{ name: 'name', type: 'string' };
    size: any = <IModelField>{ name: 'size', type: 'int' };
    lastmodified: any = <IModelField>{ name: 'lastmodified', type: 'int' };
    extension: any = <IModelField>{ name: 'extension', type: 'string' };
  }

  new FileModelFactory().defineModel();
}

Note: you need to force the type 'any' to avoid the following error:

Type 'IModelField' is not assignable to type 'number'.

You can also use ExtJS automatic typing (recommanded for strings only):

...
  export class FileModelFactory extends TypExt.addon.data.AbstractModelFactory implements IFile {
    getClassName() { return "PWD.module.dse.model.File" }
    id = 'id';
    path = 'path';
    name = 'name';
    size: any = <IModelField>{ name: 'size', type: 'int' }; // prefer to define it explicitly for non-string types
    lastmodified: any = <IModelField>{ name: 'lastmodified', type: 'int' };
    extension = 'extension';
  }
...

2. TypExt.addon.Ajax

TypExt provides shortcut methods httpPost and httpGet for HTTP calls, for example:

TypExt.addon.Ajax.httpPost({
  url: "www.my.url",
  params: {...},
  callback: function(options: TypExt.ajax.IRequestConfig, success: boolean, response: XMLHttpRequest) {
    if (success) ... else ...
  }
});

3. TypExt.addon.panel.IPanel

The main advantage of this addon panel over a classical one is that it provides the createAndExtend() method. It will return the panel as an IExtendedContainer (also a TypExt addon), much more powerfull. For example, and that was the initial motivation for creating it, assessing a panel children in javascript can be very tricky. The addon panel provides an utility method to get them more easily:

const addonPanel = TypExt.addon.Panel.createAndExtend({
  config...
});
const embeddedPanels: TypExt.addon.panel.IPanel[] = addonPanel.getItems();

Caveats

TypExt doesn't solve entirely the TS/ExtJS integration. It just provides more safety regarding the instantiation of ExtJS classes.

1. ExtJS inheritance

If you want to extend ExtJS classes using the ExtJS syntax (Ext.define) you will have to maintain a separate TS interface corresponding to the config object that will be passed to your class constructor.

This issue is well described in this Stackoverflow answer:

http://stackoverflow.com/questions/24671996/typescript-definitions-for-extjs-5
The problem is that there are two type systems in play.
One is the design-time type system enforced by the TypeScript compiler.
The other is the run-time type system defined by ExtJS.
These two type systems are very similar in regard to the way they are implemented, yet they each have a distinct syntax, and therefore have to be specified seperately.
Much of the value of a static type system is lost if it two copies of every interface have to be maintained.

Suggested workarounds:

  1. Do not use ExtJS inheritance when possible. Define as much TS classes as possible. This way you will decrease the need of maintaining both a TS interface and an ExtJS Ext.define block.

  2. When defining stores or models, use the dedicate TypExt addon (see above). It allows you to define a model using the TS syntax while generating a model compatible with the ExtJS framework.

2. ExtJS dynamic class loading

Ext.create is responsible of lazy instantiating ExtJS classes. If Ext.Loader is enabled and the class has not been defined yet, it will attempt to load the class for you via synchronous loading. The loader will look for a JS file at the given path following the Ext.Loader convention. Note that the Ext.Loader is fully configurable: http://docs.sencha.com/extjs/6.2.0/modern/Ext.Loader.html#property-setConfig http://extendedjs.blogspot.fr/2012/12/extjs-4classesdynamic-loading.html

The problem is that you will have to:

Suggested workaround: do not use the ExtJS loader anymore and genarate a single JS file. You will loose dynamic ExtJS class loading but in most cases you won't affect the application performance.