Monday, May 26, 2014

"Hey Bro, I heard you liked Dynamic UIs, so I put a Dynamic UI inside your Dynamic UI"

I was recently asked:

What is the best approach to implement conditional widgets in Javascript, i.e., to selectively show/hide widgets based on other widget selection(s)?

As discussed, Metawidget tries hard not to 'dictate' anything about your architecture. Therefore the 'best' approach depends on your needs. However here are two broad solutions:

1. Have Metawidget Do All The Work

This approach is the most flexible. Basically:

  • First, define an Inspector or InspectionResultProcessor that knows how to return different results based on different conditions. For example, Angular Metawidget has an InspectionResultProcessor that can process Angular expressions
  • Next, define a WidgetProcessor that knows how to kick off a complete Metawidget re-inspection based on some change

Combining these two gives you some awesome capabilites. Here's a complete example:

<html ng-app="myApp">
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.8/metawidget-core.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.8/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.JsonSchemaInspector( {
               properties: {
                  type: {
                     enum: [ 'Car', 'Truck', 'Aeroplane' ]
                  },
                  model: {
                     enum: [ 'Saloon', '4WD', 'Sports' ],
                     hidden: '{{vehicle.type != "Car"}}'
                  },
                  description: {
                     type: 'string',
                     large: true,
                     hidden: '{{vehicle.type != "Truck"}}'
                  },
                  details: {
                     hidden: '{{vehicle.type != "Aeroplane"}}',
                     properties: {
                        wingspan: {
                           type: 'number'
                        },
                        speed: {
                           type: 'number'
                        }
                     }
                  }
               }
            } )
         }
         $scope.vehicle = {};
      } );
   </script>
      <style>
         #metawidget {
            border: 1px solid #cccccc;
            width: 300px;
            border-radius: 10px;
            padding: 10px;
            margin: 50px auto;
            display: block;
         }
      </style>
   </head>
   <body ng-controller="myController">
      <metawidget id="metawidget" ng-model="vehicle" config="metawidgetConfig" />
   </body>
</html>

This UI will show/hide multiple widgets depending on the select box, and it's all defined declaratively!

2. Leverage Standard DOM Manipulation

This approach is less flexible, but also has less 'magic' in how it works. Basically:

  • First, add metadata to denote when widgets are conditional
  • Next, define a WidgetProcessor to attach standard DOM manipulation to those widgets. Metawidget helps by generating reliable IDs for everything

Here's a complete example using pure JavaScript:

<!DOCTYPE HTML>
<html>
   <head>
      <script src="http://metawidget.org/js/3.8/metawidget-core.min.js"></script>
      <style>
         #metawidget {
            border: 1px solid #cccccc;
            width: 250px;
            border-radius: 10px;
            padding: 10px;
            margin: 50px auto;
         }
         #table-model-row {
            visibility: hidden;
         }
      </style>
   </head>
   <body>
      <div id="metawidget"></div>
      <script type="text/javascript">
         var mw = new metawidget.Metawidget( document.getElementById( 'metawidget' ), {
         inspector: new metawidget.inspector.JsonSchemaInspector( {
               properties: {
                  make: {
                     enum: [ 'BMW', 'Ford', 'Toytota' ],
                     onSelect: 'model'
                  },
                  model: {
                     enum: [ 'Saloon', '4WD', 'Sports' ]
                  }
               }
            } ),
            addWidgetProcessors: [
               function( widget, elementName, attributes, mw ) {

                  if ( attributes.onSelect !== undefined ) {
                     widget.setAttribute( 'onchange', 'document.getElementById("table-' + attributes.onSelect + '-row").style.visibility="visible"' );
                  }


                  return widget;
               }
            ]
         } );
         mw.toInspect = {};
         mw.buildWidgets();
      </script>
   </body>
</html>

Feedback welcome!

Thursday, May 22, 2014

AngularJS: Changing the date format of dynamically generated components

I was recently asked:

"...is a better approach to set a different date formatting, besides from hacking it in metawidget-angular.js"

The answer is yes! Metawidget is designed around a five stage pluggable pipeline, which offers well-researched extension points to accomodate most UI requirements.

The easiest approach to this particular problem is to add a little WidgetProcessor to tweak Metawidget's standard date formatting (if you want to introduce a whole new control instead, better to go with a WidgetBuilder, as shown here). Here's a complete example:

