Detecting Screen Orientation Change

May 2, 2016

Browsers provide different means to detect screen orientation:

Documentation in the Mozilla Developer Network (linked above) states the first to be deprecated but currently still in the WhatWG Living Standard, whereas its documentation on the latter differs from the W3C documentation.

According to documentation, detection of screen orientation change can be achieved by implementing handlers for the events

  • window.orientationchange
  • screen.orientation.change
  • window.matchMedia() listener
  • window.resize

but specific browsers may not support all of these events, with window.resize being the catch-all solution if everything else fails.

So based on SO answers and this blog and this blog I came up with a solution that currently seems to work, and a couple of findings:

  • window.orientation gives the angle on mobile browsers only – desktop browsers always contain 0 (zero).
  • Similarly, window.onorientationchange is only supported by mobile browsers.
  • screen.orientation (and its browser-spezific siblings mozOrientation and msOrientation) contains the angle in its angle property. IE11 does support support screen.orientation on Win7. Mobile Chrome (35) and the Android 4.4.2 Browser do not seem to support it either.
  • Of the browsers I tested, none seem to implement the event screen.orientation.onchange.
  • Orientation change can be detected using the window.matchMedia() listener on both mobile and desktop browsers which support mediaqueries and its orientation selector.
  • In desktop browsers, orientation can only be derived from $(window).width() and $(window).height(), or from the .matches property of a matchMedia listener.

Note that all this need not apply for older browsers, not even the values of window.orientation! (See SO, SO, SO, Giff’s note)

So here now is my JavaScript code for screen orientation change detection:

function doOnOrientationChange(src)
{
  if (window.console && console.log) 
    console.log("width " + $(window).width() + " height " + $(window).height());

  var orientation = { 
    angle: window.orientation,
    type: ("onorientationchange" in window) ? "mobile" : "desktop"  
  };

  if (window.screen) {
    var o = window.screen.orientation || window.screen.mozOrientation 
      || window.screen.msOrientation || orientation;
    orientation = { angle: o.angle, type: o.type };
  } else if ((window.orientation === 0) || window.orientation) {
    orientation = { angle: window.orientation, type: "" + window.orientation + " degrees" };
  }
 
  if (!("onorientationchange" in window)) {
    var w = $(window).width(), h =$(window).height();
    var a = (w > h) ? 90 : 0;
    orientation.angle = a;
    if (window.console && console.log) 
      console.log("angle := " + a + " " + orientation.angle);
  }
 
  var jsonOrientation = JSON.stringify(
    { angle: orientation.angle, type: orientation.type });

  switch(orientation.angle) 
  { 
    case -90:
    case 90:
      // we are in landscape mode
      $().toastmessage('showNoticeToast', src + ' landscape ' + " " + jsonOrientation);
      if (window.console && window.console.log) console.log(src + ' landscape ' + " " + jsonOrientation);
      $("#orientation").text(src + ' landscape ' + " " + jsonOrientation);
      break; 
    case 0:
    case 180:
      // we are in portrait mode
      $().toastmessage('showNoticeToast', src + ' portrait ' + " " + jsonOrientation);
      if (window.console && window.console.log) console.log(src + ' portrait ' + " " + jsonOrientation);
      $("#orientation").text(src + ' portrait ' + " " + jsonOrientation);
      break; 
    default:
      // we have no idea
      $().toastmessage('showNoticeToast', src + ' unknown ' + " " + jsonOrientation);
      if (window.console && window.console.log) console.log(src + ' unknown ' + " " + jsonOrientation);
      $("#orientation").text(src + ' unknown ' + " " + jsonOrientation);
      break; 
  }
}

$(function () {

  if ("onorientationchange" in window) 
    window.addEventListener('orientationchange', 
      function() { doOnOrientationChange("window.orientationchange"); });
  //window.addEventListener('resize', 
  //    function() { doOnOrientationChange("window.resize") });
  if (window.screen && window.screen.orientation && window.screen.orientation.addEventListener)
    window.screen.orientation.addEventListener('change', 
      function() { doOnOrientationChange("screen.orientation.change"); });

  if (window.matchMedia) {
    var mql = window.matchMedia("(orientation: portrait)");
    mql.addListener(function(m) {
      if (m.matches) {
        doOnOrientationChange("mql-portrait");
      } else {
        doOnOrientationChange("mql-landscape");
      }
    });
  }

  doOnOrientationChange("init");
});

