Friday, June 7, 2013

Node.js UI Generator: Metawidget v3.4

Version 3.4 of Metawidget, the Node.js UI generator is now available!

This release was focused on:
As always, the best place to start is the Reference Documentation:

http://metawidget.org/doc/reference/en/pdf/metawidget.pdf

Your continued feedback is invaluable to us. Please download it and let us know what you think.

Metawidget and Node.js

Inspired by a recent forum post and blog the next release of Metawidget (3.4) will be available as a Node.js module. This lets you use Metawidget to perform server side UI generation using JavaScript, if that's your requirement.

To install, simply use npm:

npm install metawidget

Then use the Metawidget API as normal:

var metawidget = require( 'metawidget' );
...
var mw = new metawidget.Metawidget( element );
mw.toInspect = {
   name: "Joe Bloggs",
   "DOB": "1/1/2001"
};
mw.buildWidgets();


// Print what was rendered

console.log( element.toString() );

This will render:

<table>
   <tbody>
      <tr id="table-name-row">
         <th id="table-name-label-cell"><label for="name" id="table-name-label">Name:</label></th>
         <td id="table-name-cell"><input type="text" id="name" name="name" value="Joe Bloggs"/></td>
         <td></td>
      </tr><tr id="table-DOB-row">
         <th id="table-DOB-label-cell"><label for="DOB" id="table-DOB-label">DOB:</label></th>
         <td id="table-DOB-cell"><input type="text" id="DOB" name="DOB" value="1/1/2001"/></td>
         <td></td>
      </tr>
   </tbody>
</table>

Which of course can be customized using Metawidget's pipeline architecture.

Metawidget must be used in combination with a DOM implementation. This can either be jsdom, envjs, or even a simple implementation of your own. For example:

var simpleDocument = {
   createElement: function( elementName ) {

      return {
         nodeType: 1,
         tagName: elementName.toUpperCase(),
         attributes: [],
         childNodes: [],
         setAttribute: function( name, value ) {

            for ( var loop = 0, length = this.attributes.length; loop < length; loop++ ) {
               if ( this.attributes[loop].nodeName === name ) {
                  this.attributes[loop].nodeValue = value;
                  return;
               }
            }

            this.attributes.push( {
               nodeName: name,
               nodeValue: value
            } );
         },
         hasAttribute: function( name ) {

            for ( var loop = 0, length = this.attributes.length; loop < length; loop++ ) {
               if ( this.attributes[loop].nodeName === name ) {
                  return true;
               }
            }

            return false;
         },
         getAttribute: function( name ) {

            for ( var loop = 0, length = this.attributes.length; loop < length; loop++ ) {
               if ( this.attributes[loop].nodeName === name ) {
                  return this.attributes[loop].nodeValue;
               }
            }

            return null;
         },
         appendChild: function( childNode ) {

            this.childNodes.push( childNode );
         },
         cloneNode: function() {

            var clone = simpleDocument.createElement( elementName );

            for ( var loop = 0, length = this.attributes.length; loop < length; loop++ ) {
               var attribute = this.attributes[loop];
               clone.setAttribute( attribute.nodeName, attribute.nodeValue );
            }
            for ( var loop = 0, length = this.childNodes.length; loop < length; loop++ ) {
               clone.appendChild( this.childNodes[loop].cloneNode() );
            }
            return clone;
         },
         removeChild: function( childNode ) {

            for ( var loop = 0, length = this.childNodes.length; loop < length; loop++ ) {
               if ( this.childNodes[loop] === childNode ) {
                  this.childNodes.splice( loop, 1 );
                  return childNode;
               }
            }

            throw new Error( "childNode not found: " + childNode );
         },
         ownerDocument: this,
         toString: function() {

            var toString = "<" + elementName;

            for ( var loop = 0, length = this.attributes.length; loop < length; loop++ ) {
               var attribute = this.attributes[loop];
               toString += ' ' + attribute.nodeName + '="' + attribute.nodeValue + '"';
            }

            if ( this.value !== undefined ) {
               toString += ' value="' + this.value + '"';
            }

            toString += ">";

            for ( var loop = 0, length = this.childNodes.length; loop < length; loop++ ) {
               toString += this.childNodes[loop].toString();
            }

            if ( this.innerHTML !== undefined ) {
               toString += this.innerHTML;
            }

            toString += "</" + elementName + ">";
            return toString;
         }
      };
   },
   createTextNode: function( data ) {

      return {
         nodeType: 3,
         toString: function() {

            return data;
         }
      }
   }
};

