Wednesday, April 20, 2016

Scrollable Windows 10 Universal JavaScript App

As I'm working more and more on Windows 10 Universal JavaScript Application development, I just can not help but wonder how more people are not on this platform.  With all those web developers out there, you'd think it would be an easy win.

One thing that I found extremely hard to find information about is how to create vertically scrollable areas that are full window height (or full minus some other content area).

At any rate, searching for how to make a UWP app scrollable if the body content exceeds the window height returns barely anything relevant and mostly related to the XAML side of things.  The best answer I could find was to set an explicit height on the element and set the overflow-y: scroll.  This does not work if your screen size changes so I decided to dig in a little bit.

Here are a few different approaches you could take...

Body Fixed Height

You can apply styling to the body element via css to assign a fixed height and set the overflow-y to scroll.

body {
    height: 500px;
    overflow-y: scroll;
}
This will give you a 500px tall, scrollable body.  This is very unlikely to be what you want as you probably want the height to be the window height.

Body Dynamic Height

Since the height of the body should be the height of the window, you'll need to execute some javascript to get this to work.  The page height is not known until the page is rendered.  Even then, the window could be resized, etc.  Here is how to deal with those concerns.

body {
    overflow-y: scroll;
}

function setBodyToWindowHeight() {
    $('body').height($(window).innerHeight());
}

$(function(){ 
    setBodyToWindowHeight(); 
    window.resize(setBodyToWindowHeight); 
});
This will set the body to window height on document ready and whenever the window is resized.

Element Dynamic Height

There may be cases where you do not want the entire body to scroll, but instead some region in the page.  Maybe there is a static header and footer and the middle section needs to scroll.  In this case, you can do something similar if the header and footer do not change after page load.

body {
    overflow-y: scroll;
}

function setElementToRemainingWindowHeight(selector, usedHeight) {
    $(selector).height($(window).innerHeight() - usedHeight);
}

function calculateUsedHeight() {
    return $('.header').innerHeight() + $('footer').innerHeight();
}

$(function(){
    setElementToRemainingWindowHeight('#scrollingRegion', calculateUsedHeight());
    window.resize(function() {
        setElementToRemainingWindowHeight('#scrollingRegion', calculateUsedHeight());
    });
});
This will find the used height by finding the inner height of the header and footer and subtracting from the window height.

Element Dynamic Height with Dynamic Layout

There may be a case where the element's height depends on what other content happens to be present and visible on the page.  For instance, maybe the header has a slider that pushes the body and footer down.  In the previous example, the footer would be pushed off the page and you would not be able to scroll to it.

This starts to get a little messy since we have to register callbacks with everything that can change the page.  Assume it's some knockout observable like isInfoExpanded that tells us when the information panel is open.

body {
    overflow-y: scroll;
}

function setElementToRemainingWindowHeight(selector, usedHeight) {
    $(selector).height($(window).innerHeight() - usedHeight);
}

function calculateUsedHeight() {
    return $('.header').innerHeight() + $('footer').innerHeight() + $('#infoPanel').innerHeight();
}

$(function(){
    setElementToRemainingWindowHeight('#scrollingRegion', calculateUsedHeight());
    window.resize(function() {
        setElementToRemainingWindowHeight('#scrollingRegion', calculateUsedHeight());
    });

    // this is what I would like to do, but it has a problem...
    // when calculating the used height, the info panel's height is 0 when newVal = true
    // because it has not rendered yet, only the observable value has updated
    // and it is not obvious how to wait until afterRender or throttle a subscribe call
    //isInfoExpanded.subscribe(function(newVal) {
    //    setElementToRemainingWindowHeight('#scrollingRegion', calculateUsedHeight());
    //});

    // instead, gotta be dirty and use a computed
    // further, since we don't actually need to use the value returned by the computed, but need to still
    // access it for updates, more dirty code can be used :)
    ko.computed(function() { isInfoExpanded() || !isInfoExpanded() &&
        setElementToRemainingWindowHeight('#scrollingRegion', calculateUsedHeight());
    }).extend({throttle: 100}); // this allows time for the rendering
});

This will update the scrolling region's height based on the available height of the window, considering whether the info panel is showing.

Dynamic Elements with Dynamic Height and Dynamic Layout

