Dart, Web UI + js libraries, Google+ API

Today’s blog post is about a sample application that uses Web UI and js libraries and shows some information about you on Google+. To retrieve these informations, application uses the Google APIs Client Library for JavaScript.

Funny story: I chose to use Google API javascript client (handled with dart js-interop library) instead of dart client since the latter was outdated and while I was editing the code an interesting project appeared on GitHub: dart-google-oauth2-library by +Gerwin Sturm. dart-google-oauth2-library simplifies the whole authentication process in a significant way, so please consider the following just as an example of how js-interop works.

The application

The applicaton is very simple: once you have authenticated and the application is allowed to access your Google+ profile data, some information will be displayed: name, profile picture, tagline, “about me” and links.


We need the web_ui library because application uses Model-driven Views (MDV) to bind data and HTML interface, and js-interop library to handle the Google APIs client, so the pubspec.yaml file will be:

name:  dart_mdv_googleplus
description:  A sample application
dependencies:
  js: any
  web_ui: any

We also need a compiler to build our MDV template, let’s call it build.dart:

import 'package:web_ui/component_build.dart';
import 'dart:io';

void main() {
  build(new Options().arguments, ['web/google_plus.html']);
}

The DART code

Code highlights

First of all, we have to import some libraries:

import 'dart:json';
import 'package:js/js.dart' as js;
import 'package:web_ui/watcher.dart' as watchers;
import 'package:web_ui/safe_html.dart';

watchers library contains declaration of dispatch() method, we need it to update HTML interface values with the current Dart values. We’ll soon see why we are importing safe_html.

Let’s go on:

final String CLIENT_ID = '688X10452XXX.apps.googleusercontent.com';
final String SCOPE = 'https://www.googleapis.com/auth/plus.me';

CLIENT_ID and SCOPE are required to gain application access, here’s more on how Using OAuth 2.0 to Access Google APIs.

Now see how Javascript functions are handled by Dart js library:

js.scoped((){
js.context.init = new js.Callback.once((){
  js.context.window.setTimeout(js.context.auth, 1);
  });

js.context.auth = new js.Callback.many((){
  js.context.gapi.client.setApiKey('AIzaSyDOtMNdtbw17o9bs-kW7G6O3S05p0H8wYM');
  js.context.window.setTimeout(
    js.context.gapi.auth.authorize(js.map({
    'client_id':CLIENT_ID,
    'scope':SCOPE,
    'immediate':immediate
    }),
    js.context.onAuthResponse), 1
  );
});

js.context.onAuthResponse = new js.Callback.many((js.Proxy token){
  if(token!=null){
    authBtn.style.display='none';
    js.context.MakeRequest();
    immediate = true;
  }
  else{
    authBtn.style.display='block';
    immediate = false;
  }
});

You see that:

  1. all the code is wrapped into the js.scoped() method
  2. there are two kinds of Callback, once and many

This aims to prevent memory leaks. When you run code inside a local scope, all contents created whithin that scope are released and garbage collected after code exits that scope; likewise, you can use Callback’s once or many method according to how many times your callback function has to be executed: if you’re using once, after the first execution the callback will be disposed. More about Using JavaScript from Dart.

js.context.RequestCallback = new js.Callback.once((js.Proxy jsonResp, var rawResp){
  var data = JSON.parse(rawResp);

  for( var url in data[0]['result']['urls']){
    urls.add(url['value']);
  }
  displayName = data[0]['result']['displayName'];
  tagline = data[0]['result']['tagline'];
  pic = data[0]['result']['image']['url'];
  aboutMe = new SafeHtml.unsafe('<div>''${data[0]['result']['aboutMe']}</div>');

  watchers.dispatch();

});

RequestCallback is the function that is invoked when application receives MakeRequest response. Recieved data, rawResp, are parsed and converted to JSON, this will ease the process of assigning values to variables. There’s nothing special here but this line:

aboutMe = new SafeHtml.unsafe('<div>''${data[0]['result']['aboutMe']}</div>');

Returned value of ‘aboutMe’ is actually a piece of HTML code; allowing an unknown piece of HTML that comes from the outside to be included in your app would represent a security flaw, so HTML is passed as regular text by default, e.g. “<span>hello <a href=’/world’>world<a></span>”. If we’re 100% sure about safety of HTML code, this behaviour can be overridden by the unsafe method of SafeHtml class; since only one top-level element is allowed to be passed as argument of unsafe() we can just wrap the code inside a div element.

Full code

import 'dart:html';
import 'dart:json';
import 'package:js/js.dart' as js;
import 'package:web_ui/watcher.dart' as watchers;
import 'package:web_ui/safe_html.dart';

final String CLIENT_ID = '688110452481.apps.googleusercontent.com';
final String SCOPE = 'https://www.googleapis.com/auth/plus.me';

ButtonElement authBtn = query('#authorize');

bool immediate = true;

String displayName = 'Me';
String tagline;
SafeHtml aboutMe;
String pic;

List<String> urls = [];

main(){

  js.scoped((){
    js.context.init = new js.Callback.once((){
      js.context.window.setTimeout(js.context.auth, 1);
      });

    js.context.auth = new js.Callback.many((){
      js.context.gapi.client.setApiKey('AIzaSyDOtMNdtbw17o9bs-kW7G6O3S05p0H8wYM');
      js.context.window.setTimeout(
        js.context.gapi.auth.authorize(js.map({
        'client_id':CLIENT_ID,
        'scope':SCOPE,
        'immediate':immediate
        }),
        js.context.onAuthResponse), 1
      );
    });


    js.context.onAuthResponse = new js.Callback.many((js.Proxy token){
      if(token!=null){
        authBtn.style.display='none';
        js.context.MakeRequest();
        immediate = true;
      }
      else{
        authBtn.style.display='block';
        immediate = false;
      }
    });

    js.context.MakeRequest = new js.Callback.once((){
      js.scoped((){
        js.context.gapi.client.load('plus', 'v1', new js.Callback.once((){
          var request = js.context.gapi.client.plus.people.get(
              js.map({
                'userId':'me',
                'fields':'aboutMe,circledByCount,displayName,image,tagline,urls/value'
              }));
          request.execute(js.context.RequestCallback);
        }));
      });
    });
    js.context.RequestCallback = new js.Callback.once((js.Proxy jsonResp, var rawResp){
      var data = JSON.parse(rawResp);

      for( var url in data[0]['result']['urls']){
        urls.add(url['value']);
      }
      displayName = data[0]['result']['displayName'];
      tagline = data[0]['result']['tagline'];
      pic = data[0]['result']['image']['url'];
      aboutMe = new SafeHtml.unsafe('<div>''${data[0]['result']['aboutMe']}</div>');

      watchers.dispatch();

    });
  });

  authBtn.on.click.add((Event e){
      js.scoped((){
        js.context.auth();
        });
      });

  ScriptElement script = new ScriptElement();
  script.src = "http://apis.google.com/js/client.js?onload=init";
  script.type = "text/javascript";
  document.body.children.add(script);
}

The HTML interface

The app interface is really simple:

<p><b>{{ displayName }} on Google+</b></p>

<template instantiate="if tagline != null">
  <p><em>{{ tagline }}</em></p>
</template>

<button id="authorize">Authorize</button>

<template instantiate="if pic != null">
    <img id="pic" src="{{ pic }}"/>
  </template>

  <template instantiate="if aboutMe != null">
    <div id="about">{{ aboutMe }}</div>
  </template>


<template instantiate="if urls.length != 0 ">
  <ul>
    <template iterate="url in urls">
      <li><a href="{{ url }}">{{ url }}</a></li>
    </template>
  </ul>
</template>

Elements wrapped in curly brackets, e.g. , are MDV placeholders for data.

If a user had not set, on his Google+ profile, one of the fields, API returns null data; to avoid showing “null”, we can decide to instatiate the element only if value of field is not equal to “null”:

<template instantiate="if aboutMe != null">

Last interesting thing to show is how web_ui handles Dart lists:

<template iterate="url in urls">
  <li><a href="{{ url }}">{{ url }}</a></li>
</template>

You can intuitively read it as: “for each url in urls do something with url”.

Demo and source code

You can play with it following this link (javascript version): dart-mdv-googleplus

or get the code at github

Something to add?

Be sure to let me know! I am @daw985 on Twitter.