(I put the window.resize handler into comments because it generates too may events on desktop browsers.)

In this sample code, detection change only causes output of angle and orientation type to

  • $().toastmessage() – a jQuery extension
  • console.log
  • $(“#orientation”).text() – a jQuery call

Of course, your handlers may perform some useful actions…


Migrating a TypeScript application from ASP.Net MVC to Angular2 – Lessons Learned

April 26, 2016

To evaluate Angular2, I started to convert a TypeScript application hosted in an ASP.Net MVC application to be hosted by the Angular framework.

The application continuously polls data from a web service, performs a couple of calculations, and displays the calculated results. It is a display-only auto-paging application – the only user interaction being to allow for manual paging. Parts of the display are updated by requesting a PartialView and adding it to the display using jQuery append().

Getting started is quite easy:

  • download, install, and configure Visual Studio Code
  • download and install node.js
  • create application directory and configuration files, and run npm install

Components

First, we need to translate MVC Controllers and Views into Angular2 @Component classes and templates:

A page (controller+view) typically translates into a component with an associated HTML template.

Depending on your application, a PartialView may also be represented by a component. It may also translate into a part of a template being activated by NgSwitch or NgIf.

The file app/app.component.ts defines the URL routes, with @RouteConfig being the equivalent of MVC routing configuration. Each page you can directly address by a URL in a browser needs to be added to the @RouteConfig declaration.

Angular Exceptions

During development, you will occasionally meet the error condition 

The selector “my-app” did not match any elements

Currently, the only reliable way for me to resolve the condition is to close the browser window, restart npm lite-server, and hitting ctrl-F5.

Unfortunately, this was not the only (recurring) exception generated by the Angular2 framework. True, this is still a beta version (15), but if an exception in the framework occurs, there is almost no way of debugging it, as developers are used to debug their code. There is simply too much magic happening in the background. I repeatedly had to remove code or html and re-insert it step by step to figure out which part of the code caused a framework exception. And judging from browsing through GitHub issues, the situation has improved considerably from earlier versions.

Coding and Legacy Code

If your component needs to process URL parameters, there is the built-in routeParams.get() method. There is no automatic mapping of URL parameters to model class properties, but I developed a simple TypeScript function which will provide this convenience.

Adding and referencing existing TypeScript code to an Angular2 application is straight-forward. As I wanted to keep changes to existing code to a minimum, I simply added the scripts in the main index.html page using <script> tags.

However I found that Angular2 does not render <script> elements inside HTML templates. Fortunately, this code can be moved to and thus accessed from the @Component class, either in its constructor or one of the component’s lifecycle hooks, the Angular equivalent of ASP.Net events.

Angular2 uses import to load and reference TypeScript code. To reference code loaded by a <script> tag, you need to declare external symbols, as is typical for TypeScript’s .d.ts files.

To implement calls from the legacy code into the Angular2 code, I needed to store the Angular component in a window property, and access this property to invoke a method of that component:

// myComponent.ts:
constructor() {
  (<any>window).myComponent = this;
}
methodCall() {
  ..implementation..
}
// legacy.ts
(<any>window).myComponent.methodCall();

Just a tiny change. Alternatively, every referenced declaration (and their dependencies) would have needed to be marked as export.

Comparing Architecture and TypeScript Hosting in ASP.Net MVC and Angular2

Comparing Architecture and TypeScript Hosting in ASP.Net MVC and Angular2

Dynamic Views

To dynamically render additional views, I created another component and assigned it a selector. The parameters displayed in the dynamic view are stored in a class, and the main component stores an array of this class:

export class MySubView {
  ... members ...
}

In the main component:

public subviews: MySubView[] = [];

In the main component’s HTML template:

<mysubview *ngFor="#subview of subviews" [data]="subview"></mysubview>

The sub component is declared as

@Component({
 selector: 'mysubview'
}
export class MySubViewComponent {
  @Input() data: MySubView;
}

A new mysubview item is simply created by Angular2 by adding an object to the array:

this.subviews.push(new MySubView(...parameters..));

If a component needs to dynamically switch between several available templates, things get a bit complicated, but it’s feasable.

Disabling some Angular2 features

Angular2 allows CSS styles per @Component, and when rendering a component’s CSS, adds a component-specific attribute to the HTML elements, as well as to their CSS declarations.

Since I did not want to edit my CSS (which would have certainly made the CSS declarations cleaner and more specific), I deactivated this CSS magic by declaring the component

encapsulation: ViewEncapsulation.None,

Also, I wanted to disable Angular’s data-binding mechanism, since my legacy code takes care of all data-binding, and I wanted to avoid any interference between the two.

Adding the component declaration

changeDetection: ChangeDetectionStrategy.OnPush

switches change detection and auto-updates off.

Conclusion

Starting with Angular2 certainly comes with a learning curve. Switching platforms, even though they are comparable, may be a major effort. Whereas my little sample just concerned hosting existing functionality in Angular2, adapting existing code as a “real” Angular2 application – designing and using components, CSS handling, data-binding and change detection – is probably an effort similar to writing the application from scratch.

Resources


Migrating ASP.Net MVC PartialViews to Angular2

April 24, 2016

An Angular2 @Component combines the equivalents of MVC Controller and View. This mostly also applies to MVC PartialViews.

In my application, TypeScript dynamically adds PartialViews to the page’s HTML.

Migrating the code to Angular2, the page’s component, let’s call it ParentComponent, contains an array of ChildComponents. If a new ChildComponent is added to the array, Angular2 will automatically render its template or templateURL, and add the resulting HTML inside the <childcomponent> tag

<childcomponent *ngFor="#c of childcomponents" [childdata]="c">
</childcomponent>

Your custom JavaScript code can then process the rendered component in one of the LifeCycle Hooks.

The story gets more complicated if your code needs to dynamically select which PartialView to render. Of course, there is the NgSwitch directive, which acts as the templating’s switch/case mechanism, but template code may easily get ugly and overly complex.

The alternative is to define the template or templateURL programmatically by calling loader.loadIntoLocation() of DynamicComponentLoader, but, as GitHub issues show (#7815, #6701, #7596, #2753), this is not as straight-forward as one might hope, especially if template data-binding is involved (#6223, #7453SO, SO).

The solution that I extracted from the referenced issues looks like this

constructor(private loader: DynamicComponentLoader, 
    private elementRef: ElementRef, 
    private ref: ChangeDetectorRef) {
  this.myData = ..some data..;
  this.myOtherData = ..some data..;
}

ngOnInit() {
  var templateURL = .... calculate template ...;

  this.loader.loadIntoLocation(
    this.toComponent(templateURL + '.html', null),
    this.elementRef, 'newItem'
  ).then(component => { 
    component.instance.myData = this.myData;
    component.instance.myOtherData = this.myOtherData;
    this.ref.markForCheck() 
  }); 
}

Based on SO and plnkr

toComponent(templateUrl, directives = []) {
  @Component({ selector: 'child-fake-component', 
  templateUrl: templateUrl,
  directives: [COMMON_DIRECTIVES])
  class ChildFakeComponent {}
 
  return ChildFakeComponent;
}

Note that I need ChangeDetectorRef to explicitly update the template because I deactivated change detection using ChangeDetectionStrategy.OnPush, so this need not apply to your case.

The essential part is that you need to copy all data referenced by the template to component.instance. #newItem is the anchor of the HTML element in the @Component’s template or template, and the loaded template will be attached after the original template, not replacing the original template.


Invoking Custom JavaScript Code from Angular2

April 22, 2016

I am trying to migrate an ASP.Net MVC application to Angular2. One thing I am still missing is how to invoke custom/legacy JavaScript code, as Angular2 does not render <script> tags contained in templates.

Actually we do not need <script> tags, as custom JavaScript/TypeScript can be called from the @Component!

Custom code can be invoked from various places of your component’s TypeScript code:

Logging to the console shows that code is executed in the following order

  • constructor
  • ngAfterContentInit
  • ngAfterViewInit
  • asynchronous code invoked in constructor (e.g. by http.get())

(for more details, see Angular’s documentation of Lifecycle Hooks)

Since executed code resides in the @Component, we do not need to pass JavaScript objects to the template.

Whereas in ASP.Net MVC we have a strict distinction between Controller and View, and process and data flow are from the controller to the view, and the view cannot (usually does not) reference the controller, this distinction does not apply in Angular2, where we end up in the component after the template has been rendered.


Angular2 “Does not support more than 9 expressions”

April 21, 2016

If you need to pass more than just a couple of values from your Angular2 component to the template, you’ll sooner or later run into the error message

EXCEPTION: Does not support more than 9 expressions

I needed to do just that to provide TypeScript/JavaScript objects with a huge set of values. The solution to work around this restriction is to create an object in the component which stores all these values, and then access its JSONified value

export class MyComponent {
  o: any;

  constructor() {
    var o = { .... complex JavaScript object ... };
    this.o = JSON.stringify(o);
  }
}

from the template

< script >
  var o = {{ o }};
  ....
</script >

…except that <script> tags are not rendered in Angular2 templates.

We can still store the JSONified value in an HTML attribute. But to call custom or legacy JavaScript code, we can extend the @Component.


The selector “my-app” did not match any elements

April 21, 2016

Developing my first Angular2 application (baby steps, baby steps), I edit code and, without any changes in the core files (index.html, app.ts, main.ts, etc), the application suddenly won’t start up, and the browser console displays the error message

The selector “my-app” did not match any elements

and an impressive stack dump

angular2.dev.js:23935 EXCEPTION: The selector "my-app" did not match any elements
angular2.dev.js:23925 EXCEPTION: The selector "my-app" did not match any elementsBrowserDomAdapter.logError @ angular2.dev.js:23925
angular2.dev.js:23935 EXCEPTION: Error: Uncaught (in promise): The selector "my-app" did not match any elements
angular2.dev.js:23925 EXCEPTION: Error: Uncaught (in promise): The selector "my-app" did not match any elementsBrowserDomAdapter.logError @ angular2.dev.js:23925
angular2.dev.js:23925 STACKTRACE:BrowserDomAdapter.logError @ angular2.dev.js:23925
angular2.dev.js:23925 Error: Uncaught (in promise): The selector "my-app" did not match any elements
 at resolvePromise (angular2-polyfills.js:602)
 at angular2-polyfills.js:638
 at ZoneDelegate.invokeTask (angular2-polyfills.js:423)
 at Object.NgZoneImpl.inner.inner.fork.onInvokeTask (angular2.dev.js:2118)
 at ZoneDelegate.invokeTask (angular2-polyfills.js:422)
 at Zone.runTask (angular2-polyfills.js:320)
 at drainMicroTaskQueue (angular2-polyfills.js:541)
 at XMLHttpRequest.ZoneTask.invoke (angular2-polyfills.js:493)BrowserDomAdapter.logError @ angular2.dev.js:23925
angular2-polyfills.js:528 Unhandled Promise rejection: The selector "my-app" did not match any elements ; Zone: angular ; Task: Promise.then ; Value: BaseExceptionmessage: "The selector "my-app" did not match any elements"stack: "Error: The selector "my-app" did not match any elements? at new BaseException (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:7587:21)? at DomRenderer.selectRootElement (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:13997:15)? at DebugDomRenderer.selectRootElement (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:7393:37)? at HostViewFactory.viewFactory_HostAppComponent0 [as viewFactory] (viewFactory_HostAppComponent:72:18)? at AppViewManager_.createRootHostView (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:6665:34)? at http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:12521:46? at ZoneDelegate.invoke (http://localhost:3000/node_modules/angular2/bundles/angular2-polyfills.js:390:29)? at Object.NgZoneImpl.inner.inner.fork.onInvoke (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:2126:31)? at ZoneDelegate.invoke (http://localhost:3000/node_modules/angular2/bundles/angular2-polyfills.js:389:35)? at Zone.run (http://localhost:3000/node_modules/angular2/bundles/angular2-polyfills.js:283:44)"__proto__: ErrorconsoleError @ angular2-polyfills.js:528
angular2-polyfills.js:530 Error: Uncaught (in promise): The selector "my-app" did not match any elements(…)consoleError @ angular2-polyfills.js:530
angular2-polyfills.js:528 Unhandled Promise rejection: The selector "my-app" did not match any elements ; Zone: <root> ; Task: Promise.then ; Value: BaseExceptionmessage: "The selector "my-app" did not match any elements"stack: "Error: The selector "my-app" did not match any elements? at new BaseException (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:7587:21)? at DomRenderer.selectRootElement (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:13997:15)? at DebugDomRenderer.selectRootElement (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:7393:37)? at HostViewFactory.viewFactory_HostAppComponent0 [as viewFactory] (viewFactory_HostAppComponent:72:18)? at AppViewManager_.createRootHostView (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:6665:34)? at http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:12521:46? at ZoneDelegate.invoke (http://localhost:3000/node_modules/angular2/bundles/angular2-polyfills.js:390:29)? at Object.NgZoneImpl.inner.inner.fork.onInvoke (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:2126:31)? at ZoneDelegate.invoke (http://localhost:3000/node_modules/angular2/bundles/angular2-polyfills.js:389:35)? at Zone.run (http://localhost:3000/node_modules/angular2/bundles/angular2-polyfills.js:283:44)"__proto__: ErrorconsoleError @ angular2-polyfills.js:528
angular2-polyfills.js:530 Error: Uncaught (in promise): The selector "my-app" did not match any elements(…)consoleError @ angular2-polyfills.js:530

Opening another browser instance with the same URL does not raise any errors.

Strange.

Replacing the line

 System.import('app/main')

in index.html with

 System.import('./app/main')

seems to fix this behavior – for now, at least.

Update: Seems that joy was premature.

The only way to resolve this condition as of Angular 2.0.0-beta.15 seems to be

  • close the web browser tab
  • cancel npm lite-server
  • restart lite-server using npm start
  • completely reload page in browser developer mode

Mapping URL Querystring Parameters to Parameter Model Classes in Angular2

April 20, 2016

In ASP.Net MVC we can define a parameter model class, which is automatically filled by the framework before invocation of the controller’s method implementing the action. (More precisely, the Databinder sets the values of the public properties of the object)

public class QueryStringParameters 
{
  public int Id { get; set; }
  public string Foo  { get; set; }
  public string Bar  { get; set; }
}

Angular2 does not seem to provide such a mechanism out of the box. I found 3 classes handling URL parameters, and there are probably more

  • import { RouteParams } from 'angular2/router';
  • import { URLSearchParams } from 'angular2/http';
  • UrlParser in angular2/ts/src/router/url_parser

where UrlParser cannot be imported in TypeScript code.

So I tried to figure out how to use RouteParams and URLSearchParams and, using the URL http://localhost:3000/default?id=3&foo=8&bar=4711, found these differences:

routeParams.params

routeParams.params

URLSearchParams

URLSearchParams

Whereas routeParams.params is a simple Object with propery names derived from the query string, URLSearchParams contains a Map<string, string[]> (see url_search_params.d.ts in node_modules\angular2\src\http).

I chose routeParams to continue, because I do not use multiple occurrences of URL parameters.

URL parameters are case-sensitive in Angular2 / TypeScript / JavaScript, whereas they are case-insensitive in ASP.Net MVC. To copy the query string parameter values into a model class, a method to ignore case of both the URL parameters and the model class properties is therefore necessary.

To get the names of the parameters, we can use the function Object.keys() applied on routeParams.params, and we map the lower-case version of the name to the name.

Next, we iterate through the Object.keys of the parameter model class, and assign the model class property the value given by the query string parameter if one exists:

import {RouteParams} from 'angular2/router';

export function fromRouteParams<T>(model: T, routeParams: RouteParams): T {
  var rk = [];
  Object.keys(routeParams.params).forEach(k => { rk[k.toLowerCase()] = k; });

  var mk = Object.keys(model);
  mk.forEach(
    k => {
      if (rk.indexOf[k.toLowerCase()] != -1)
      model[k] = routeParams.get(rk[k.toLowerCase()]);
  });

  return model;
}

Finally, given the TypeScript parameter model class

export class QueryStringParameters {
  public Id: number = null;
  public Foo: number = null;
  public Bar: number = null;
}

the component defined as

import {RouteParams} from 'angular2/router';
import {QueryStringParameters} from './model/QueryStringParameters';
import {fromRouteParams} from './helpers/routeParamsHelpers';
export class DefaultComponent { 
  private q: QueryStringParameters; 
  constructor(routeParams: RouteParams) {
    this.q = fromRouteParams(new QueryStringParameters(), routeParams);
  }
}

containing a simple Angular2 template

template: `Id: {{ q.Id }}, Bar: {{ q.Bar }}, Foo: {{ q.Foo }}`

we can navigate to the URL http://localhost:3000/default?id=3&foo=8&bar=4711 and view the resulting HTML:

Id: 3, Bar: 4711, Foo: 8

Note that my function does not perform type checks on the model class properties, as it just copies the original parameter string values.


Follow

Get every new post delivered to your Inbox.

Join 77 other followers