Migrating from Mojo to Enyo on WebOS

Enough of these consumer-oriented blog posts about TapNote! It is finally time for a look at WebOS and the TouchPad from a developer’s standpoint.

If like myself you developed an app in Mojo, you are likely now considering whether to port it to Enyo (or are in the process of porting already). I wanted to post this a month or two ago, but thanks to the NDA was out of luck. Now, however, here are some pointers about the differences between the two frameworks, and specific suggestions for how to more quickly migrate your application. I am assuming that you have already worked through the basic Enyo documentation and tutorials and are at least somewhat comfortable with the basic ideas (like kinds); if not, you will likely find the discussion to follow confusing.

As you first start getting into Enyo, it will seem very weird compared to Mojo. However, there is a method to this madness, and if your experience is anything like my own you will find that you are able to do the same things as you did in Mojo, except with less code that is better organized. I was skeptical of the benefits of Enyo when I first got into it (and particularly skeptical about not being able to code HTML by hand) but I have since come to believe that HP has made the right choice in switching frameworks. I was particularly heartened as, through the course of the Enyo beta, HP consistently made iterative improvements to the most egregious parts of the Enyo framework and very actively listened to developer feedback.

But you probably don’t care about that; after all, you’re here because you want to migrate your code and take advanage of the new hotness that is the TouchPad.

File layout and appinfo

Unlike Mojo, Enyo is pretty flexible about how you organize and name your files. There are only four required files for an Enyo project, three of them shared with Mojo:

  • appinfo.json
  • framework_config.json
  • index.html
  • depends.js

You will basically need to rewrite your index.html file to include a reference to the Enyo framework and to initialize your base application kind, but appinfo.json and framework_config.json are effectively unchanged. The only thing to note here is that you absolutely must not forget to add this to your appinfo.json:

"uiRevision": 2

The uiRevision key tells WebOS that your app is capable of running full-screen on a TouchPad, avoiding the annoying phone emulator. (Side note: you can actually convert a Mojo application to run on TouchPad without the emulator simply by including this line in your appinfo.json, but I would not recommend it. Theoretically, Mojo should stretch to fill the space just fine, but you will find that the Mojo widgets and default styling do not work effectively on a large screen.)

As for depends.js, this is where you include the various Javascript files that you need for your project. It effectively replaces the Mojo sources.json file that was previously required.

When it comes to your application’s Javascript files, you will be able to keep some virtually intact and completely rewrite others:

  • Models and helper classes can be ported very easily simply by replacing Mojo references with the appropriate Enyo references, and converting them to kinds. Compare, for instance, my own Database class in Mojo vs. its Enyo equivalent. The core logic is unchanged; merely the Prototype and Mojo-specific code had to be converted.
  • View assistants will basically need to be rewritten, but you can typically keep your logic code intact. For instance, I used Prototype classes throughout my app, and although I had to replace the widget setup code with Enyo components most of my event handlers and so forth I was able to port simply by tweaking the arguments they received (see below).

Mojo methods: initialize, setup, cleanup

The basic methods you used for setting up your Mojo assistants map over to Enyo reasonably well.

initialize from Mojo was something you only used if you were writing Prototype classes. Otherwise, this would be the basic function that you used to initialize your object. In Enyo, initialization is done in the constructor method of your kinds.

However, you have to make at least one more change besides renaming the method: you must add this.inherited(arguments) to either the top or the bottom of the method. this.inherited() is a special function that invokes the parent kind’s method, and any time you override an existing kind method you will probably need to call it. Particularly if you forget to put it in your constructor, you will get some very strange errors from the Enyo framework.

For setup and cleanup in Mojo, you can switch to using create and destroy in Enyo, with the same caveat as above to include a call to the parent kind’s definition of those methods.

If you were using Prototype classes, your old code might look like this:

var MySceneAssistant = Class.create({
    initialize: function() {
        // Initialization tasks here
    },
    setup: function() {
        // Setup widgets here
    },
    cleanup: function() {
        // Cleanup for garbage collection
    }
});

And you will transition to code that looks something like this:

enyo.kind({
    name: "MyKind",
    constructor: function() {
        this.inherited(arguments);
        // Initialization tasks here
    },
    create: function() {
        this.inherited(arguments);
        // New Enyo code to initialize items in this.$ hash
    },
    destroy: function() {
        // Cleanup for garbage collection
        this.inherited(arguments);
    }
});

As shown, I typically call this.inherited(arguments) at the beginning of constructor and create and at the end of destroy. The reason for this is that in constructor it triggers the basic setup for the kind (so after that I will have access to my getters and setters, events, and so forth) and in create it sets up the all-important this.$ hash of the kind’s components. In destroy, though, it cleans up the this.$ hash, and I typically need to do my own cleanup before that hash is emptied.

Some things to note:

  • Not all kinds handle create the same way. In particular, if you are working with a Popup or other LazyControl, you will need to use componentsReady or similar, instead.
  • Unlike in Mojo, destroy is not always called in Enyo. In particular, it is not triggered when the user tosses your card off screen, so putting last-minute saving instructions in here is not a good idea. Instead, use ApplicationEvents and onUnload.

