Let’s start TypeScript – Part 1

TypeScript isn't really a new language, it's a way of writing code in the next generation of JavaScript before it's fully supported by all browsers. By the time that browser support for ES6 is there, we'll be all writing TypeScript using ES9 features that 'transpile' down to ES6 code! Of course, there are cool features of the typescript compiler and related tools that are nothing to do with the JavaScript language standard, but you get the idea!

The key point is that TypeScript is a superset of JavaScript. JavaScript is TypeScript without all the new features and strict compiler checks. For this reason, TypeScript is nothing to be afraid of if you are already developing in JavaScript. You can convert your JavaScript into TypeScript with very little effort and then start to make use of the new features available in ES6 gradually over time.

This 2 part post is going to take the DependantOptionset.js JavaScript from the olds SDK samples and walk through converting it to TypeScript in the hope that it will help you see how easy it is to start using TypeScript!

Step 1 – Setup up your TypeScript project

I'm going to use Visual Studio 2017 because I like the fact that I can develop C# Plugins and TypeScript in the same IDE – but you could equally use VSCode.

  1. Download and install Node.js from https://nodejs.org/en/
  2. Select the 'Current' build and install.
  3. Open Visual Studio and install the 'Open Command Line' extension using Tools->Extensions and Updates…
  4. Search for 'Open Command Line' and select Download
  5. Restart Visual Studio to install the extension
  6. Select New -> Project
  7. Pick ASP.NET Empty Web Site
  8. Select Add->Add New Item…
  9. Select TypeScript config file
  10. Add the compileOnSave option:
    {
        "compilerOptions": {
            "noImplicitAny": false,
            "noEmitOnError": true,
            "removeComments": false,
            "sourceMap": true,
            "target": "es5"
        },
      "compileOnSave":  true,
      "exclude": [
        "node_modules",
        "wwwroot"
      ]
    }
  11. Add a folder WebResources with a subfolder js, and then place the SDK.DependentOptionSet.js file in that folder (you can pick up the file from the old SDK sample)
  12. I use spkl to deploy Web Resources, so use the NuGet Package Manager (Tools-> NuGet Package Manager -> Package Manager Console) to run
    Install-Package spkl
  13. Edit the spkl.json file to point to the SDK.DependentOptionSet.js file:
    {
      "webresources": [
        {
          "profile": "default,debug",
          "root": "Webresources/",
          "files": [
            {
                "uniquename": "sdk_/js/SDK.DependentOptionSet.js",
                "file": "js\\SDK.DependentOptionSet.js",
                "description": ""
            }
          ]
        }
      ]
    }
  14. Select the Web Site Project in the Solution Explorer and use the Alt-Space shortcut to open the command line
  15. Enter the following command on the command prompt that popups up:
    npm init
  16. Accept all the defaults
  17. You should now see a packages.json file in your project folder.
  18. To install the TypeScript definitions for working with the Dynamics client-side SDK, On the same console window enter:
    npm install @types/xrm --save-dev
  19. You will now have a node_modules folder in your project directory.

 

Step 2 – Updates to allow JavaScript to compile as TypeScript

