Sunday, July 28, 2013

Troubleshooting JSON UI Generation: nested properties

I was asked why the following piece of Metawidget code returns an unusual result:

<!DOCTYPE html>
<html>
   <head>
      <script src="http://metawidget.org/js/3.5/metawidget-core.min.js"></script>
      <script type="text/javascript">
      var person = {
         name: "Homer Simpson",
         age: 40,
         retired: false
      };
      </script>
   </head>
   <body>
      <div id="metawidget"></div>
      <script type="text/javascript">
         var mw = new metawidget.Metawidget( document.getElementById( 'metawidget' ), {
            inspector: new metawidget.inspector.CompositeInspector( [
               new metawidget.inspector.PropertyTypeInspector(),
               function( toInspect, type, names ) {
                  return {
                     properties: {
                        last_name: {
                           type:"object",
                           required:false,
                           properties: {
                              prefix: {
                                 type:"string",
                                 required:false
                              },
                              suffix: {
                                 type:"number",
                                 required:false
                              }
                           }
                        }
                     }
                  };
               }

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

The developer was trying to add a custom Inspector to declare a new property last_name. This should be rendered along with his normal JSON object (which contains name, age and retired). But the result looks strange:

Let's explain what Metawidget is doing. Metawidget uses its Inspectors to retrieve metadata about which properties to render. Then it iterates over those properties and chooses appropriate widgets for them. For simple types, like string and number, Metawidget chooses native widgets like input type="text" and input type="number". But for complex types, such as object, it creates a nested Metawidget and recurses into it.

For this to work, the Inspectors have to play along. They cannot return the same metadata for the nested Metawidget as for the top-level Metawidget. If they do, we'll just recurse infinitely. This is what we're seeing. Actually you can see Metawidget helpfully caps the recursions so that the browser doesn't crash completely!

To fix this, our custom Inspector has to understand what it's being asked for and return appropriate metadata. It can use the names array to do this. A complete example:

<!DOCTYPE html>
<html>
   <head>
      <script src="http://metawidget.org/js/3.5/metawidget-core.min.js"></script>
      <script type="text/javascript">
         var person = {
            name: "Homer Simpson",
            age: 40,
            retired: false
         };
      </script>
   </head>
   <body>
      <div id="metawidget"></div>
      <script type="text/javascript">
         var mw = new metawidget.Metawidget( document.getElementById( 'metawidget' ), {
            inspector: new metawidget.inspector.CompositeInspector( [
               new metawidget.inspector.PropertyTypeInspector(),
               function( toInspect, type, names ) {
                  if ( names === undefined ) {
                     return {
                        properties: {
                           last_name: {
                              type:"object",
                              required:false,
                           }
                        }
                     };
                  }


                  return {
                     type:"object",
                     required:false,
                     properties: {
                        prefix: {
                           type:"string",
                           required:false
                        },
                        suffix: {
                           type:"number",
                           required:false
                        }
                     }
                  }
               }
            ] )
         } );
         mw.toInspect = person;
         mw.buildWidgets();
      </script>
   </body>
</html>

This will correctly render:

This is the 'native' way of doing things. Another way of thinking of it is: the 'native' result from an Inspector cannot contain nested properties. Of course, for more complex use cases your Inspector will have to consider which names it's being asked for and act accordingly.

However Metawidget can make things a little easier. It comes with a JsonSchemaInspector which understands how to navigate JSON Schemas for nested properties. So you can simply do:

<!DOCTYPE html>
<html>
   <head>
      <script src="http://metawidget.org/js/3.5/metawidget-core.min.js"></script>
      <script type="text/javascript">
         var person = {
            name: "Homer Simpson",
            age: 40,
            retired: false
         };
      </script>
   </head>
   <body>
      <div id="metawidget"></div>
      <script type="text/javascript">
         var mw = new metawidget.Metawidget( document.getElementById( 'metawidget' ), {
            inspector: new metawidget.inspector.CompositeInspector( [
               new metawidget.inspector.PropertyTypeInspector(),
               new metawidget.inspector.JsonSchemaInspector( {
                  properties: {
                     last_name: {
                        type:"object",
                        required:false,
                        properties: {
                           prefix: {
                              type:"string",
                              required:false
                           },
                           suffix: {
                              type:"number",
                              required:false
                           }
                        }
                     }
                  }
               } )

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

Hope that helps!

6 comments:

Shahana Shafiuddin said...

I like the way you gave example of your code.

Neeraj said...

Hi Richhard,

I want to combine 2 JSON scehma by composite inspector. On schema is coming from back-end and is purely about data, then I want to combine it with UI properites given in another JSON schema with same properties.

to achieve this I am using compositeInspector as below:
new metawidget.inspector.CompositeInspector([ new metawidget.inspector.PropertyTypeInspector(),
new metawidget.inspector.JsonSchemaInspector(dataConfig), function(toInspect, type, names){
if( UIMetadata ){

return UIMetadata //getPropertyByString(UIMetadata.properties,names.join("."));
}
} ]

But it goes in infinite loop.

JSON Schema is as belwo :
{
"properties": {
"Active": {
"type": "boolean"
},
"CodeNumber": {
"type": "number"
},
"Text": {
"type": "string"
},
"Types": {
"properties": {
"WSACommentType": {
"multiSelect": true,
"type": "array",
"title":null,
"items": {
"properties": {
"name": {
"type": "string"
}
}
}
}
}
}
}
}


UI Schema is:
{
"properties": {

"Text": {
"title": "comment text"
},
"Types": {
"properties": {
"WSACommentType": {
"title": null
}
}
}
}
}

Richard said...

As described in this blog post, make sure you are checking the 'names' array in your custom inspector.

Metawidget will pass different 'names' to inspect different parts of your schema. If you're always returning the same schema regardless of 'names', then you'll get an infinite loop.

Does that solve your problem?

Neeraj said...

Hi Richhard,

Many many thanks for prompt response.
it solves the problem and pageisgetting displayed.
but I have new issues now.
My Approach is to get JSON schema for data from server(generated from existing XSD), and then specify UI properties (like: readOnly, title,propertyOrder etc) in JSON schma on UI side. and then use compositeInspector to combine both schema to generate UI.

Combining of two JSON is working for some of the properties but not for all
For example in below JSON I have given Title null for WSAComment, WSACommentType. It works for WSACommentType (label is not shown)but it doesn't work for WSAComment (label is shown ), Similarly propeties specified for name and check are not working, if I add these properties in shcema returned from server (I am passing that scham to JSONSchemaInspctor), then it works.

belwo are both JSON files:

Server JSON file passed in JSONSchemaInspector
:{
"properties": {
"WSAComment": {
"properties": {
"Active": {
"type": "boolean"
},
"CodeNumber": {
"type": "number"
},
"Text": {
"type": "string"
},
"Types": {
"properties": {
"WSACommentType": {
"type": "array",
"items": {
"properties": {
"Name": {
"type": "string"
},
"check": {
"type": "boolean"
}
}
}
}
}
}
}
}
}
}

JSON schema defined on UI,returned by Inspector function :

{
"properties": {
"WSAComment": {
"title":null,
"properties": {
"Text": {
"title": "comment text",
"propertyOrder":1
},
"Types": {
"properties": {
"WSACommentType": {
"title": null,
"multiSelect": true,
"items": {
"properties": {
"Name": {
"propertyOrder":1,
"readOnly": true,
"title": null
},
"check": {
"title": null,
"propertyOrder":2
}
}
}
}
}
}
}
}
}
}

Neeraj said...

code snippet for Composite Inspector

new metawidget.inspector.CompositeInspector([ new metawidget.inspector.PropertyTypeInspector(),
new metawidget.inspector.JsonSchemaInspector(dataConfig), function(toInspect, type, names){
if( UIMetadata ){

return getPropertyByString(UIMetadata.properties,names.join("."));
}
} ]

method to return property value

var getPropertyByString = function(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
var a = s.split('.');
while (a.length) {
var n = a.shift();
if (n in o) {
o = o[n];
} else {
return;
}
}
return o;
}

Richard said...

Please send a small, complete project to support@metawidget.org so that I can debug it