Event handling

Event handling in Enyo is completely different from Mojo. Where before you needed to explicitly subscribe to events in activate and deactivate, you now assign event listeners directly in your components array. So where before you had this:

initialize: function() {
    // Save our bindings for later
    this.bound = { editItem: this.editItem.bind(this) };
},

setup: function() {
    // Setup the document list
    this.controller.setupWidget('documents-list', docListAttr, null);
},

activate: function() {
    // Setup the listTap event
    this.controller.listen('documents-list', Mojo.Event.listTap, this.bound.editItem);
},

deactivate: function() {
    // Disable the listTap event
    this.controller.listen('documents-list', Mojo.Event.listTap, this.bound.editItem);
}

In Enyo you will convert it to something like this:

components: [
    {
        name: "list",
        kind: "VirtualList",
        onclick: "editItem",
        // ...
    }
]

Some events handlers will transfer over very easily, but one thing to remember is that Enyo events always have the control that triggered the event as their first argument (unlike Mojo events, which typically have the Event object first, a la vanilla Javascript). So in the example above, this is our old handler:

editItem: function(event) {
    // work with our event object
}

And this is its Enyo equivalent:

editItem: function(sender, event) {
    // work with our event object
}

Not all Enyo events will pass an event object as the second argument; refer to the docs for more information. In general, if you are typing into a DOM event (like onclick in the above example) you will get a standard Event object as your second argument. If you are tying into a custom Enyo kind event, however, you will probably receive something else (you can tell a custom Enyo event because it will be in camelCaps: onCustomEvent vs. onmousedown).

There are other interesting complexities to Enyo event handling, as well (like using *Handler methods to automatically capture DOM events), but most of them are things you will be adding to your project new rather than things you will be using to replace Mojo elements.

Activate, deactivate, and the Enyo ApplicationEvents kind

For events that are not attached to a particular control on screen, Enyo provides a generic ApplicationEvents kind that you can include in the components for any kind that needs access to universal events.

Typically this is most useful because you need to replace your Mojo activate and deactivate logic that triggers when the card is minimized or maximized. For instance:

components: [
    {kind: "ApplicationEvents", onWindowActivated: "activate", onWindowDeactivated: "deactivate"}
],

activate: function() {
    // Do window maximized logic
},

deactivate: function() {
    // Do window minimized logic
}

ApplicationEvents is also what you will use to tie into application relaunch events, the window onunload event (as referenced above), document-level handling of clicks, and more.

Binding

Binding behaves identically in Mojo and Enyo, but the syntax is different. In Mojo, you could use the Prototype bind method: myFunction.bind(this). In Enyo, you need to use the enyo.bind method: enyo.bind(this, myFunction). I used the following regular expressions to convert my bindings (syntax for replacements may vary depending on your editor, and if you are binding functions that are not attached to this you will need to handle those separately):

Find:
(this\.\w+?)\.bind\(this(, .+?)?\)

Replace:
enyo.bind(this, \1\2)

Cookies

Converting from Mojo to Enyo cookies is reasonably straight-forward, with the following caveats:

  • You no longer need to create a Cookie object before you can assign or get a value from it
  • Enyo will not automatically convert Javascript objects! You must stringify and parse objects with JSON to store or retrieve them

For instance, if this is your Mojo cookie code:

var myCookie = new Mojo.Menu.Cookie('DeliciousCookies');
if (!myCookie.get()) {
    var cookies = [
        "Chocolate Chip",
        "Snickerdoodle",
        "Ginger snaps"
    ];
    myCookie.put(cookies);
}

Then you will need to revise it like so:

if (!enyo.getCookie('DeliciousCookies')) {
    var cookies = [
        "Chocolate Chip",
        "Snickerdoodle",
        "Ginger snaps"
    ];
    enyo.setCookie('DeliciousCookies', enyo.json.stringify(cookies));
}
// And later when you fetch the cookie...
var cookies = enyo.json.parse(enyo.getCookie('DeliciousCookies'));

Reading appinfo and device details

In Mojo, if you needed to check a value in your appinfo.json file or lookup the type of device you were using you could use the Mojo.Controller.appInfo or the Mojo.Environment.DeviceInfo objects. In Enyo, this information is now accessible through functions:

  • Mojo.Controller.appInfo enyo.fetchAppInfo()
  • Mojo.Environment.DeviceInfo enyo.fetchDeviceInfo()
  • Mojo.Environment.frameworkConfiguration enyo.fetchFrameworkConfig()

Headless applications and noWindow: true

Setting "noWindow": true in your appinfo.json still works, but Enyo handles it differently than Mojo. Of particular note, in Mojo your assistants could access shared code easily using things like Mojo.Controller.getAppController(). However, in Enyo every window is effectively sandboxed, and has no access to other windows except for the special enyo.application object (to which you can attach whatever properties you need).

For instance, in my own app I used my ApplicationAssistant to handle all the shared behavior, route things through the model, manage synching, and so forth. Most of my assistants thus included this in their initialize function:

