Monday, 29 February 2016

Creating Angular UI Routes in the Controller

So you're creating a link with the Angular UI Router. You're passing more than a few parameters and it's getting kinda big. Something like this:

    <a class="contains-icon" 
       ui-sref="Entity.Edit({ entityId: (vm.selectedEntityId ? vm.selectedEntityId: null), initialData: vm.initialData })">
         <i class="fa fa-pencil"></i>Edit
    </a>

See? It's too long to fit on the screen without wrapping. It's clearly mad and bad.

Generally I try to keep the logic in a view to a minimum. It makes the view harder to read, it makes behaviour of the app harder to reason about. Also, it's not testable and (if you're using some kind of static typing like TypeScript) it is entirely out of the realms that a compiler can catch. So what to do? Move the URL generation to the controller. That's what I decided to do after I had a typo in my view which I didn't catch until post-commit.

ui-sref in the Controller

Actually, that's not exactly what you want to do. If you look at the Angular UI Router docs you will see that ui-sref is:

...a directive that binds a link (<a> tag) to a state. If the state has an associated URL, the directive will automatically generate & update the href attribute via the $state.href() method.

So what we actually want to do is use the $state.href() method in our controller. To take our example above we'll create another method on our controller called getEditUrl

export class EntityController {

    $state: angular.ui.IStateService;

    static $inject = ["$state"];
    constructor($state: angular.ui.IStateService) {
        this.$state = $state;
    }

    //... Other stuff

    getEditUrl() {
        return this.$state.href("Entity.Edit", { 
            selectedEntityId: this.selectedEntityId ? this.selectedEntityId: null, 
            initialData: this.initialData 
        });
    }
}

You can see I'm using TypeScript here; but feel free to strip out the type annotations and go with raw ES6 classes; that'll still give you testability if not static typing.

Now we've added the getEditUrl method we just need to reference it in our view:

    <a class="contains-icon" ng-href="{{vm.getEditUrl()}}"><i class="fa fa-pencil"></i>Edit</a>

Note we've ditched usage of the ui-sref directive and gone with Angular's native ng-href. Within that directive we execute our getEditUrl as an expression which gives us our route. As a bonus, our view is much less cluttered and comprehensible as a result. How lovely.

Friday, 19 February 2016

Visual Studio, tsconfig.json and external TypeScript compilation

TypeScript first gained support for tsconfig.json back with the 1.5 release. However, to my lasting regret and surprise Visual Studio will not be gaining meaningful support for it until TypeScript 1.8 ships. However, if you want it now, it's already available to use in beta.

I've already leapt aboard. Whilst there's a number of bugs in the beta it's still totally usable. So use it.

External TypeScript Compilation and the VS build

Whilst tsconfig.json is useful and super cool it has limitations. It allows you to deactivate compilation upon file saving using compileOnSave. What it doesn't allow is deactivation of the TypeScript compilation that happens as part of a Visual Studio build. That may not matter for the vanilla workflow of just dropping TypeScript files in a Visual Studio web project and having VS invoke the TypeScript compilation. However it comes to matter when your workflow deviates from the norm, as mine does. Using external compilation of TypeScript within Visual Studio is a little tricky. My own use case is somewhat atypical but perhaps not uncommon.

I'm working on a project which has been built using TypeScript since TS 0.9. Not surprisingly, this started off using the default Visual Studio / TypeScript workflow. Active development on the project ceased around 9 months ago. Now it's starting up again. It's a reasonable sized web app and the existing functionality is, in the main, fine. But the users want to add some new screens.

Like any developer, I want to build with the latest and greatest. In my case, this means I want to write modular ES6 using TypeScript. With this approach my code can be leaner and I have less script ordering drama in my life. (Yay import statements!) This can be done by bringing together webpack, TypeScript (ts-loader) and Babel (babel-loader). There's an example of this approach here. Given the size of the existing codebase I'd rather leave the legacy TypeScript as is and isolate my new approach to the screens I'm going to build. Obviously I'd like to have a common build process for all the codebase at some point but I've got a deadline to meet and so a half-old / half-new approach is called for (at least for the time being).

Goodbye TypeScript Compilation in VS

Writing modular ES6 TypeScript which is fully transpiled to old-school JS is not possible using the Visual Studio tooling at present. For what it's worth I think that SystemJS compilation may make this more possible in the future but I don't really know enough about it to be sure. That's why I'm bringing webpack / Babel into the mix right now. I don't want Visual Studio to do anything for the ES6 code; I don't want it to compile. I want to deactivate my TypeScript compilation for the ES6 code. I can't do this from the tsconfig.json so I'm in a bit of a hole. What to do?

Well, as of (I think) TypeScript 1.7 it's possible to deactivate TypeScript compilation in Visual Studio. To quote:

there is an easier way to disable TypeScriptCompile:

Just add <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> to the .csproj, e.g. in the first <PropertyGroup>.

Awesomeness!

But wait, this means that the legacy TypeScript isn't being compiled any longer. Bear in mind, I'm totally happy with the existing / legacy TypeScript compilation. Nooooooooooooooo!!!!!!!!!!!!!!!

Hello TypeScript Compilation outside VS

Have no fear, I gotcha. What we're going to do is ensure that Visual Studio triggers 2 external TypeScript builds as part of its own build process:

  • The modular ES6 TypeScript (new)
  • The legacy TypeScript (old)

How do we do this? Through the magic of build targets. We need to add this to our .csproj: (I add it near the end; I'm not sure if location matters though)

  <PropertyGroup>
    <CompileDependsOn>
      $(CompileDependsOn);
      WebClientBuild;
    </CompileDependsOn>
    <CleanDependsOn>
      $(CleanDependsOn);
      WebClientClean
    </CleanDependsOn>
    <CopyAllFilesToSingleFolderForPackageDependsOn>
      CollectGulpOutput;
      CollectLegacyTypeScriptOutput;
      $(CopyAllFilesToSingleFolderForPackageDependsOn);
    </CopyAllFilesToSingleFolderForPackageDependsOn>
    <CopyAllFilesToSingleFolderForMsdeployDependsOn>
      CollectGulpOutput;
      CollectLegacyTypeScriptOutput;
      $(CopyAllFilesToSingleFolderForPackageDependsOn);
    </CopyAllFilesToSingleFolderForMsdeployDependsOn>
  </PropertyGroup>
  <Target Name="WebClientBuild">
    <Exec Command="npm install" />
    <Exec Command="npm run build-legacy-typescript" />
    <Exec Command="npm run build -- --mode $(ConfigurationName)" />
  </Target>
  <Target Name="WebClientClean">
    <Exec Command="npm run clean" />
  </Target>
  <Target Name="CollectGulpOutput">
    <ItemGroup>
      <_CustomFiles Include="dist\**\*" />
      <FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
        <DestinationRelativePath>dist\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
      </FilesForPackagingFromProject>
    </ItemGroup>
    <Message Text="CollectGulpOutput list: %(_CustomFiles.Identity)" />
  </Target>
  <Target Name="CollectLegacyTypeScriptOutput">
    <ItemGroup>
      <_CustomFiles Include="Scripts\**\*.js" />
      <FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
        <DestinationRelativePath>Scripts\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
      </FilesForPackagingFromProject>
    </ItemGroup>
    <Message Text="CollectLegacyTypeScriptOutput list: %(_CustomFiles.Identity)" />
  </Target>

There's a few things going on here; let's take them one by one.

The WebClientBuild Target

This target triggers our external builds. One by one it runs the following commands:

npm install
Installs the npm packages.
npm run build-legacy-typescript
Runs the "build-legacy-typescript" script in our package.json
npm run build -- --mode $(ConfigurationName)
Runs the "build" script in our package.json and passes through a mode parameter of either "Debug" or "Release" from MSBuild - indicating whether we're creating a debug or a release build.

As you've no doubt gathered, I'm following the convention of using the scripts element of my package.json as repository for the various build tasks I might have for a web project. It looks like this:

{
  // ...
  "scripts": {
    "test": "karma start --reporters mocha,junit --single-run --browsers PhantomJS",
    "build-legacy-typescript": "tsc -v&&tsc --project Scripts",
    "clean": "gulp delete-dist-contents",
    "watch": "gulp watch",
    "build": "gulp build"
  },
  // ...
}

As you can see, "build-legacy-typescript" invokes tsc (which is registered as a devDependency) to print out the version of the compiler. Then it invokes tsc again using the project flag directly on the Scripts directory. This is where the legacy TypeScript and its associated tsconfig.json resides. Et voilá, the old / existing TypeScript is compiled just as it was previously by VS itself.

Next, the "build" invokes a gulp task called, descriptively, "build". This task caters for our brand new codebase of modular ES6 TypeScript. When run, this task will invoke webpack, copy static files, build less etc. Quick digression: you can see there's a "watch" script that does the same thing on a file-watching basis; I use that during development.

The WebClientClean Target

The task that runs to clean up artefacts created by WebClientBuild.

The CollectLegacyTypeScriptOutput and CollectGulpOutput Targets

Since we're compiling our TypeScript outside of VS we need to tell MSBuild / MSDeploy about the externally compiled assets in order that they are included in the publish pipeline. Here I'm standing on the shoulders of Steve Cadwallader's excellent post. Thanks Steve!

CollectLegacyTypeScriptOutput and CollectGulpOutput respectively include all the built files contained in the "Scripts" and "dist" folders when a publish takes place. You don't need this for when you're building on your own machine but if you're looking to publish (either from your machine or from TFS) then you will need exactly this. Believe me that last sentence was typed with a memory of great pain and frustration.

So in the end, as far as TypeScript is concerned, I'm using Visual Studio solely as an editor. It's the hooks in the .csproj that ensure that compilation happens. It seems a little quirky that we still need to have the original TypeScript targets in the .csproj file as well; but it works. That's all that matters.

Monday, 1 February 2016

TFS 2012, .NET 4.5 and C# 6

So, you want to use C# 6 language features and you’re working on an older project that’s still rocking .NET 4.5. Well, with some caveats, you can.

The new compiler will compile targeting older framework versions. Well that’s all lovely; let’s all go home.

Now. What say you’ve got an old, old build server? It’s TFS 2012 Update 2, creaking away, still glad to alive and kind of wondering why it hasn’t been upgraded or retired. This is where you want to compile .NET 4.5 from C# 6. Well it can be done. Here’s how it’s done:

  1. Install Visual Studio 2015 on the build server (I’m told this can be achieved using Microsoft Build Tools 2015 but I haven’t tried it myelf so caveat emptor)
  2. set the MSBuild Arguments in the build definition to /p:VisualStudioVersion=14.0 (i.e. Visual Studio 2015 mode)
  3. in each project that uses C# 6 syntax, install the NuGet package Microsoft.Net.Compilers with a quick install-package Microsoft.Net.Compilers

That’s it; huzzah! String interpolation here I come…