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!

0 comments: