question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Create a type definitions file for the client-side JavaScript API

See original GitHub issue

This issue is for discussing and creating a types definitions file for the JavaScript API of PrimeFaces - with the goal of enabling people to write more maintainable, less error-prone client side code for dynamic pages with PrimeFaces, and to provide a better coding experience with better tooling.

image

Also,

Typescript is fully supported as type definition files are provided in the npm package of PrimeReact. A sample typescript-primereact application is available as well at github.

What and why

This issue is asking for TypeScript type defintions (rather than say Flow) as TypeScript has gotten pretty popular these days.

To quote from the forum post I made:

We are using PrimeFaces for a complex application with many features, some of which require customizing components and/or writing client-side JavaScript for interacting with existing PrimeFaces components. New possibilities such as splitting the code into ES6 modules or using transpilers like closure compiler help a lot to prevent the JavaScript from becoming a mess.

When I need to interact with PrimeFaces components, I need to know which JavaScript functions there are. Some are documented on the help pages, but that is far from complete. I can open the dev tools and check it out, but doing so every time is cumbersome. So I decided to write some TypeScript type definitions for the part of the JavaScript API I needed. To give you an idea of how this looks:

(removed, see below)

TypeScript type definitions are already available for a lot of node packages and many newer libraries provide them themselves. This has several advantages:

  • For the Java part, when we update to a newer of PrimeFaces, the compiler checks which part of the API was modified and identifies the code needs to be adapted. Currently, this is not possible for the client-side JavaScript API. Updating PrimeFaces requires checking manually whether that results in any JavaScript errors. Since it’s usually not possible to test all code paths, this remains incomplete and bugs are introduced easily. With a type-definitions file, much more checking could be done before the JavaScript code is even sent to the browser.
  • It also makes the JavaScript API easier to discover and prevents misspelling properties and function names. Right now, discovering the JavaScript API means opening up the console.
  • It also tells people explicitly which part of the API is not public (by virtue of not being included). With the right tools, this can also be checked. Right now, that is up to anybody’s guess.
  • The type-definitions files can also be used for documentation (DocComments, similar to Java). Tools like typedoc can even generate some nice-looking API docs from typescript (definition) files.

Ideally, it would be great if PrimeFaces would come bundled with with a type definitions file that people could use. Perhaps I might be able to help out with an initial type definitions file, but in the long run, the PrimeFaces team needs to maintain it.

Generating the type definitions

One idea that was mentioned in the forum was to generate the type definitions directly from the JavaScript source files. This is a great idea and eases the amount of effort required, but unfortunately there’s no such thing as a free lunch. Generating the type definitions completely auto-magically from the JavaScript source is (afaik) not possible (but see the suggestion below). A few reasons include:

  • Fields (properties on a new class-like object) are not spelled out explicitly. Some may even be added to the object dynamically within some setup method (and that is very common for widgets, especially as they have no constructor() {}
  • Method (functions) may refer to the special variable arguments (which should not be used anymore in favor of varargs function foo(x, ..y){}). But if they’re used, that makes it nearly impossible to tell what they are and how many there can be.
  • Optional arguments are not marked. When an argument is optional, a method usually attempts to assign some default value to it in the function body (though that would be easier with modern JavaScript function foo(x, y, z = 3) }{}).
  • And at any rate, figuring out the intended type of the arguments as well as the return type, just from the JavaScript source, is not possible. And this is half of the reason of having type definitions.
  • (Plain-text English) comments need to be added to the methods, arguments, and property manually either way.

So we need to put in some extra effort to create and maintain a type definitions file. In my opinion it is well worth it, and that is why I opened this issue.

Maven and node

Depending of how we approach this, TypeScript is written in JavaScript (node.js) and many tools are written in JavaScript as well. Chances are we might need to make use of some of these tools in some manner during the build process.

There is a very nice maven plugin that I use in my projects as well. It is called maven-frontend-plugin and basically installs a completely local version of node in the target directory during the build process. It works with Windows and Linux (and probably macOS as well, though I haven’t tried it personally) and makes it easy to integrate a node build task with maven.

A first suggestion

First of all, one way to achieve this involves a rather major change and I don’t know if people would welcome it. For the sake of completeness, let me mention it though:

  • Rewrite the JavaScript source code to TypeScript. This would mainly involve adding type annotations and using classes instead of PrimeFaces.widget.BaseWidget.extend({...}).
  • Then a type definitions file would be generated during the transpilation process from TS to JS.
  • As a plus, your JavaScript code gets type-checked as well.

JavaScript API

The JavaScript API consists of two main parts:

  • The core functions in META-INF/resources/primefaces/core, consisting of the modules
    • ajax
    • csp
    • dialog
    • env
    • expression
    • utils
    • widgets
  • The individual widgets in META-INF/resources/primefaces/...

The lion’s share the API is represented the widgets. I count about ~99 widget folders. I looked through some of them and they seem to be maintained by a handful of different author. Each widget is a class and consists of three parts:

  • Direct fields (or properties), such as PrimeFaces.widget.LightBox.navigators These are often JQuery elements that hold the HTML elements the widgets are made of.
  • Properties in the cfg object, such as PrimeFaces.widget.LightBox.cfg.mode. These are usually generated on the server and sent to the client, and are thus mostly primitive types such as strings and boolean. Sometimes also holds callback functions etc.
  • Methods, such as PrimeFaces.widget.LightBox.show(). Most of these methods are “action methods”, ie. methods that take no or only a few basic arguments, perform some action (eg. showing or hiding a dialog), and return nothing. Some are also simple getters (and a few setters), such as PrimeFaces.widget.LightBox.isVisible(). The point to note here is that most methods are rather simple, as far as type annotations are concerned, and if streamline the development process, we should streamline it this case.

Additionally, some fields, configuration properties and methods are private and not meant to be used from the outside, while others are public and are part of the public API. Right now, there is no to little indication which are which. Sometimes internal methods and properties start with an underscore _, but not always.

This is one reason a component maintainer should review the relevant part of the type definitions file before it is released to the public. I think we should also establish a few guidelines on which property and methods should be exposed, as there are many common types of properties and methods between widgets, see above.

How to proceed

I’d say how best to proceed depends on how comfortable these authors are with TypeScript definitions in general.

I can think of two similar ideas:

  1. For each widget, create a new .d.ts type definitions file, preferably in the same directory as the widget.
    • This keeps the JavaScript and the type definitions completely separate, and makes them completely optional.
    • On the other hand, widget maintainers (and the occasional contributor) now need to know about two files. It’s also easy for the two files to drift apart, especially when someone updates the widget and forgets to update the type definitions as well.
  2. Include the comments and types directly in the widget file where they belong; by adding JSDoc-like comments (similar to JavaDocs).
    • This has the advantage of keeping the method defintions and the docs/comments closely together, reducing the risk of somebody forgetting to update them.
    • It also reduces the amount of TypeScript that needs to be written (as it omits the interfaces andextends etc.), which may be helpful for people not comfortable with TypeScript.
    • It means a little bit more work to implement though. I would probably parse the JavaScript files with a parser such as acorn, extract and parse the JSDoc comments, and generate the type definitions file for the widget from that. Would also need a few custom tags such as @cfg to indicate some widget specific information.

Regarding the first idea, this seems like how they’re doing it over at primereact

For the second idea, here is a quick draft of how it could look like for the LightBox widget.

At the top of the file you can see some additional TypeScript definitions. For the LightBox widget, they are not necessary and could be inlined. But in general it may become necessary and should be supported. Although we could also make a separate file for those few cases. Also, observer how some methods are marked @internal to exclude them from the type definitions file.

Example of how inline comments and type annotations could look like
/**
 * @ts
 *
 * interface ShowUrlOptions {
 *    height: number;
 *    width: number;
 * }
 *
 * 
 * type Mode = "image" | "inline"| "iframe";
 */

/**
 * PrimeFaces LightBox Widget.
 * @field {JQuery} links Element that contains the links.
 * @field {JQuery} panel Element with the dialog panel <-- THIS IS ADDED DYNAMICALLY IN A METOHD. ONE REASON WE CANNOT GENERATE TYPEDEFS COMPLETELY AUTOMATICALLY
 * @field {JQuery} content ...
 * @field {JQuery} imageDisplay ...
 * @field {JQuery} navigators ... 
 * @field {JQuery} iframe ...
 * @field {JQuery} imageDisplay ...
 * @field {JQuery} caption ...
 * @field {JQuery} captionText ...
 * @field {JQuery} closeIcon ...
 * @field {(() => void)[]} onshowHandlers ...
 * @cfg {string} appendTo Search expression for the element to which the dialog should be appended.
 * @cfg {Mode} mode The mode of this lightbox component. 
 * @cfg {boolean} visible Whether the lightbox dialog is currently visible.
 * @cfg {string} width CSS length with the width of the dialog.
 * @cfg {string} height CSS length with the height of the dialog.
 * @cfg {JQuery} iframeTitle
 * @cfg {() => void} onShow
 * @cfg {() => void} onHide
 */
