9 minutes read
The incredibly fast Parallel DOM is surely the most famous of all the features Ractive.JS provides but there’s more. With Adaptors you can extend Ractive.JS so it can communicate with other libraries, modules or components. In this article we’ll create an adaptor for a query-service that delivers JSON data.
The demo app is located here.
The sources are here.
Project Structure
The project contains four JavaScripts and a simple template: main.html
- query-service.js, for querying JSON data by using the new fetch-API
- query-helper.js, which acts as a module that’ll be wrapped by the adapter
- query-adapter.js, which Ractive.JS uses to access the query-helper
- main.js, where Ractive.JS gets instantiated
The compilation is done with Gulp + WebPack by entering gulp or gulp watch in the console. Local testing with npm start (it utilizes Hapi.JS to boot the app).
What is an Adaptor?
I suppose that many of you already know Ractive’s philosophy of “getting out of the way” when it comes to external libraries, app structure, model definitions, persistence etc. If not then I recommend to read this article. Simply spoken: we have all the freedom to decide what kind of external logic encoded in libraries, modules and components we want to use in our app. However, sometimes we simply can’t or don’t want just to plug in an external entity and let it work. Maybe the component isn’t directly usable because of some configuration issue? Or the data types do not fit with our internal structure? Maybe we want to implement some advanced debugging/logging technique for better control? Whatever it may be, Ractive.JS offers a clean interface for wrapping any kind of external code: Adaptors.
Adaptor Interface Description
Every Ractive.JS adaptor must implement two methods: filter and wrap. The former is used to determine if a certain object is of type which should be controlled by the adaptor and returns a boolean. The latter delivers all the wrapping logic like variable mappings, event bindings, function calls, redirection etc. Therefore the wrap method must return an object that comprises of following functions:
- teardown method for cleaning up resources when the adapter is destroyed
- get method which returns an object containing the public API of the wrapped object (here we decide how Ractive.JS should “see” the actual object)
- set method which gets called when we try to “set” anything on the original object
- reset method which gets called when we try to execute a “set” on a keypath that is identical to the keypath of the actual object. In such cases the object could either modify itself by using the new data or simply return false to get torn down.
Creating an adaptor for a simple JSON query service
QueryService implements the fetchData method for querying JSON services. It basically returns a promise object containing a JSON structure. But being a rather abstract object which can query any kind of JSON data we decide to create another object to hide this structure behind some “public” interface: the QueryHelper class.
Now we want to “teach” Ractive how to use this nice object by providing a proper adapter for it.
We see its definition containing the filter method that checks if the given object is of type QueryHelperClass. Below is the wrap method containing following important entries:
- qhInstance.setUrl
- qhInstance.queryApi
- return statement giving back an object containing teardown, get, set and reset methods
The qhInstance object is one of the arguments provided by Ractive.JS environment and represents the object our adapter wraps (btw., the name “qhInstance” was deliberately chosen by me and is not “canonical“). But there are some more arguments: the reference to Ractive itself, the keypath and the prefixer helper function, which is used to turn relative keypaths into absolute ones.
We then use the reference to the actual object to overwrite its prototype’s methods. Of course, these new definitions don’t change the original behavior but only let Ractive know about changes that happen when the original methods get called. By using this strategy we want to preserve the original behavior but additionally let the new environment, Ractive.JS in this case, know what’s happening inside the wrapped entity. In our example the amount of “important” information is not very complex but one can surely imagine more complex cases where additional logging or changes of data types, formats etc. must happen before proceeding any further. One scary example could be some accessing some legacy RPC-XML WebService and converting the results into JSON first before letting this data be consumed by some nice mobile-first web app.
In our case the original object QueryHelper implements two methods we want to be called indirectly by our Adapter and not by the QueryHelper object itself: setUrl and queryApi. And this is why we overwrite them inside the wrap function. To make the execution visible I’ve inserted a few log-calls so we can later see them shown in the console.
This is how they look inside:
The first one forwards the given url to the adapter object and to the wrapped QueryHelper instance. We utilize two private variables which give us access to QueryHelper’s service reference and its url property. So, the whole “magic” is the direct access to the QueryService’s API in qhInstance.queryApi method. By utilizing this strategy we “degrade” the QueryHelper object to a mere shell which is used by Ractive.JS, because Ractive doesn’t care which way the data comes in.
The Wrapper Object
The most complex object is the returned wrapper. It must implement a teardown method which removes the previously defined replacement methods and the get, set and reset functions. The get method is just returning the actual Adapter API containing all the Ractive-visible methods. These will be used by Ractive when calling the original methods of the wrapped object. The set method gets called whenever we execute ractive.set() on the wrapped object. The reset method is actually an additional set-method but this one only gets called when the given keypath equals to the keypath of the wrapped object. In such case the wrapped object can either update itself or return a false which will activate its removal.
Registering an Adapter in Ractive.JS
Every Ractive.JS instance contains an object property named adaptors. Here we have to register our adaptor by simply entering a new property in adaptors object and name it after the new adaptor. Its initial value is the adaptor’s class itself. In our case the adaptor registration was done main.js
In the same file we’ve defined a new Ractive object and referenced the available adaptor logic by using the property adapt which is an array. This time we insert the name of the adaptor as a string. Now Ractive.JS will automatically instantiate this adaptor upon its own creation and each time an object inside Ractive.JS gets used by calling Ractive-API’s like set(), get() etc. the adaptor’s filter method will be called to determine if the object is an instance of a type that is wrapped by the adaptor.
Using the Adaptor
In our demo we’re using a simple on-click event directive to activate the JSON-service query.
Our Ractive instance implements the click-event handler which uses the QueryHelper instance to access its API.
We know what will happen when a get() or set() is called on a Ractive that contains an adapter: its filter method kicks in and if the requested object is of type being wrapped by an adapter no direct call on this object will happen. Instead, the adapter takes control of it. In the browser console we can see the output of our log-call:
We’ve successfully queried the service but not by using the original QueryHelper object. Despite the fact that we’ve instantiated QueryHelper in Ractive’s data field the Adaptor took care of accessing the JSON service instance. Now let’s change the service URL by trying to directly call the original QueryHelper object. First we get from Ractive the QueryHelper’s instance.
For testing purposes I’ve placed this demo app into the global namespace “App” so that it’s Ractive instance can be accessed directly by using the property _main. However, the wording _main is just a preference of mine and has nothing to do with Ractive.JS handling of instances, environments etc. And I think I don’t have to explain in minute detail why you should never, never, never pollute global namespace when developing “real” web applications
As next we try to modify the internal state of the QueryHelper instance by setting a new URL:
And again, the Adaptor kicks in and changes the URL. Now we call the service via queryApi method by clicking the button “Fetch Data“
And again the Adapter is here to take care of calling the service and returning the data. Above we can see the output has changed without refreshing the page thanks to Ractive’s super-fast Parallel DOM.
Conclusion
Adaptors are very powerful and in the beginning surely a little bit confusing. There are several methods to implement, certain patterns to follow, a few additional verbs to learn (wrap, filter, get etc.) but in the long run adapters provide a very usable API which helps “tame” almost any of the available JavaScript libraries.