<html ng-app="myApp">
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.8/metawidget-core.min.js" type="text/javascript"></script>
      <script src="http://metawidget.org/js/3.8/metawidget-angular.min.js" type="text/javascript"></script>
      <script type="text/javascript">
         angular.module( 'myApp', [ 'metawidget' ] ).controller( 'myController', function( $scope ) {
   
         $scope.metawidgetConfig = {
            addWidgetProcessors: [ function( widget, elementName, attributes, mw ) {
   
               if ( attributes.type === 'date' ) {
                  widget.setAttribute( 'ng-bind', widget.getAttribute( 'ng-bind' ) + ":'d MMMM yyyy'" );
               }
               
               return widget;
            } ]

         };
         $scope.person = {
            firstname: 'Homer',
            surname: 'Simpson',
            date: new Date( 1953, 5, 12 )
         };
      } );
   </script>
      <style>
         #metawidget {
            border: 1px solid #cccccc;
            width: 250px;
            border-radius: 10px;
            padding: 10px;
            margin: 50px auto;
            display: block;
         }
      </style>
   </head>
   <body ng-controller="myController">
      <metawidget id="metawidget" ng-model="person" read-only="true" config="metawidgetConfig" />
   </body>
</html>

Feedback welcome!

Thursday, May 15, 2014

AngularJS: Create editable tables with dynamic columns (Part 3)

Further to my previous post, and as mentioned in Part 1, there are lots of different UI approaches to creating editable tables.

For example: a table where you click an edit button next to each row to open a pop-up window; a table where you click the row itself to open a pop-up window; a table where you click each row and edit it 'in place'; a table where all rows are editable at once; a table where each row has both an edit button and a delete button; etc. etc. I'm sure you can think of plenty more. All of them are valid choices depending on your UI design.

It's because of this that Metawidget doesn't try to 'own' your UI. Metawidget only strives to be a piece of your overall solution: a useful widget you can sprinkle liberally all over your UI, wherever you need UI generation, but nowhere that you don't.

The downside of this flexibility is that you have to do a bit more work to wire everything together. In this post I'm going to implement one of the above approaches: a table where all rows are editable at once. It's very similar to my previous post. This time, instead of the editable table instantiating a plain piece of HTML, it instantiates a Metawidget for every row and every column:

var columnMetawidget = $( '<metawidget ng-model="row.' + columns[loop] + '" config="metawidgetConfig">' );
tr1.append( $( '<td>' ).append( columnMetawidget ) );

This works because Metawidget is very lightweight. All parts of the metawidgetConfig are immutable, so the same objects are reused for every Metawidget in the table. The result looks like this:

You can download a complete example here. Feedback welcome!

UPDATE: the built-in metawidget.widgetbuilder.HtmlWidgetBuilder now supports alwaysUseNestedMetawidgetInTables which may be useful in these cases

Wednesday, May 14, 2014

AngularJS: Create editable tables with dynamic columns (Part 2)

Following on from my previous post, I was asked:

"[In your post the] table columns are rendered using html input control with type="text"... [Can the user] add/edit new object in accordance with json schema (checkbox for boolean, dropdown for enum,...)?"

This is where Metawidget really shines. Metawidget is designed to be lightweight and embedded multiple times on a page - wherever you need small pieces of UI generation. So we can easily use a Metawidget at the foot of each column:

The implementation is almost the same as before, except instead of instantiating an <input type="text">, the editable table instantiates another <metawidget> tag:

var nestedMetawidget = $( '<metawidget ng-model="newRow.' + columns[loop] + '" config="metawidgetConfig">' );
tr2.append( $( '<td>' ).append( nestedMetawidget ) );

The hardest part is passing the row schema metadata through to the editable table:

$scope[rowSchemaKey] = inspectionResult;
var widget = $( '<table>' ).attr( 'edit-table', '' ).attr( 'schema', rowSchemaKey );

You can download a complete example here. You can also take this approach further. Feedback welcome!

Tuesday, May 13, 2014

AngularJS: Create editable tables with dynamic columns

I was recently asked:

"[how do I] implement functionality for new/delete for arrays in AngularJS... [when] object structure is unknown until runtime execution."

There are lots of ways to do this using Metawidget, which is why Metawidget doesn't dictate one out-of-the-box. By default, AngularJS Metawidget will only render arrays as read-only:

Adding editing capabilities depends on your particular UI needs. Here's one approach. Essentially we:

  • Create an Angular directive to render a simple <table> with editable rows. This directive is not specific to Metawidget
  • Create a Metawidget WidgetBuilder that can instantiate and configure the editable table. This includes reading metadata at runtime to determine the table's columns

So you'll end up with something like this:

The important Metawidget bit looks like this:

$scope.metawidgetConfig = {

   ...custom WidgetBuilder...
   
   widgetBuilder: new metawidget.widgetbuilder.CompositeWidgetBuilder( [ function( elementName, attributes, mw ) {

      if ( attributes.type === 'array' ) {

         ...inspects array metadata...
         
         var inspectionResult = mw.inspect( mw.toInspect, typeAndNames.type, typeAndNames.names );
         
         ...creates editable table

         var widget = $( '<table>' ).attr( 'edit-table', '' )
               .attr( 'columns', columns ).attr( 'ng-model', mw.path + '.' + attributes.name );
         return widget[0];
      }

You can download a complete project here. Feedback welcome!