PrimeFaces.widget.LightBox = PrimeFaces.widget.BaseWidget.extend({

    /**
	 * Main initialization method for the widget. Creates the LightBox and binds the events.
     * @param {PrimeFaces.WidgetCfg.LightBoxCfg} cfg
     */
    init: function(cfg) {
        // the dynamic overlay must be appended to the body
        cfg.appendTo = '@(body)';

        this._super(cfg);

        this.links = this.jq.children(':not(.ui-lightbox-inline)');

        this.createPanel();

        if(this.cfg.mode === 'image') {
            this.setupImaging();
        } else if(this.cfg.mode === 'inline') {
            this.setupInline();
        } else if(this.cfg.mode === 'iframe') {
            this.setupIframe();
        }

        this.bindCommonEvents();

        if(this.cfg.visible) {
            this.links.eq(0).click();
        }
    },

    /**
	 * Refreshes the dynamic overlay dialog with LightBox. Called during an AJAX update.
	 * Use this when...
     * @param {PrimeFaces.WidgetCfg.LightBoxCfg} cfg
     * @override
     */
    refresh: function(cfg) {
        PrimeFaces.utils.removeDynamicOverlay(this, this.panel, this.id + '_panel', $(document.body));

        this._super(cfg);
    },

    /**
     * Creates the panel.
     * @internal
     */
    createPanel: function() {
        this.panel = $('<div id="' + this.id + '_panel" class="ui-lightbox ui-widget ui-helper-hidden ui-corner-all ui-shadow">'
            + '<div class="ui-lightbox-content-wrapper">'
            + '<a class="ui-state-default ui-lightbox-nav-left ui-corner-right ui-helper-hidden"><span class="ui-icon ui-icon-carat-1-w">go</span></a>'
            + '<div class="ui-lightbox-content ui-corner-all"></div>'
            + '<a class="ui-state-default ui-lightbox-nav-right ui-corner-left ui-helper-hidden"><span class="ui-icon ui-icon-carat-1-e">go</span></a>'
            + '</div>'
            + '<div class="ui-lightbox-caption ui-widget-header"><span class="ui-lightbox-caption-text"></span>'
            + '<a class="ui-lightbox-close ui-corner-all" href="#"><span class="ui-icon ui-icon-closethick"></span></a><div style="clear:both" /></div>'
            + '</div>');

        PrimeFaces.utils.registerDynamicOverlay(this, this.panel, this.id + '_panel');

        this.contentWrapper = this.panel.children('.ui-lightbox-content-wrapper');
        this.content = this.contentWrapper.children('.ui-lightbox-content');
        this.caption = this.panel.children('.ui-lightbox-caption');
        this.captionText = this.caption.children('.ui-lightbox-caption-text');
        this.closeIcon = this.caption.children('.ui-lightbox-close');
    },

    /**
     * Does setup of imaging.
     * @interal
     */
    setupImaging: function() {
        var _self = this;

        this.content.append('<img class="ui-helper-hidden"></img>');
        this.imageDisplay = this.content.children('img');
        this.navigators = this.contentWrapper.children('a');

        this.imageDisplay.on('load', function() {
            var image = $(this);

            _self.scaleImage(image);

            //coordinates to center overlay
            var leftOffset = (_self.panel.width() - image.width()) / 2,
            topOffset = (_self.panel.height() - image.height()) / 2;

            //resize content for new image
            _self.content.removeClass('ui-lightbox-loading').animate({
                width: image.width()
                ,height: image.height()
            },
            500,
            function() {
                //show image
                image.fadeIn();
                _self.showNavigators();
                _self.caption.slideDown();
            });

            _self.panel.animate({
                left: '+=' + leftOffset
                ,top: '+=' + topOffset
            }, 500);
        });

        this.navigators.mouseover(function() {
            $(this).addClass('ui-state-hover');
        })
        .mouseout(function() {
            $(this).removeClass('ui-state-hover');
        })
        .click(function(e) {
            var nav = $(this);

            _self.hideNavigators();

            if(nav.hasClass('ui-lightbox-nav-left')) {
                var index = _self.current == 0 ? _self.links.length - 1 : _self.current - 1;

                _self.links.eq(index).trigger('click');
            }
            else {
                var index = _self.current == _self.links.length - 1 ? 0 : _self.current + 1;

                _self.links.eq(index).trigger('click');
            }

            e.preventDefault();
        });

        this.links.click(function(e) {
            var link = $(this);

            if(_self.isHidden()) {
                _self.content.addClass('ui-lightbox-loading').width(32).height(32);
                _self.show();
            }
            else {
                _self.imageDisplay.fadeOut(function() {
                    //clear for onload scaling
                    $(this).css({
                        'width': 'auto'
                        ,'height': 'auto'
                    });

                    _self.content.addClass('ui-lightbox-loading');
                });

                _self.caption.slideUp();
            }

            setTimeout(function() {
                _self.imageDisplay.attr('src', link.attr('href'));
                _self.current = link.index();

                var title = link.attr('title');
                if(title) {
                    _self.captionText.text(title);
                }
            }, 1000);


            e.preventDefault();
        });
    },

    /**
     * Scales the image.
     * @param {JQuery} image
     */
    scaleImage: function(image) {
        var win = $(window),
        winWidth = win.width(),
        winHeight = win.height(),
        imageWidth = image.width(),
        imageHeight = image.height(),
        ratio = imageHeight / imageWidth;

        if(imageWidth >= winWidth && ratio <= 1){
            imageWidth = winWidth * 0.75;
            imageHeight = imageWidth * ratio;
        }
        else if(imageHeight >= winHeight){
            imageHeight = winHeight * 0.75;
            imageWidth = imageHeight / ratio;
        }

        image.css({
            'width':imageWidth + 'px'
            ,'height':imageHeight + 'px'
        });
    },

    /**
     * Does setup of inline.
     * @internal
     */
    setupInline: function() {
        this.inline = this.jq.children('.ui-lightbox-inline');
        this.inline.appendTo(this.content).show();
        var _self = this;

        this.links.click(function(e) {
            _self.show();

            var title = $(this).attr('title');
            if(title) {
                _self.captionText.text(title);
                _self.caption.slideDown();
            }

            e.preventDefault();
        });
    },

    /**
     * Does setup of iframe.
     * @internal
     */
    setupIframe: function() {
        var $this = this;
        this.iframeLoaded = false;
        this.cfg.width = this.cfg.width||'640px';
        this.cfg.height = this.cfg.height||'480px';

        this.iframe = $('<iframe frameborder="0" style="width:' + this.cfg.width + ';height:'
                        + this.cfg.height + ';border:0 none; display: block;"></iframe>').appendTo(this.content);

        if(this.cfg.iframeTitle) {
            this.iframe.attr('title', this.cfg.iframeTitle);
        }

        this.links.click(function(e) {
            if(!$this.iframeLoaded) {
                $this.content.addClass('ui-lightbox-loading').css({
                    width: $this.cfg.width
                    ,height: $this.cfg.height
                });
                $this.show();

                $this.iframe.on('load', function() {
                                $this.iframeLoaded = true;
                                $this.content.removeClass('ui-lightbox-loading');
                            })
                            .attr('src', $this.links.eq(0).attr('href'));
            }
            else {
                $this.show();
            }

            var title = $this.links.eq(0).attr('title');
            if(title) {
                $this.captionText.text(title);
                $this.caption.slideDown();
            }

            e.preventDefault();
        });
    },

    /**
     * Binds all events.
     * @internal
     */
    bindCommonEvents: function() {
        var $this = this;

        this.closeIcon.mouseover(function() {
            $(this).addClass('ui-state-hover');
        })
        .mouseout(function() {
            $(this).removeClass('ui-state-hover');
        });

        this.closeIcon.click(function(e) {
            $this.hide();
            e.preventDefault();
        });

        var hideEvent = PrimeFaces.env.ios ? 'touchstart' : 'click';
        PrimeFaces.utils.registerHideOverlayHandler(this, hideEvent + '.' + this.id + '_hide', $this.panel,
            function() { return $this.links.add($this.closeIcon); },
            function(e, eventTarget) {
                if(!($this.panel.is(eventTarget) || $this.panel.has(eventTarget).length > 0)) {
                    e.preventDefault();
                    $this.hide();
                }
            });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', $this.panel, function() {
            $(document.body).children('.ui-widget-overlay').css({
                'width': $(document).width()
                ,'height': $(document).height()
            });
        });
    },

    /**
     * Shows the lightbox.
     */
    show: function() {
        this.center();

        this.panel.css('z-index', ++PrimeFaces.zindex).show();

        if(!PrimeFaces.utils.isModalActive(this.id)) {
            this.enableModality();
        }

        if(this.cfg.onShow) {
            this.cfg.onShow.call(this);
        }
    },

    /**
     * Hides the lightbox
     */
    hide: function() {
        this.panel.fadeOut();
        this.disableModality();
        this.caption.hide();

        if(this.cfg.mode == 'image') {
            this.imageDisplay.hide().attr('src', '').removeAttr('style');
            this.hideNavigators();
        }

        if(this.cfg.onHide) {
            this.cfg.onHide.call(this);
        }
    },

    /**
     * Centers the lightbox.
     */
    center: function() {
        var win = $(window),
        left = (win.width() / 2 ) - (this.panel.width() / 2),
        top = (win.height() / 2 ) - (this.panel.height() / 2);

        this.panel.css({
            'left': left,
            'top': top
        });
    },

    /**
     * Makes the dialog modal.
     */
    enableModality: function() {
        PrimeFaces.utils.addModal(this, this.panel.css('z-index') - 1);
    },

    /**
     * Makes the dialog not modal.
     */
    disableModality: function() {
        PrimeFaces.utils.removeModal(this);
    },

    /**
     * Shows the navigation buttons.
     */
    showNavigators: function() {
        this.navigators.zIndex(this.imageDisplay.zIndex() + 1).show();
    },

    /**
     * Hides the navigation buttons.
     */
    hideNavigators: function() {
        this.navigators.hide();
    },

    /**
     * Adds the event handlers.
     * @param {() => void} fn The handler to append to the existing handlers.
     * @internal
     */
    addOnshowHandler: function(fn) {
        this.onshowHandlers.push(fn);
    },

    /**
     * @param {boolean} `true` if the lightbox dialog is currently closed, `false` otherwise.
     */
    isHidden: function() {
        return this.panel.is(':hidden');
    },

    /**
     * Shows the given URL.
     * @param {ShowUrlOptions} opt
     */
    showURL: function(opt) {
        if(opt.width)
            this.iframe.attr('width', opt.width);
        if(opt.height)
            this.iframe.attr('height', opt.height);

        this.iframe.attr('src', opt.src);

        this.captionText.text(opt.title||'');
        this.caption.slideDown();

        this.show();
    }
});

The core of the API is smaller, does not change as much and is (probably?) maintained by less people. Here we could just make a separate type definitions file. Or, if the approach mentioned above works out well and can be adapted, add the type definitions inline as well.

What’s next

I would love to hear your ideas and opinions on this. I’ve got some experience with TypeScript and could contribute a bit towards an initial type definitions file.

Before it’s released, I think it would be prudent to let the widget maintainers have a look, to catch errors and mistakes and to check which part of the API should be exposed.

Further down the road, you could also consider publishing the type definitions to the DefinitelyTyped repository. This would make it a lot easier for people who use npm to consume and work with it. It would also be possible to generate the JavaScript API docs directly from the type definitions via tools such as typedoc. The PrimeFaces doc pages for each widget could then just link to the relevant page of the JavaScript API doc pages

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:1
  • Comments:23 (23 by maintainers)

github_iconTop GitHub Comments

1reaction
blutorangecommented, Aug 11, 2019

Alright, I went ahead and made a prototype (needs a bit of work + refactoring) for the second option to see if it could work. I came up with the following:

  • Node.js script that parses the JavaScript widget files with acorn and generates a .d.ts type definitions file
  • maven-frontend-plugin to run the script with maven

For example, I can now run the following:

cd primefaces
mvn package -P typedefs -DskipTests

which results in this output

Click to expand and view maven output
[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------------< org.primefaces:primefaces >----------------------
[INFO] Building PrimeFaces 7.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-enforcer-plugin:1.0:enforce (enforce-maven) @ primefaces ---
[INFO] 
...
...
...
[INFO] --- frontend-maven-plugin:1.7.6:npm (npm install) @ primefaces ---
[INFO] Running 'npm install' in /home/xxx/git/primefaces/primefaces-blutorange/src/main/type-definitions
[WARNING] npm WARN type-definitions@1.0.0 No repository field.
[ERROR] 
[INFO] audited 8 packages in 0.529s
[INFO] found 0 vulnerabilities
[INFO] 
[INFO] 
[INFO] --- frontend-maven-plugin:1.7.6:npm (generate type definitions) @ primefaces ---
[INFO] Running 'npm run generate-d-ts -- --outputDir /home/xxx/git/primefaces/primefaces-blutorange/target/generated-resources/type-definitions' in /home/xxx/git/primefaces/primefaces-blutorange/src/main/type-definitions
[INFO] 
[INFO] > type-definitions@1.0.0 generate-d-ts /home/xxx/git/primefaces/primefaces-blutorange/src/main/type-definitions
[INFO] > node index.js "--outputDir" "/home/xxx/git/primefaces/primefaces-blutorange/target/generated-resources/type-definitions"
[INFO] 
[INFO] Processing component directory ../resources/META-INF/resources/primefaces/accordion
[INFO]   -> Processing program  /home/xxx/git/primefaces/primefaces-blutorange/src/main/resources/META-INF/resources/primefaces/accordion/accordion.js
[INFO]     -> Processing widget definition AccordionPanel
[INFO] Processing component directory ../resources/META-INF/resources/primefaces/ajaxstatus
[INFO]   -> Processing program  /home/xxx/git/primefaces/primefaces-blutorange/src/main/resources/META-INF/resources/primefaces/ajaxstatus/ajaxstatus.js
[INFO]     -> Processing widget definition AjaxStatus
...
...
...
[INFO] Writing output file to  /home/xxx/git/primefaces/primefaces-blutorange/target/generated-resources/type-definitions/PrimeFaces.d.ts
...
...
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  23.401 s
[INFO] Finished at: 2019-08-11T16:01:50+02:00
[INFO] ------------------------------------------------------------------------

I also took the first widget, AccordionPanel, and added some doc comments so you can see how it will look like:

Click to expand and view accordion.js
/**
 * PrimeFaces AccordionPanel widget.
 * 
 * The AccordionPanel is a container component that displays content in stacked format.
 * 
 * @prop {JQuery} headers The DOM elements for the header of each tab.
 * @prop {JQuery} panels The DOM elements for the content of each tab pabel.
 * 
 * @extends {PrimeFaces.WidgetCfg.BaseWidget} cfg
 * @prop {PrimeFaces.WidgetCfg.AccordionPanel} cfg The configuration of this accordion widget instance.
 * @prop {number[]} cfg.active List of tabs that are currenty active (open). Eaach item is a 0-based index of a tab.
 * @prop {boolean} cfg.cache `true` if activating a dynamic tab should not load the contents from server again and use the cached contents; or `false` if the caching is disabled.
 * @prop {string} cfg.collapsedIcon The icon class name for the collapsed icon.
 * @prop {boolean} cfg.controlled `true` if a tab controller was specified for this widget; or `false` otherwise. A tab controller is a server side listener that decides whether a tab change or tab close should be allowed.
 * @prop {boolean} cfg.dynamic `true` if the contents of each panel are loaded on-demand via AJAX; `false` otherwise.
 * @prop {string} cfg.expandedIcon The icon class name for the expanded icon.
 * @prop {boolean} cfg.multiple `true` if multiple tabs may be open at the same time; or `false` if opening one tab closes all other tabs.
 * @prop {boolean} cfg.rtl `true` if the current text direction `rtl` (right-to-left); or `false` otherwise.
 *
 * @author Çağatay Çivici
 */
PrimeFaces.widget.AccordionPanel = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @param {PrimeFaces.WidgetCfg.AccordionPanel} cfg 
     */
    init: function(cfg) {
        this._super(cfg);

        this.stateHolder = $(this.jqId + '_active');
        this.headers = this.jq.children('.ui-accordion-header');
        this.panels = this.jq.children('.ui-accordion-content');
        this.cfg.rtl = this.jq.hasClass('ui-accordion-rtl');
        this.cfg.expandedIcon = 'ui-icon-triangle-1-s';
        this.cfg.collapsedIcon = this.cfg.rtl ? 'ui-icon-triangle-1-w' : 'ui-icon-triangle-1-e';

        this.initActive();
        this.bindEvents();

        if(this.cfg.dynamic && this.cfg.cache) {
            this.markLoadedPanels();
        }
    },

    /**
     * @private
     */
    initActive: function() {
        if(this.cfg.multiple) {
            this.cfg.active = [];

            if (this.stateHolder.val().length > 0) {
                var indexes = this.stateHolder.val().split(',');
                for(var i = 0; i < indexes.length; i++) {
                    this.cfg.active.push(parseInt(indexes[i]));
                }
            }
        }
        else {
            this.cfg.active = parseInt(this.stateHolder.val());
        }
    },

    /**
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.headers.mouseover(function() {
            var element = $(this);
            if(!element.hasClass('ui-state-active')&&!element.hasClass('ui-state-disabled')) {
                element.addClass('ui-state-hover');
            }
        }).mouseout(function() {
            var element = $(this);
            if(!element.hasClass('ui-state-active')&&!element.hasClass('ui-state-disabled')) {
                element.removeClass('ui-state-hover');
            }
        }).click(function(e) {
            var element = $(this);
            if(!element.hasClass('ui-state-disabled')) {
                var tabIndex = $this.headers.index(element);

                if(element.hasClass('ui-state-active')) {
                    $this.unselect(tabIndex);
                }
                else {
                    $this.select(tabIndex);
                    $(this).trigger('focus.accordion');
                }
            }

            e.preventDefault();
        });

        this.bindKeyEvents();
    },

    /**
     * @private
     */
    bindKeyEvents: function() {
        this.headers.on('focus.accordion', function(){
            $(this).addClass('ui-tabs-outline');
        })
        .on('blur.accordion', function(){
            $(this).removeClass('ui-tabs-outline');
        })
        .on('keydown.accordion', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                $(this).trigger('click');
                e.preventDefault();
            }
        });
    },

    /**
     * @private
     */
    markLoadedPanels: function() {
        if(this.cfg.multiple) {
            for(var i = 0; i < this.cfg.active.length; i++) {
                if(this.cfg.active[i] >= 0)
                    this.markAsLoaded(this.panels.eq(this.cfg.active[i]));
            }
        } else {
            if(this.cfg.active >= 0)
                this.markAsLoaded(this.panels.eq(this.cfg.active));
        }
    },

    /**
     * Activates (opens) the tab with given index. This may fail by returning `false`, such
     * as when a callback is registered that prevent the tab from being opened.
     * @param {number} index 0-based index of the tab to open. Must not be out of range.
     * @return {boolean} `true` when the given panel is now active, `false` otherwise. 
     */
    select: function(index) {
        var panel = this.panels.eq(index);

        //Call user onTabChange callback
        if(this.cfg.onTabChange) {
            var result = this.cfg.onTabChange.call(this, panel);
            if(result === false)
                return false;
        }

        var shouldLoad = this.cfg.dynamic && !this.isLoaded(panel);

        //update state
        if(this.cfg.multiple)
            this.addToSelection(index);
        else
            this.cfg.active = index;

        this.saveState();

        if(shouldLoad) {
            this.loadDynamicTab(panel);
        }
        else {
            if(this.cfg.controlled) {
                this.fireTabChangeEvent(panel);
            }
            else {
                this.show(panel);

                this.fireTabChangeEvent(panel);
            }

        }

        return true;
    },

    /**
     * Deactivates (closes) the tab with given index.
     * @param {number} index 0-based index of the tab to close. Must not be out of range.
     */
    unselect: function(index) {
        if(this.cfg.controlled) {
            this.fireTabCloseEvent(index);
        }
        else {
            this.hide(index);

            this.fireTabCloseEvent(index);
        }
    },

    /**
     * @private
     */
    show: function(panel) {
        var _self = this;

        //deactivate current
        if(!this.cfg.multiple) {
            var oldHeader = this.headers.filter('.ui-state-active');
            oldHeader.children('.ui-icon').removeClass(this.cfg.expandedIcon).addClass(this.cfg.collapsedIcon);
            oldHeader.attr('aria-selected', false);
            oldHeader.attr('aria-expanded', false).removeClass('ui-state-active ui-corner-top').addClass('ui-corner-all')
                .next().attr('aria-hidden', true).slideUp(function(){
                    if(_self.cfg.onTabClose)
                        _self.cfg.onTabClose.call(_self, panel);
                });
        }

        //activate selected
        var newHeader = panel.prev();
        newHeader.attr('aria-selected', true);
        newHeader.attr('aria-expanded', true).addClass('ui-state-active ui-corner-top').removeClass('ui-state-hover ui-corner-all')
                .children('.ui-icon').removeClass(this.cfg.collapsedIcon).addClass(this.cfg.expandedIcon);

        panel.attr('aria-hidden', false).slideDown('normal', function() {
            _self.postTabShow(panel);
        });
    },

    /**
     * @private
     */
    hide: function(index) {
        var _self = this,
        panel = this.panels.eq(index),
        header = panel.prev();

        header.attr('aria-selected', false);
        header.attr('aria-expanded', false).children('.ui-icon').removeClass(this.cfg.expandedIcon).addClass(this.cfg.collapsedIcon);
        header.removeClass('ui-state-active ui-corner-top').addClass('ui-corner-all');
        panel.attr('aria-hidden', true).slideUp(function(){
            if(_self.cfg.onTabClose)
                _self.cfg.onTabClose.call(_self, panel);
        });

        this.removeFromSelection(index);
        this.saveState();
    },

    /**
     * The content of a tab panel may be loaded dynamically on demand via AJAX. This method
     * loads the content of the given tab. Make sure to check first that this widget has got
     * dynamic tab panel (`cfg.dynamic`) and that the given tab panel is not loaded already
     * (`#isLoaded`).
     * @param {JQuery} panel A tab panel to load.
     */
    loadDynamicTab: function(panel) {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_contentLoad', value: true},
                {name: this.id + '_newTab', value: panel.attr('id')},
                {name: this.id + '_tabindex', value: parseInt(panel.index() / 2)}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            panel.html(content);

                            if(this.cfg.cache) {
                                this.markAsLoaded(panel);
                            }
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.show(panel);
            }
        };

        if(this.hasBehavior('tabChange')) {
            this.callBehavior('tabChange', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * @private
     */
    fireTabChangeEvent : function(panel) {
        if(this.hasBehavior('tabChange')) {
            var ext = {
                params: [
                    {name: this.id + '_newTab', value: panel.attr('id')},
                    {name: this.id + '_tabindex', value: parseInt(panel.index() / 2)}
                ]
            };

            if(this.cfg.controlled) {
                var $this = this;
                ext.oncomplete = function(xhr, status, args, data) {
                    if(args.access && !args.validationFailed) {
                        $this.show(panel);
                    }
                };
            }

            this.callBehavior('tabChange', ext);
        }
    },

    /**
     * @private
     */
    fireTabCloseEvent : function(index) {
        if(this.hasBehavior('tabClose')) {
            var panel = this.panels.eq(index),
            ext = {
                params: [
                    {name: this.id + '_tabId', value: panel.attr('id')},
                    {name: this.id + '_tabindex', value: parseInt(index)}
                ]
            };

            if(this.cfg.controlled) {
                var $this = this;
                ext.oncomplete = function(xhr, status, args, data) {
                    if(args.access && !args.validationFailed) {
                        $this.hide(index);
                    }
                };
            }

            this.callBehavior('tabClose', ext);
        }
    },

    /**
     * @private
     */
    markAsLoaded: function(panel) {
        panel.data('loaded', true);
    },

    /**
     * The content of a tab panel may be loaded dynamically on demand via AJAX. This method checks
     * whether the content of a tab panel is currently loaded.
     * @param {JQuery} panel A tab panel to check.
     * @return {boolean} `true` if the content of the tab panel is loaded, `false` otherwise.
     */
    isLoaded: function(panel) {
        return panel.data('loaded') == true;
    },

    /**
     * @private
     */
    addToSelection: function(nodeId) {
        this.cfg.active.push(nodeId);
    },

    /**
     * @private
     */
    removeFromSelection: function(nodeId) {
        this.cfg.active = $.grep(this.cfg.active, function(r) {
            return r != nodeId;
        });
    },

    /**
     * @private
     */
    saveState: function() {
        if(this.cfg.multiple)
            this.stateHolder.val(this.cfg.active.join(','));
        else
            this.stateHolder.val(this.cfg.active);
    },

    /**
     * @private
     */
    postTabShow: function(newPanel) {
        //Call user onTabShow callback
        if(this.cfg.onTabShow) {
            this.cfg.onTabShow.call(this, newPanel);
        }

        PrimeFaces.invokeDeferredRenders(this.id);
    }

});