Now you've got your project ready, we can start to use TypeScript.

  1. Copy the SDK.DependentOptionSet.js to a new folder named src and rename to be .ts rather than .js
  2. If you open the TypeScript file you'll start to see some errors that the TypeScript compiled has found since it is adding some stricter checks than ES5 JavaScript:

  3. Let's sort out the namespaces – in ES5 JavaScript, there was no concept of modules or namespaces so we had to manually contruct them – in this case we are creating a namespace of SDK with effectively a type called DependentOptionSet. We can convert this to the following TypeScript:
    namespace SDK {
        export class DependentOptionSet {
            static init(webResourceName) {
    Notice how the class is marked as 'export' so that it will be accessible to other TypeScript code, and the function pointer field init has be converted into a static method.
  4. We can repeat this for all the function pointer fields on the DependentOptionSet class and do the same for the Util class.

The outline will look like:

Step 3 – Attribute Strong Typing

  1. If you look at the methods now you'll start to notice that there are some more errors that the typescript compiled needs help with. The first we'll look at is there is an error on getValue(). This is because the typing for the attribute collection isn't what is expected.

  2. In order that the attribute variables are typed correctly, we change:
    Xrm.Page.data.entity.attributes.get(parentField)
    to
    Xrm.Page.getAttribute<Xrm.Attributes.OptionSetAttribute>(childField)
     
  3. The same should be repeated for the line:
    Xrm.Page.data.entity.attributes.get(parent)
  4. Repeat this for both ParentField and ChildField
  5. Next, we need to deal with TypeScript's type expectations:

  6. JavaScript allows fields to be defined on objects without any predefined typing, but TypeScript requires that we define the type at the point of assignment to the mapping variable.
    Consequently, the mapping type needs to be defined as follows:
    var mapping = {
        parent : ParentField.getAttribute("id"),
        dependent : SDK.Util.selectSingleNode(ParentField, "DependentField").getAttribute("id"),
        options : []
    };
    The same technique needs to be then repeated for the option and optionToShow variables.

Step 4 – Class level fields

The next issue that is highlighted by the compiler is that JavaScript allows assigning field level variables without prior definition.

  1. We must add the config field variable into the DependentOptionSet class definition:
    export class DependentOptionSet {
        static config = [];
  2. Now that there are no more compile errors, you should start to see the JavaScript generated:
 

Step 5 – Taking it one step further

You can now start to turn on stricter checks. The most common is adding to your tsconfig.json under compilerOptions:

"noImplicitAny": true,
  1. You'll now start to see errors in your TypeScript where there is no type inferable from the code:
  2. In this case we need to add some additional types for the Xml Http types that the browser use, so edit your tsconfig.json and the following to the compilerOptions:
    "lib": [ "dom","es5" ]
  3. You can now change the signature of completeInitialization to:
    static completeInitialization(xhr : XMLHttpRequest) {
  4. We'll also need to sort out other type inference issues such as:
  5. We can add the following type definitions
    namespace SDK {
        class Option {
            value: string;
            showOptions: string[]
        }
     
    
        class Mapping {
            parent: string;
            dependent: string;
            options: Option[]
        }
  6. Finally, the code should look like:
namespace SDK {
    class Option {
        value: number;
        text?: string;
        showOptions?: Xrm.OptionSetValue[]
    }
 

    class Mapping {
        parent: string;
        dependent: string;
        options: Option[]
    }
 

    export class DependentOptionSet {
        static config: Mapping[] = [];
        static init(webResourceName : string) {
            //Retrieve the XML Web Resource specified by the parameter passed
            var clientURL = Xrm.Page.context.getClientUrl();
 

            var pathToWR = clientURL + "/WebResources/" + webResourceName;
            var xhr = new XMLHttpRequest();
            xhr.open("GET", pathToWR, true);
            xhr.setRequestHeader("Content-Type", "text/xml");
            xhr.onreadystatechange = function () { SDK.DependentOptionSet.completeInitialization(xhr); };
            xhr.send();
        }
 

        static completeInitialization(xhr : XMLHttpRequest) {
            if (xhr.readyState == 4 /* complete */) {
                if (xhr.status == 200) {
                    xhr.onreadystatechange = null; //avoids memory leaks
                    var JSConfig: Mapping[] = [];
                    var ParentFields = xhr.responseXML.documentElement.getElementsByTagName("ParentField");
                    for (var i = 0; i < ParentFields.length; i++) {
                        var ParentField = ParentFields[i];
                        var mapping : Mapping = {
                            parent : ParentField.getAttribute("id"),
                            dependent : SDK.Util.selectSingleNode(ParentField, "DependentField").getAttribute("id"),
                            options : []
                        };
 

                        var options = SDK.Util.selectNodes(ParentField, "Option");
                        for (var a = 0; a < options.length; a++) {
                            var option : Option = {
                                value: parseInt(options[a].getAttribute("value")),
                                showOptions: [],
                            };
                            var optionsToShow = SDK.Util.selectNodes(options[a], "ShowOption");
                            for (var b = 0; b < optionsToShow.length; b++) {
                                var optionToShow : Xrm.OptionSetValue = {
                                    value: parseInt(optionsToShow[b].getAttribute("value")),
                                    text: optionsToShow[b].getAttribute("label")
                                };
                                option.showOptions.push(optionToShow)
                            }
                            mapping.options.push(option);
                        }
                        JSConfig.push(mapping);
                    }
                    //Attach the configuration object to DependentOptionSet
                    //so it will be available for the OnChange events 
                    SDK.DependentOptionSet.config = JSConfig;
                    //Fire the onchange event for the mapped optionset fields
                    // so that the dependent fields are filtered for the current values.
                    for (var depOptionSet in SDK.DependentOptionSet.config) {
                        var parent = SDK.DependentOptionSet.config[depOptionSet].parent;
                        Xrm.Page.getAttribute(parent).fireOnChange();
                    }
                }
            }
        }
 

        // This is the function set on the onchange event for 
        // parent fields
        static filterDependentField(parentField: string, childField : string) {
            for (var depOptionSet in SDK.DependentOptionSet.config) {
                var DependentOptionSet = SDK.DependentOptionSet.config[depOptionSet];
                /* Match the parameters to the correct dependent optionset mapping*/
                if ((DependentOptionSet.parent == parentField) && (DependentOptionSet.dependent == childField)) {
                    /* Get references to the related fields*/
                    var ParentField = Xrm.Page.getAttribute<Xrm.Attributes.OptionSetAttribute>(parentField);
                    var ChildField = Xrm.Page.getAttribute<Xrm.Attributes.OptionSetAttribute>(childField);
                    /* Capture the current value of the child field*/
                    var CurrentChildFieldValue = ChildField.getValue();
                    /* If the parent field is null the Child field can be set to null */
                    if (ParentField.getValue() == null) {
                        ChildField.setValue(null);
                        ChildField.setSubmitMode("always");
                        ChildField.fireOnChange();
 

                        // Any attribute may have any number of controls
                        // So disable each instance
                        var controls = ChildField.controls.get()
 

                        for (var ctrl in controls) {
                            controls[ctrl].setDisabled(true);
                        }
                        return;
                    }
 

                    for (var os in DependentOptionSet.options) {
                        var Options = DependentOptionSet.options[os];
                        var optionsToShow = Options.showOptions;
                        /* Find the Options that corresponds to the value of the parent field. */
                        if (ParentField.getValue() == Options.value) {
                            var controls = ChildField.controls.get();
                            /*Enable the field and set the options*/
                            for (var ctrl in controls) {
                                controls[ctrl].setDisabled(false);
                                controls[ctrl].clearOptions();
 

                                for (var option in optionsToShow) {
                                    controls[ctrl].addOption(optionsToShow[option]);
                                }
 

                            }
                            /*Check whether the current value is valid*/
                            var bCurrentValueIsValid = false;
                            var ChildFieldOptions = optionsToShow;
 

                            for (var validOptionIndex in ChildFieldOptions) {
                                var OptionDataValue = ChildFieldOptions[validOptionIndex].value;
 

                                if (CurrentChildFieldValue == OptionDataValue) {
                                    bCurrentValueIsValid = true;
                                    break;
                                }
                            }
                            /*
                            If the value is valid, set it.
                            If not, set the child field to null
                            */
                            if (bCurrentValueIsValid) {
                                ChildField.setValue(CurrentChildFieldValue);
                            }
                            else {
                                ChildField.setValue(null);
                            }
                            ChildField.setSubmitMode("always");
                            ChildField.fireOnChange();
                            break;
                        }
                    }
                }
            }
        }
    }
 

    export class Util {
        //Helper methods to merge differences between browsers for this sample
        static selectSingleNode(node: Element, elementName : string) {
            if ((<any>node).selectSingleNode) {
                return <Element>(<any>node).selectSingleNode(elementName);
            }
            else {
                return node.getElementsByTagName(elementName)[0];
            }
        }
 

        static selectNodes(node: Element, elementName : string) {
            if ((<any>node).selectNodes) {
                return <NodeListOf<Element>>(<any>node).selectNodes(elementName);
            }
            else {
                return node.getElementsByTagName(elementName);
            }
        }
    }
 

}

The differences between the original JavaScript and the TypeScript are mostly structural with some additional strongly typing:

The resulting differences between the original JavaScript and the final compiled TypeScript are also minimal:

Of course, the key difference is now that we have strong type checking through type inference. Type inference is your friend – the compiler knows best!

In the next part, I'll show you how to deploy and debug this TypeScript using source maps.

 

Comments are closed