Phew, so in the previous case, we have an element in the page that will always exist that is having its height manipulated.  What happens if the element whose height is dynamic does not exist in the page when the layout changes.

Consider a button in the layout panel that creates a div whose height should be the available window height.  The above example will not work without having another subscription that somehow is fired when the new div is created and it waits until the div is rendered to set the height, ...

Instead, lets just alter the css rules for the page to accommodate any element popping into the page.

<div data-bind="visible: observableValue">Info Panel</div>
<div data-bind="if: observableValue">
    <div class="scrollableContent">True observable scrolling area</div>
</div>
<div data-bind="ifnot: observableValue">
  <div class="scrollableContent">False observable scrolling area</div>
<div>

In the above markup, the divs with class scrollableContent are only in the page when the condition is true so we cannot target them via jQuery to set the height without the complications mentioned earlier.

Instead, we'll simply alter the rule for scrollableContent to be the desired height.


export class ScrollableContentHelper {
    getCSSRule(ruleName:string, deleteFlag?:string) {
        for (var i = 0; i < document.styleSheets.length; i++) {
            var styleSheet : any = document.styleSheets[i];
            var ii = 0;
            do { // For each rule in stylesheet
                if (styleSheet.rules[ii] && styleSheet.rules[ii].selectorText == ruleName) {
                    return styleSheet.rules[ii]; // return the style object.
                }
                ii++;
            } while (styleSheet.rules[ii]) // end While loop
        } // end For loop
        return false; // we found NOTHING!
    }

    giveSelectorRemainingHeight(ruleName, usedHeightCalculation: () => number) {
        this.getCSSRule(ruleName).style.height = ($(window).innerHeight() - usedHeightCalculation()) + "px";
    }
}


Here is a TypeScript class that can help manage this process.  The giveSelectorRemainingHeight function takes a callback that is run to calculate the used height before setting the element height.

Then, in your default.js file (or whatever is powering your page), instead of using jQuery to set the height of elements by a selector, we'll modify the css rules to do the same thing.


args.setPromise(WinJS.UI.processAll().then(function () { 
    window['applicationViewModel'] = new ApplicationViewModel(); 
    var scrollHelper = new scrollableHelper.ScrollableContentHelper(); 
    ko.computed(() => window['applicationViewModel'].observableValue() &&
                         scrollHelper.giveSelectorRemainingHeight(".scrollContent",
                                                                  () => $("div.header").innerHeight() +  $('footer').innerHeight() + 225))
      .extend({throttle: 100}); 
}));


With this, when the observable value changes, instead of finding the elements and adjusting their height, the css rule is changed to set the height of that selector globally (so that elements that appear after the code is run are still affected).

So it would seem this is not really a UWP concern as much as how to use javascript to dynamically set the height of elements in the page.  By default, web pages viewed in a browser are scrollable when the content exceeds the browser window's height.  In a UWP app, however, no scrolling is provided automatically when the body content exceeds the window's height.  This was the confusing part of me and hopefully this code helps the other developer that is actually creating a UWP application using javascript...

Monday, April 4, 2016

Windows 10 Universal JS + Typescript = Broken in 2015 Update 2

I have been working on a windows universal 10 javascript application and everything was going great.  All my familiar tools were working perfectly and very little change needed to get things working the same way I usually develop.

I needed to re-pave my laptop and tried using a boxstarter script (more on this in another post).  Just about everything was going well, but when I opened the previously working project, it no longer had support for typescript.  Everything that was included with TypeScriptCompile tags were not included in the project and it would only run one time before erroring out saying that it was unable to find the produced js files.

After hours of wondering if my install failed, I was finally able to find an issue submitted on github.  Using Azure Virtual Machines, I was able to identify that the problem was actually related to Visual Studio 2015 Update 2 (see my comments on the issue with included pictures).  TypeScript seemed to work fine in other projects, but just not universal javascript apps.

There seems to be no one else on the internet with this problem, however.  It was insanely difficult to find even this issue, which frankly had very little information inside.  It makes me wonder if people are just not writing universal JS apps or if they are just no using typescript (which is crazy to me).

So far the universal javascript app experience has been quite amazing and very familiar to typical web development.  Hopefully people will begin writing these apps more and support for them improves.