This results in the following type definitions file:

Click to expand and view accordion.d.ts
namespace PrimeFaces {
  namespace Widget {
    /**
     * PrimeFaces AccordionPanel widget.
     The AccordionPanel is a container component that displays content in stacked format.
     *
     * @author Çağatay Çivici
     */
    class AccordionPanel extends PrimeFaces.widget.BaseWidget {
      /**
       * The DOM elements for the header of each tab.
       */
      headers: JQuery;
      /**
       * The DOM elements for the content of each tab pabel.
       */
      panels: JQuery;
      /**
       * The configuration of this accordion widget instance.
       */
      cfg: PrimeFaces.WidgetCfg.AccordionPanel;
      /**
       * @param cfg
       */
      init(cfg: PrimeFaces.WidgetCfg.AccordionPanel): void;
      /**
       * @param panel A tab panel to check.
       * @return `true` if the content of the tab panel is loaded, `false` otherwise.
       */
      isLoaded(panel: JQuery): boolean;
      /**
       * @param panel A tab panel to load.
       */
      loadDynamicTab(panel: JQuery): void;
      /**
       * @param index 0-based index of the tab to open. Must not be out of range.
       * @return `true` when the given panel is now active, `false` otherwise.
       */
      select(index: number): boolean;
      /**
       * @param index 0-based index of the tab to close. Must not be out of range.
       */
      unselect(index: number): void;
    }
  }
}
namespace PrimeFaces {
  namespace WidgetCfg {
    interface AccordionPanel extends PrimeFaces.WidgetCfg.BaseWidget {
      /**
       * List of tabs that are currenty active (open). Eaach item is a 0-based index of a tab.
       */
      active: number[];
      /**
       * `true` if activating a dynamic tab should not load the contents from server again and use the cached contents; or `false` if the caching is disabled.
       */
      cache: boolean;
      /**
       * The icon class name for the collapsed icon.
       */
      collapsedIcon: string;
      /**
       * `true` if a tab controller was specified for this widget; or `false` otherwise. A tab controller is a server side listener that decides whether a tab change or tab close should be allowed.
       */
      controlled: boolean;
      /**
       * `true` if the contents of each panel are loaded on-demand via AJAX; `false` otherwise.
       */
      dynamic: boolean;
      /**
       * The icon class name for the expanded icon.
       */
      expandedIcon: string;
      /**
       * `true` if multiple tabs may be open at the same time; or `false` if opening one tab closes all other tabs.
       */
      multiple: boolean;
      /**
       * `true` if the current text direction `rtl` (right-to-left); or `false` otherwise.
       */
      rtl: boolean;
    }
  }
}

The script itself parses the JavaScript source code and checks the doc comments for type annotations. This worked out pretty nice:

  • Since I’m already parsing the JavaScript, I can check the code against doc comments and output warnings. Such as when there widget methods without a corresponding doc comment. This should make it a lot easier to keep it in sync. It would also be possible to even make the maven build fail if there are undocumented methods etc.
  • I spent some extra time to make it future-proof and ensure new JavaScript features are supported and can be documented in case you want to start using them: async and generator functions, function parameter initializers, function rest arguments, destructured arguments.
  • It’s also not a big commitment: If you ever decide it would be better to just have a static .d.ts file, switching is easy as you can just take the generated file and edit that.
  • Internal methods can be hidden by adding the @private tag.

What do you think? In case you like it, the next step would be to go ahead and add some actual type definitions to the widget JavaScript files.

PS: Here’s a pastebin of the script run against all widgets. All type annotations are of course still any.

0reactions
mellowarecommented, Apr 18, 2020

Yes lets leave it strict it caught me where my params were not right and I didn’t have a return statement.

Read more comments on GitHub >

github_iconTop Results From Across the Web

TypeScript Definitions & JavaScript API Library - GetStream.io
Adding TypeScript Type Definitions to the Stream JavaScript API Client Library · A TypeScript primer · Options for distributing Type Definitions.
Read more >
Add your own Type Definition to any Javascript 3rd party module
In the folder src/local-types/org-name__package-name , create a declaration file, where you will write the module's type definition, name index.
Read more >
Create a type definitions file for the client-side JavaScript API
This issue is for discussing and creating a types definitions file for the JavaScript API of PrimeFaces - with the goal of enabling...
Read more >
How to Write TypeScript Ambients Types Definition for a ...
Types definitions made easy for any JavaScript library. Create, extend, and contribute to any repository where types are missing.
Read more >
JavaScript modules - MDN Web Docs
This guide gives you all you need to get started with JavaScript module syntax.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found