var element = simpleDocument.createElement( 'div' );

Metawidget must be wrapped around a DOM element. The Metawidget constructor takes this element, and thereafter always uses element.ownerDocument rather than referencing any global document object.

Thursday, May 23, 2013

Metawidget meets My Digital Structure

I've been working with the guys at My Digital Structure to create a 'proof of concept' integration between Metawidget and their REST APIs.

The project uses Stylus and Uglify (via Grunt), AngularJS, Bootstrap (which needs JQuery) and Metawidget to dynamically (and generically) render entities by accessing the My Digital Structure REST APIs for both data and metadata. You can download it at https://github.com/kennardconsulting/digitalstructure.

Exploring the Proof of Concept

You should be able to open the project's index.html directly in Firefox. For Chrome (which blocks local AJAX requests) you'll need to open it via HTTP. You'll then need to login using your My Digital Structure login:

This will take you to a home screen showing those My Digital Structure entities that have been upgraded to their new /rpc REST API:

Clicking on any home entity will take you to a search screen. The table on the search screen is rendered dynamically by Metawidget, based on the data returned by the REST API:

Clicking on any item will take you to a read-only screen. This screen is constructed from the REST metadata APIs, so that: the fields appear in their correct order; fields with no data still appear; fields that are drop downs have their values mapped to human-readable values (e.g. value 4 is mapped to 'Dr' in the screenshot):

Clicking Edit will take you to an edit screen where you can Save or Delete the entity. This screen includes drop downs. The values in the drop downs have been further sourced by secondary REST metadata calls.

Finally, the process is repeated for other My Digital Structure entities. The project uses the same two search.html and crud.html screens, just powered by different metadata. This saves enormous amounts of code:

My thanks to the My Digital Structure team for all their support!

Monday, April 22, 2013

Metawidget and Backbone Forms

I recently read a Google+ comment comparing AngularJS Metawidget to the excellent Backbone Forms library. The two approaches look surprisingly similar, so I thought it might be beneficial to start a dialogue between our respective teams.

So, here's the Backbone Forms Usage Example mapped (roughly) into AngularJS Metawidget:

<html>
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular.min.js"></script>
      <script src="http://metawidget.org/js/3.4/metawidget-core.min.js"></script>
      <script src="http://metawidget.org/js/3.4/metawidget-angular.min.js"></script>
      <script>
         angular.module( 'myApp', [ 'metawidget' ] );

         function UserController( $scope ) {

            $scope.user = {
               title: "Mr",
               name: "John Doe",
               email: "",
               birthday: new Date( 1, 1, 1970 ),
               password: "",
               address: {
                  street: "123 Sample Street",
                  city: "Sampleville",
                  postcode: "1234"
               },
               notes: ""
            };

         }
      </script>
   </head>
   <body ng-app="myApp" ng-controller="UserController">
      <metawidget ng-model="user"/>
   </body>
</html>

Backbone Forms further supports schemas to refine your UI. Metawidget does something similar using the JSON Schema specification:

