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...
Great post! I found this via your SO response, exactly the same problem I was having (here, a few years later, and the same workarounds needed!) I would hope web developers are using this platform more.
ReplyDeleteJust wanted to note, today, you have to use:
window.addEventListener("resize", function() {etc.})
instead of formerly:
window.resize(function() {etc.})
...or at least, _I_ had to. Thanks again for your post, solving this was a deciding factor for what languages/platforms/frameworks/environments to use for a particular set of apps (html, css, js; uwp; .net, bootstrap, jquery, vue; vs2017 and vs code).