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!

7 comments:

Davor said...

Hi Richard, this example is awesome! If you would be some kind to add data:"array" parameter in example for checkbox option, I was breakign my head viewing example and not getting it :) But I figure it out :)

I have one more question regarding enum, is there possible to "split" key and value? I tried making something like this

enum: [ {'mr':'Mister'},{'mrs':'Mistress'}] etc..
my idea is to split value and select/radio/check label
but i get [Object object] rendered, so I guess I'm doing it wrong :)

thank you again!
davor

Richard said...

Davor,

Thanks for your kind words.

You shouldn't need to add type: array in the checkbox example. The use of PropertyTypeInspector (within CompositeInspector) should detect the type for you?

With regards to enum, you are almost right! Try enum: [ 'mr', 'mrs' ] with enumTitles: [ 'Mister', 'Mistress' ]

Shahana Shafiuddin said...

Very helpful

Neeraj said...

Hi Richard,

What if I have a complex type in my array, like :

"Types": {
"id": "Types",
"type": "array",
"enumTitles" :[
"PAY_CODES",
"PAY_FROM_SCHEDULE",
"PUNCHES",
"SHIFTS"
],
"enum" : [
{
"WSACommentType": {
"Name": "PAY_CODES"
}
},
{
"WSACommentType": {
"Name": "PAY_FROM_SCHEDULE"
}
},

{
"WSACommentType": {
"Name": "PUNCHES"
}
},
{
"WSACommentType": {
"Name": "SHIFTS"
}
}
],
"properties": {
"WSACommentType": {
"title":null,
"id": "WSACommentType",
"type":"object",
"properties":{
"name":{
"type":"string"
}
}
}
}
},

above Json generate checkboxes with label given in enumTitles but the value in checkbox is '[object object]' which result in selection of all checkboxes on click of anyone

Richard said...

Yes, this is not currently supported. I don't think it's really supported by Angular either?

If you could send an example of how to do it in Angular, I can look at making Metawidget generate the same code.

Anonymous said...

Richard,

Why is the log in console happens twice.. all the model data is logging twice when we click save. Is it default behaviour, or will it replicate the data on every click..!

Richard said...

This blog entry is using a very old version of Metawidget (and Angular). If you use the latest versions this bug is fixed.