<html>
   <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular.min.js"></script>
      <script src="http://metawidget.org/js/3.4/metawidget-core.min.js"></script>
      <script src="http://metawidget.org/js/3.4/metawidget-angular.min.js"></script>
      <script>
         angular.module( 'myApp', [ 'metawidget' ] );

         function UserController( $scope ) {

            $scope.user = {};

            $scope.config = {
               inspector: new metawidget.inspector.JsonSchemaInspector( {
                        properties: {
                           title: { type: 'string', enum: ['Mr', 'Mrs', 'Ms'] },
                           name: { type: 'string' },
                           email: { type: 'string', required: true },
                           birthday: { type: 'date' },
                           password: { type: 'string', masked: true },
                           address: {
                              properties: {
                                 street: { type: 'string' },
                                 city: { type: 'string' },
                                 postcode: { type: 'string' }
                              }
                           },
                           notes: { type: 'string', large: true }
                        }
                     } )

            }
         }
      </script>
   </head>
   <body ng-app="myApp" ng-controller="UserController">
      <metawidget ng-model="user" config="config"/>
   </body>
</html>

I think our two approaches have a lot of common. But it'd be interesting to explore our differences and see if we can converge further. For example, I'd be interested in whether Backbone Forms has (either implicit or explicit) equivalents to the five stages of Metawidget's pipeline. Or whether Metawidget's API can be simplified for JavaScript users.

Monday, April 15, 2013

JSON Schema UI Generator: Metawidget v3.3

Version 3.3 of Metawidget, the JSON Schema UI generator is now available!

This release was focused on:
This release contains breaking changes. We apologise for the disruption and provide this Metawidget 3.2 to 3.3 Migration Guide.

As always, the best place to start is the Reference Documentation:


http://metawidget.org/doc/reference/en/pdf/metawidget.pdf


Your continued feedback is invaluable to us. Please download it and let us know what you think.

Tuesday, April 2, 2013

Metawidget 3.2 to 3.3 Migration Guide

The next release of Metawidget (v3.3) contains some breaking changes. We apologise for the disruption and provide this Migration Guide to help in moving from v3.2 to v3.3. All of the existing documentation and examples have already been migrated, as a reference point.

Migrate JavaScript Metawidgets to JSON Schema

The three existing JavaScript Metawidgets (AngularJS, JQueryUI and pure JavaScript) have migrated to using JSON Schema internally. This change was made because:

  • JSON Schema is a lightweight, extensible standard that fits nicely with Metawidget
  • Metawidget's proprietary, internal format was already close to JSON Schema
  • adopting a standard has knock-on benefits - not least interoperability and documentation

Although beneficial, this migration has affected some code. First, the format returned by the existing Inspector and InspectionResultProcessor interfaces used to be an array of objects. For example:

[ { "name": "root", "_root": "true" }, { "name": "prop1", "required": "true" }, { "name": "prop2", "large": "true" } ]

Under JSON Schema this becomes an object with a properties sub-object:

{ "name": "root", properties: { "prop1": { "required": "true" }, "prop2": { "large": "true" } } }

Second, an additional parameter elementName has been added to the WidgetBuilder, WidgetProcessor and Layout interfaces:

function buildWidget( elementName, attributes, mw ) { ... }
function processWidget( widget, elementName, attributes, mw ) { ... }
function layout( widget, elementName, attributes, mw ) { ... }

This parameter can take the values entity, property or action and helps distinguish 'root' properties from child properties. It is also more consistent with the Java-based Metawidget. Third, some metadata names have been migrated to JSON Schema:

label -> title
lookup -> enum
lookupLabels -> enumTitles
minimumValue -> minimum
maximumValue -> maximum
minimumLength -> minLength
maximumLength -> maxLength

Finally, metawidget.util.combineInspectionResults no longer returns anything (it just modifies the parameters you give it).

Migrate to Vaadin 7

The Vaadin 7 API breaks compatibilty with previous versions. We have moved the existing VaadinMetawidget to a different artifactId metawidget-vaadin6 and will be starting work on a Vaadin 7-compatible Metawidget for future releases.

Feedback welcome!

Monday, April 1, 2013

Java-based JSON and JSON Schema User Interface (UI) generator

In a previous post I talked about using Metawidget on the client-side to generate UIs from JavaScript objects. I was asked if it could do the same thing in Java environments.

The answer is yes! The next release of Metawidget (3.3) will be able to inspect both JSON and JSON Schema files for useful metadata. Here's a sample using SWT:

