API Development

Creating Cross-Platform Alloy Tags

Creating Cross-Platform Alloy Tags

One of the great features of Alloy is the ability to customise and write Alloy tags. I use this feature a lot to deal with a common challenge — reducing the amount of duplicate code when working with other mobile platforms.

Take the example of Navigation in an app — the Titanium API gives us a Ti.UI.TabGroup control that works on iOS and Android, but Ti.IU.IOS.NavigationWindow is iOS only .

Typically, you might workaround this by having a view as follows:

<Alloy>
    <NavigationWindow platform=“ios”>
        <Window>
            <View id="content" layout="vertical">
                <TableView onClick="openWindow">
                    <TableViewRow title="My Details" hasChild="true"/>
                    <TableViewRow title="Logout" hasChild="true"/>
                </TableView>
            </View>
        </Window>
    <NavigationWindow>
    <Window platform=“android”>
        <View id="content" layout="vertical">
            <TableView onClick="openWindow">
                <TableViewRow title="My Details" hasChild="true"/>
                <TableViewRow title="Logout" hasChild="true"/>
            </TableView>
        </View>
    </Window>
<Alloy>

In this example, I’ve marked the NavigationWindow as being iOS only and added a new tag for a Window for Android. The problem here is that any content in the Window element is duplicated. I could solve that by moving my Window content into a second view, and requiring it, so it becomes:

<Alloy>
    <NavigationWindow platform=“ios”>
        <Window>
            <Require src="settings"/>
        </Window>
    <NavigationWindow>
    <Window platform=“android”>
        <Require src="settings"/>
    </Window>
<Alloy>

With this approach, I’m not duplicating as much and by using require, I’m adding in the settings view as include, but it still feels a little messy. Ideally, the best solution here is to redefine the NavigationWindow tag itself, so we can essentially make it work on iOS and Android.

First, I need to create a new commonJS module in the /lib folder of the app, called navigationWindow.js.

In order to emulate / redefine a NavigationWindow, we have to create some methods that Alloy and Titanium expect — so I’ll add this code to the navigationWindow.js file:

exports.createNavigationWindow = function(args){
    return Ti.UI.iOS.createNavigationWindow(args);
}

I also need to add the module attribute to my view:

<Alloy>
    <NavigationWindow module="navigationWindow">
        <Window>
            <Require src="settings"/>
        </Window>
    <NavigationWindow>
<Alloy>

When I run the app nothing should have changed — the NavigationWindow opens normally. This is because I’ve told Alloy to look in the module navigationWindow to create the tag, so Alloy looks for an exported method called createNavigationWindow and it expects to get back a `Ti.UI.iOS.NavigationWindow element, which it does.

You can add the module attribute to the Alloy tag itself if you don’t want to have to add it to every tag you’re customising, and Alloy will check your library first. In this case, using a name like ui.js is probably better.

So, now for the fun part. With Alloy thinking it’s created a NavigationWindow, I can now return something else and have that adapted to work on Android.

Luckily, pretty much all visual elements in Titanium inherit from a view, so they all share comment properties and methods. This is the same for Ti.UI.Window, Ti.UI.TabGroup and Ti.Ui.NavigationWindow. They all have .open() and .close() methods for example.

So, I can change the commonJS module code to this:

exports.createNavigationWindow = function(args){
    return args.window;
};

When I re-launch the app, it now shows the default Window of the NavigationWindow. Alloy thinks it’s created a Ti.UI.NavigationWindow and when it calls the .open() method it opens the Window instead!

This is great, but I need to support some other methods that Alloy is expecting — if I tried to call the Navigation Window .openWindow() method to open a sub-window, it would fail, because it doesn’t exist.

The quickest way to do that is to add the method to the Window that I’m returning so:

exports.createNavigationWindow = function(args){
    args.window.openWindow = function(win){
          win.open();
    };
    return args.window;
};

The final part is I need to adapt this for iOS and Android, so that on iOS the a NavigationWindow and Android returns a Window.

Here’s the final code:

exports.createNavigationWindow = function(args) {

    if (OS_IOS) {
        return Ti.UI.iOS.createNavigationWindow(args);
    }

    args.window.openWindow = function(win) {
        win.open();
    };

    return args.window;
};

Now, when Alloy creates the NavigationWindow, it gets a normal Ti.UI.iOS.NavigationWindow on iOS, and a Window on Android. On iOS, .openWindow() works as expected, sliding in the new Window, and on Android the new Window opens on top.

With a few lines of code, I have a cleaner XML view that works on both platforms!

This is a quick example to show how to do this — there’s a much better version available in xp.ui.js and crux.navwindow.js . There’s also an example in the CRUX library of using a Template tag, which enhances the Require tag by allowing you to wrap a view in a template, like so:

<Alloy module="crux.templates">
    <Template src="template1" >
        <Label color="black">Hello World!</Label>
    </Template>
</Alloy>

Enjoy playing with Alloy tags and let me know in the comments about any you’ve redefined, or new ones you’ve created yourself.

Happy coding!