Over a million developers have joined DZone.

Two Ways to Use Ceylon in the Browser

Up until now, using Ceylon in a browser wasn't really straightforward. The good news is, Ceylon 1.2.1 brought two major features that overcome this problem. Let's check them out.

· Web Dev Zone

Make the transition to Node.js if you are a Java, PHP, Rails or .NET developer with these resources to help jumpstart your Node.js knowledge plus pick up some development tips.  Brought to you in partnership with IBM.

As you might (or might not) know, Ceylon is more than a JVM language. It has been possible to compile Ceylon code to JavaScript for a long time, but other platforms such as Dart or LLVM are around the corner.

Having a JS backend means that you can actually write Ceylon code that can be run in a web browser, giving the opportunity to share code between the server and the client. The web IDE is a very good example of this. Up until now, using Ceylon in a browser wasn't really straightforward, though. The good news is, Ceylon 1.2.1 brought two major features that overcome this problem:

Let's see how these fit together.

Creating a New Project

First things first, we need an empty project that will hold two modules:

  • com.acme.client is a native("js") module that imports ceylon.interop.browser:

    native("js") module com.acme.client "1.0.0" { import ceylon.interop.browser "1.2.1-1"; }

  • com.acme.server is a native("jvm") module that imports ceylon.net:

    native("jvm") module com.acme.server "1.0.0" { import ceylon.net "1.2.1"; }

Serving Ceylon Modules

In order to run com.acme.client in a browser, we have to import it from an HTML file. The recommended way is to use RequireJS:

<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Hello from Ceylon!</title>
</head>
<body>
    <div id="container">
    </div>
    <script src="//requirejs.org/docs/release/2.1.22/minified/require.js"></script>
    <script type="text/javascript">
    require.config({
        baseUrl : 'modules'
    });
    require(
            [ 'com/acme/client/1.0.0/com.acme.client-1.0.0' ],
            function(app) {
                app.run();
            }
    );
    </script>
</body>
</html>

Here, we tell RequireJS to use the prefix modules when downloading artifacts from the server, which means we need something on a server that will listen on /modules, parse module names, and serve the correct artifact.

Option 1: Using a Ceylon Server

Ceylon SDK 1.2.1 introduced a new endpoint named RepositoryEndpoint, that uses a RepositoryManager to look up module artifacts in one or more Ceylon repositories like the compiler or Ceylon IDE do:

import ceylon.net.http.server.endpoints {
    RepositoryEndpoint
}
import ceylon.net.http.server {
    newServer
}

"Run the module `com.acme.server`."
shared void run() {
    value modulesEp = RepositoryEndpoint("/modules");
    value server = newServer { modulesEp };

    server.start();
}

By default, this endpoint will look for artifacts in your local Ceylon repository, but also in the compiler's output directory. This greatly simplifies our development workflow, because each time we modify files in com.acme.client, Ceylon IDE will rebuild the JS artifacts, which can then be immediately refreshed in the browser.

Finally, to serve static files (HTML, CSS, images, etc.), we need a second endpoint that uses serveStaticFile to look up files in the www folder, and serve index.html by default:

function mapper(Request req) 
        => req.path == "/" then "/index.html" else req.path;

value staticEp = AsynchronousEndpoint(
    startsWith("/"), 
    serveStaticFile("www", mapper),
    {get}
);

value server = newServer { modulesEp, staticEp };

If we start the server and open http://localhost:8080/, we can see in the web inspector that the modules are correctly loaded:

Option 2: Using Static HTTP Servers

Option 1 is interesting if you already have a backend written in Ceylon. Otherwise, it might be a little too heavy because you're basically starting a Ceylon server just to serve static files. Luckily, there's a way to create a standard Ceylon repository containing a module and all its dependencies: ceylon copy.

ceylon copy --with-dependencies com.acme.client

This command will copy the module com.acme.client and all its dependencies to a given folder (by default ./modules), preserving a repository layout like the one RequireJs expects. This means we can start httpd or nginx and bind them directly on the project folder. Modules will be loaded from ./modules, we just have to configure the server to look for other files in the www directory.

Attention though, each time we modify dependencies of com.acme.client, we will have to run ceylon copy again to update the local repository.

Option 2 is clearly the way to go for client apps that don't require a backend. Like option 1, it doesn't force you to publish artifacts in ~/.ceylon/repo.

Of course, if you are running a local Ceylon JS application, and your browser allows you to include files directly from the filesystem, you can also avoid the HTTP server and load everything for the filesystem.

Using Browser APIs

Now that we have bootstrapped a Ceylon application running in a browser, it's time to do actual things that leverage browser APIs. To do this, we'll use the brand new ceylon.interop.browser which was introduced in the Ceylon SDK 1.2.1 a few days ago. Basically, it's a set of dynamic interfaces that allow wrapping native JS objects returned by the browser in nice typed Ceylon instances. For example, this interface represents the browser's Document:

shared dynamic Document satisfies Node & GlobalEventHandlers {
    shared formal String \iURL;
    shared formal String documentURI;
    ...
    shared formal HTMLCollection getElementsByTagName(String localName);
    shared formal HTMLCollection getElementsByClassName(String classNames);
    ...
}

An instance of Document can be retrieved via the toplevel object document, just like in JavaScript:

shared Document document => window.document;

Note that window is also a toplevel instance of the dynamic interface Window.

ceylon.interop.browser contains lots of interfaces related to:

Making an AJAX call, retrieving the result, and adding it to a <div> is now super easy in Ceylon:

import ceylon.interop.browser.dom {
    document,
    Event
}
import ceylon.interop.browser {
    newXMLHttpRequest
}

shared void run() {
    value req = newXMLHttpRequest();

    req.onload = void (Event evt) {
        if (exists container = document.getElementById("container")) {
            value title = document.createElement("h1");
            title.textContent = "Hello from Ceylon";
            container.appendChild(title);

            value content = document.createElement("p");
            content.innerHTML = req.responseText;
            container.appendChild(content);
        }
    };

    req.open("GET", "/msg.txt");
    req.send();
}

Going Further

Dynamic interfaces are really nice when it comes to using JavaScript objects in Ceylon. They are somewhat similar to TypeScript's type definitions, which means in theory, it is possible to use any JavaScript framework directly from Ceylon, provided that someone writes dynamic interfaces for its API.

The Ceylon team is currently looking for ways to load TypeScript definitions and make them available to Ceylon modules, which would greatly simplify the process of adding support for a new framework/API.

The complete source code for this article is available on GitHub.

A live example is available on the Web IDE.

This article was written by Bastien Jansen

Learn why developers are gravitating towards Node and its ability to retain and leverage the skills of JavaScript developers and the ability to deliver projects faster than other languages can.  Brought to you in partnership with IBM.

Topics:
ceylon ,javascript

Published at DZone with permission of Gavin King, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}