this.app = Mojo.Controller.getAppController().assistant;

In Enyo, while I still have a shared application kind, I have to explicitly make it discoverable by my other windows. In the shared application kind’s constructor I have this:

constructor: function() {
    this.inherited(arguments);
    // Other initialization code
    enyo.application.app = this;
}

And then I access it inside of kinds in other windows like so:

constructor: function() {
    this.inherited(arguments);
    this.app = enyo.application.app;
}

The benefit of the Enyo setup is that I can load the kinds that I need as I need them. For instance, the only items in my root-level depends.js file are the shared application kind, data models, and so forth. Then I place my actual windows in their own directories (with their own index.html and depends.js) and only load in the necessary view-related kinds for them.

You will find more information about the difference between Enyo and Mojo headless app handling in the Enyo documentation.

The dreaded switch from widgets to components

Converting your old Mojo-widget-reliant view files to Enyo is going to be the hardest part of the process, for here find and replace will avail you not.

In Mojo, your view files were typically split across several locations:

  • First, you would have one or more HTML files where you would code stub divs representing Mojo widgets and any surrounding markup needed
  • Second, you would have a Javascript Assistant that would initialize your widgets, populate them with data, and handle interaction logic and events
  • Third, you would style your widgets and surrounding markup using CSS (typically all stored in a single central file)

In Enyo the Javascript and CSS is still there, but the HTML is completely absent. Instead, you create a tree of components, each of which typically represents a DOM node (usually a div, although the specific markup will not affect you much).

Most Mojo widgets have Enyo equivalent controls, so a large part of converting your view file is simply a matter of digging through the Enyo API reference in search of the right component.

However, you will need to make some adjustments to how you style your app. Before, you typically would know what the markup surrounding your widgets looked like because you coded it yourself (not necessarily a good thing, of course; digging through the Mojo source code so that I could use un-documented standard classes to achieve groups and so forth was no fun). In Enyo, you can set the className for any component, but if you need to figure out what the standard classes are you will need to use the WebKit Inspector by either running your app in a browser (best) or using a tool like weinre (see my weinre tutorial for more info).

I was skeptical about the Enyo Javascript-only approach at first, but I have since become a convert. Although I theoretically like the idea of having direct control over my markupt, the truth was tht Mojo did not provide direct control over the most important markup, anyway, and being able to run Enyo apps in a normal browser (without an emulator at all) means that it is very, very easy to quickly and accurately style things, even when I have not defined a single one of the classes that went into the markup.

Go forth and Enyo-ize

There are certainly many other differences between Mojo and Enyo, but hopefully this selection will help you to get through some of the nuts-and-bolts conversion tasks that can be so onerous. Good luck with your WebOS app!

8 responses to “Migrating from Mojo to Enyo on WebOS”

Leave a response

  1. rsanchez1 says:

    Ian, I tried myFunction.bind(this) in an enyo kind and it still functions as expected. Did you try it and it did not work for you?

  2. Ian Beck says:

    Are you including Prototype or a similar framework in your app? So far as I know, vanilla Enyo doesn’t do any modifications to root Javascript prototypes (which is necessary for a .bind() function).

    I haven’t tested binding without enyo.bind in maybe a month or two, though. It’s possible they added it in later WebOS 3.0 releases.

  3. rsanchez1 says:

    I’m pretty sure I am only including vanilla enyo in my app. You should try it and and see if it’s not just me seeing things.

  4. Dan Nielson says:

    enyo.bind is declared in lang.js and is not bound to Object.prototype. At least not anywhere I could find.

    If you’re running a late build of V8(Chrome and webOS Javascript engine) you’ll have access to Object.prototype.bind since it’s been included in ECMAScript 5. It works similar to the Prototype.js version and you can read an article about it here: http://dmitrysoshnikov.com/notes/note-1-ecmascript-bound-functions/

  5. rsanchez1 says:

    Ah yes, makes sense now.

  6. Ian Beck says:

    That explains it; I was testing in Safari (which doesn’t use V8).

    I think I’ll stick with my recommendation to switch to enyo.bind simply because it’s safe to use anywhere you use Enyo. Granted, HP has not given us permission to use Enyo anywhere other than WebOS, so the point is moot at the moment, but a guy can dream. :-)

  7. Dan Nielson says:

    Just my two-cents. You can always conditionally add .bind() to *Function.prototype(I know, I said Object.prototype before, but that’s not where it goes.:P). Something like:

    if(!Function.prototype.bind){
    Function.prototype.bind = function(scope){
    var args = arguments;
    args.splice(1,0,this);
    enyo.bind.apply(args);
    }}

    Note, I haven’t actually tested this, but what it should do is just insert the current function into the arguments for enyo.bind so the following statements would be equivalent:

    myObject.myFunction.bind(this);
    enyo.bind(this,myObject.myFunction);

  8. Ben says:

    Thanks for writing this, I have been waiting for a migration guide. Hopefully this will help me on my way.

    Cheers,
    Ben

Leave a response

Clicky Web Analytics