public static void main( String[] args ) {

   // Metawidget

   Display display = new Display();
   Shell shell = new Shell( display );
   shell.setText( "JSON Viewer" );
   shell.setLayout( new FillLayout() );

   String json = "{ \"firstname\": \"Richard\", \"surname\": \"Kennard\", \"notes\": \"Software developer\" }";
   String jsonSchema = "{ properties: { \"firstname\": { \"required\": true }, \"notes\": { \"large\": true }}}";


   final SwtMetawidget metawidget = new SwtMetawidget( shell, SWT.None );
   metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
         new JsonInspector( new JsonInspectorConfig().setInputStream( new ByteArrayInputStream( json.getBytes() ) ) ),
         new JsonSchemaInspector( new JsonInspectorConfig().setInputStream( new ByteArrayInputStream( jsonSchema.getBytes() ) ) ) )
) );
   metawidget.setInspectionPath( "personObject" );

   // Shell

   shell.setVisible( true );
   shell.open();

   while ( !shell.isDisposed() ) {
      if ( !display.readAndDispatch() ) {
         display.sleep();
      }
   }

   display.dispose();
}

This will produce the screen below:

Binding the JSON values into the UI will depend on your particular requirements. Here's an example that binds into a Google GSON object:

public class JsonBindingProcessor
   implements AdvancedWidgetProcessor<Control, SwtMetawidget> {

   //
   // Public methods
   //

   public void onStartBuild( SwtMetawidget metawidget ) {

      getWrittenComponents( metawidget ).clear();
   }

   /**
    * Retrieve the values from the JSON and put them in the Controls.
    */

   public Control processWidget( Control control, String elementName, Map<String, String> attributes, SwtMetawidget metawidget ) {

      String attributeName = attributes.get( NAME );
      getWrittenComponents( metawidget ).put( attributeName, control );

      // Fetch the value...

      JsonObject jsonObject = metawidget.getToInspect();
      String value = jsonObject.get( attributeName ).getAsString();

      // ...and apply it to the component. For simplicity, we won't worry about converters

      String controlProperty = metawidget.getValueProperty( control );
      ClassUtils.setProperty( control, controlProperty, value );

      return control;
   }

   public void onEndBuild( SwtMetawidget metawidget ) {

      // Do nothing
   }

   /**
    * Store the values from the Controls back into the JSON.
    */

   public void save( SwtMetawidget metawidget ) {

      JsonObject jsonObject = metawidget.getToInspect();

      for ( Map.Entry<String, Control> entry : getWrittenComponents( metawidget ).entrySet() ) {

         Control control = entry.getValue();
         String controlProperty = metawidget.getValueProperty( control );
         Object value = ClassUtils.getProperty( control, controlProperty );

         jsonObject.addProperty( entry.getKey(), (String) value );
      }
   }

   //
   // Private methods
   //

   /**
    * During load-time we keep track of all the controls. At save-time we write them all back
    * again.
    */

   private Map<String, Control> getWrittenComponents( SwtMetawidget metawidget ) {

      @SuppressWarnings( "unchecked" )
      Map<String, Control> writtenComponents = (Map<String, Control>) metawidget.getData( JsonBindingProcessor.class.getName() );

      if ( writtenComponents == null ) {
         writtenComponents = CollectionUtils.newHashMap();
         metawidget.setData( JsonBindingProcessor.class.getName(), writtenComponents );
      }

      return writtenComponents;
   }
}

To add it into the example above:

metawidget.setInspectionPath( "personObject" );

metawidget.addWidgetProcessor( new JsonBindingProcessor() );
metawidget.setToInspect( new JsonParser().parse( json ) );

Button button = new Button( metawidget, SWT.NONE );
button.setText( "Save" );

button.addSelectionListener( new SelectionAdapter() {

   @Override
   public void widgetSelected( SelectionEvent e ) {

      metawidget.getWidgetProcessor( JsonBindingProcessor.class ).save( metawidget );
      System.out.println( metawidget.getToInspect().toString() );
   }
} );


// Shell

shell.setVisible( true );

Feedback welcome!