Thursday, September 5, 2013

AngularJS UI Generator: custom widgets

I've been asked to blog about "select boxes versus radio buttons versus checkboxes" in Metawidget. There are two answers:

Answer #1: WidgetBuilders

UIs often have exacting requirements about which widgets to use. Metawidget is designed for this. It has a pluggable WidgetBuilder architecture allowing you to plug-in (and chain together) custom WidgetBuilders to handle just your custom case, and fall back to built-in WidgetBuilders for more general cases. So if you find yourself needing a particular custom widget: write a little WidgetBuilder.

Here's an example:

<html ng-app="myApp">
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-core.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-angular.min.js" type="text/javascript"></script>
      <script type="text/javascript">
         angular.module( 'myApp', [ 'metawidget' ] )
            .controller( 'myController', function( $scope ) {
               $scope.metawidgetConfig = {
                  widgetBuilder: new metawidget.widgetbuilder.CompositeWidgetBuilder( [
                     function( elementName, attributes, mw ) {
                        if ( attributes.name === 'hobby' ) {
                           var select = document.createElement( 'select' );
                           select.innerHTML = '<option/><option>Beer</option><option>TV</option><option>Eating</option>';
                           return select;
                        }
                     },

                     new metawidget.widgetbuilder.HtmlWidgetBuilder()
                  ] )
               };
               $scope.person = {
                  firstname: 'Homer',
                  surname: 'Simpson',
                  age: 36,
                  hobby: 'Beer'
               };
               $scope.save = function() {
                  console.log( $scope.person );
               }
            } );
      </script>
      <style>
         #metawidget {
            border: 1px solid #cccccc;
            width: 250px;
            border-radius: 10px;
            padding: 10px;
            margin: 50px auto;
            display: block;
         }
         #metawidget button {
            display: block;
            margin: 10px auto 0px;
         }
      </style>
   </head>
   <body ng-controller="myController">
      <metawidget id="metawidget" ng-model="person" config="metawidgetConfig">
         <button ng-click="save()">Save</button>
      </metawidget>
   </body>
</html>

Note that Metawidget's WidgetProcessors (in this case AngularWidgetProcessor) are still able to automatically data-bind your custom widget. And your widget choice can key off any piece of metadata, not just attributes.name.

Answer #2: The Default

Having said that, the built-in WidgetBuilders do a decent job. You may find them sufficient for your use case. Here's an example of using JSON Schema metadata to create the same dropdown:

<html ng-app="myApp">
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-core.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-angular.min.js" type="text/javascript"></script>
      <script type="text/javascript">
         angular.module( 'myApp', [ 'metawidget' ] )
            .controller( 'myController', function( $scope ) {
               $scope.metawidgetConfig = {
                  inspector: new metawidget.inspector.CompositeInspector( [
                     new metawidget.inspector.PropertyTypeInspector(),
                     new metawidget.inspector.JsonSchemaInspector( {
                        properties: {
                           hobby: {
                              enum: [ 'Beer', 'TV', 'Eating' ]
                           }
                        }

                     } )
                  ] )
               };
               $scope.person = {
                  firstname: 'Homer',
                  surname: 'Simpson',
                  age: 36,
                  hobby: 'Beer'
               };
               $scope.save = function() {
                  console.log( $scope.person );
               }
            } );
      </script>
      <style>
         #metawidget {
            border: 1px solid #cccccc;
            width: 250px;
            border-radius: 10px;
            padding: 10px;
            margin: 50px auto;
            display: block;
         }
         #metawidget button {
            display: block;
            margin: 10px auto 0px;
         }
      </style>
   </head>
   <body ng-controller="myController">
      <metawidget id="metawidget" ng-model="person" config="metawidgetConfig">
         <button ng-click="save()">Save</button>
      </metawidget>
   </body>
</html>

Widget choice is often dictated by data type. If you want checkboxes instead of a dropdown, then your data type needs to be an array rather than a string, so that the user can select multiple values:

<html ng-app="myApp">
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-core.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-angular.min.js" type="text/javascript"></script>
      <script type="text/javascript">
         angular.module( 'myApp', [ 'metawidget' ] )
            .controller( 'myController', function( $scope ) {
               $scope.metawidgetConfig = {
                  inspector: new metawidget.inspector.CompositeInspector( [
                     new metawidget.inspector.PropertyTypeInspector(),
                     new metawidget.inspector.JsonSchemaInspector( {
                        properties: {
                           hobby: {
                              enum: [ 'Beer', 'TV', 'Eating' ]
                           }
                     }
                  } )
                  ] )
               };
               $scope.person = {
                  firstname: 'Homer',
                  surname: 'Simpson',
                  age: 36,
                  hobby: [ 'Beer' ]
               };
               $scope.save = function() {
                  console.log( $scope.person );
               }
            } );
      </script>
      <style>
         #metawidget {
            border: 1px solid #cccccc;
            width: 250px;
            border-radius: 10px;
            padding: 10px;
            margin: 50px auto;
            display: block;
         }
         #metawidget button {
            display: block;
            margin: 10px auto 0px;
         }
      </style>
   </head>
   <body ng-controller="myController">
      <metawidget id="metawidget" ng-model="person" config="metawidgetConfig">
         <button ng-click="save()">Save</button>
      </metawidget>
   </body>
</html>

Sometimes widget choice purely is aesthetic, though. If you want radio buttons instead of a select box, the built-in WidgetBuilder recognises a componentType metadata:

<html ng-app="myApp">
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-core.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.5/metawidget-angular.min.js" type="text/javascript"></script>
      <script type="text/javascript">
         angular.module( 'myApp', [ 'metawidget' ] )
            .controller( 'myController', function( $scope ) {
               $scope.metawidgetConfig = {
                  inspector: new metawidget.inspector.CompositeInspector( [
                     new metawidget.inspector.PropertyTypeInspector(),
                     new metawidget.inspector.JsonSchemaInspector( {
                     properties: {
                        hobby: {
                              enum: [ 'Beer', 'TV', 'Eating' ],
                              componentType: 'radio'
                           }
                        }
                     } )
                  ] )
               };
               $scope.person = {
                  firstname: 'Homer',
                  surname: 'Simpson',
                  age: 36,
                  hobby: 'Beer'
               };
               $scope.save = function() {
                  console.log( $scope.person );
               }
            } );
      </script>
      <style>
         #metawidget {
            border: 1px solid #cccccc;
            width: 250px;
            border-radius: 10px;
            padding: 10px;
            margin: 50px auto;
            display: block;
         }
         #metawidget button {
            display: block;
            margin: 10px auto 0px;
         }
      </style>
   </head>
   <body ng-controller="myController">
      <metawidget id="metawidget" ng-model="person" config="metawidgetConfig">
         <button ng-click="save()">Save</button>
      </metawidget>
   </body>
</html>

Feedback welcome!