Atom is a modern, to the core hackable editor. This is great, but for developers who aren't fluent in CoffeeScript, it is hard to follow the documentation. Understanding the ecosystem of Atom can become confusing. Let's go through all aspects of how writing an Atom package in JavaScript works.
Understanding Atom
Atom is a Node.js and Chromium based application, written with GitHub's Electron framework. That means it is technically a web application, running on the desktop. Atom's internal functionality is split up into tiny core packages; they're developed the same way as any other package from the community. Although they are all written in CoffeeScript, it is possible to either write them in plain JavaScript, or transpile them via Babel.
Activating Full ES2015 Support with Babel
Babel is a source-to-source compiler; turning ECMAScript 2015 (formerly known as ES6) code into ECMAScript 5 code. Since the environment is Chromium, there are already a lot of supported ES2015 features available. But instead of always looking up which ones are implemented, I recommend using Babel to transpile your code. In a later release — when ES2015 is better supported in Chromium — you can deactivate Babel again and keep your code base (almost) untouched.
To activate transpiling with Babel, each file needs a 'use babel';
statement at the beginning, similar to strict mode in ECMAScript 5. This gives you also the ability to decide which files should be transpiled and which not, by omitting the statement.
The package.json
It helps to view an Atom package as npm module. You have the same access to the API as any tool running on Node.js. Therefore it's possible to add any npm dependency needed. A package.json
is also required, containing all meta data for your project. The basic file should be as follows:
{
"name": "your-package",
"main": "./lib/main",
"version": "0.1.0",
"description": "A short description of your package",
"keywords": [
"awesome"
],
"repository": "http://ift.tt/1T5vSjA;",
"license": "MIT",
"engines": {
"atom": ">=1.0.0 <2.0.0"
},
"dependencies": {
}
}
The important keys are main
— defining the main entry point of your package (defaults to index.js
/index.coffee
) — and engines
— telling Atom on which version your package runs. There is also a set of optional keys available, documented in the "wordcount" package documentation (section package.json
).
The Package Source Code
All your package code belongs in the top-level directory lib/
. I recommend having your entry point in this folder as well, as it keeps the structure clean and makes it easier to scan the project.
Your main file must be a singleton object that maintains the entire lifecycle of your package. Even if your package consists only of a single view, it will all be managed from this object. Your entry-point requires one activate()
method, but should also have the optional deactivate()
and serialize()
.
// lib/main.js
'use babel';
// This is your main singleton.
// The whole state of your package will be stored and managed here.
const YourPackage = {
activate (state) {
// Activates and restores the previous session of your package.
},
deactivate () {
// When the user or Atom itself kills a window, this method is called.
},
serialize () {
// To save the current package's state, this method should return
// an object containing all required data.
}
};
export default YourPackage;
Activate Your Package
The activate()
function is the only required method. Initialise all your modules, views or helpers here. It is passed an object, containing the previous serialized state of your package. If you don't serialize anything in your package, it will be an empty object. That means, it is entirely up to you and your package architecture on what to serialize.
Deactivating
The deactivate()
method is optional, but important. It will be called by Atom when the window is shutting down, or the user deactivates it in the settings. When your package gets deactivated by the user, and you don't dispose of added events/commands, they are still available. This isn't a problem when Atom is shutting down the window. It will tear down events and commands. But if your package is watching files or doing any other work, you should release them in deactivate()
.
Event Subscription
A package usually subscribes to multiple events like adding custom commands, listening to changes, or modified files. It is possible to bundle these into an instance of CompositeDisposable()
, and this way they can all be disposed of at once.
// lib/main.js
import { CompositeDisposable } from 'atom';
const YourPackage = {
subscriptions: null,
activate (state) {
// Assign a new instance of CompositeDisposable...
this.subscriptions = new CompositeDisposable();
// ...and adding commands.
this.subscriptions.add(
atom.commands.add('atom-workspace', {
'your-package:toggle': this.togglePackage
})
);
},
// When your package get's deactivated, all added
// subscriptions will be disposed of at once.
deactivate () {
this.subscriptions.dispose();
},
togglePackage () {
// Code to toggle the package state.
}
};
Serialize All the Things!
Serialization is a powerful, but again optional, feature of Atom packages. Serialization/deserialization happens when a window is shutting down, refreshed or restored from a previous session. It is up to you to define which and how many of your components should serialize their data. What's important is that it returns JSON. If you have a view, and want that to be able to be refreshed, you need to make it compatible with serialization and deserialization.
This very basic component takes an object, which will be used as the component's internal data. Your component then might do some work with the data and can allow its state to be serialized via the serialize()
method.
// lib/fancy-component.js
class FancyComponent {
constructor (configData) {
this.data = configData;
}
// This method will be called when the class
// is restored by Atom.
static deserialize (config) {
return new FancyComponent(config);
}
// The returned object will be used to restore
// or save your data by Atom.
// The "deserializer" key must be the name of your class.
serialize () {
return {
deserializer: 'FancyComponent',
data: this.data
};
}
doSomethingWithData () {}
}
// Add class to Atom's deserialization system
atom.deserializers.add(FancyComponent);
export default FancyComponent;
To make all this useful, this component must be called and serialized in your packages main singleton.
// lib/main.js
import FancyComponent from './fancy-component';
import SomeView from './some-view';
const YourPackage = {
fancyComponent: null,
someView: null,
activate (state) {
// If the component has been saved at a previous session of Atom,
// it will be restored from the deserialization system. It calls your
// your components static 'deserialize()' method.
if (state.fancy) {
this.fancyComponent = atom.deserializers.deserialize(state.fancy);
}
else {
this.fancyComponent = new FancyComponent({ otherData: 'will be used instead' });
}
// More activation logic.
},
// As well as your component, your package has a serialize method
// to save the current state.
serialize () {
return {
fancy: this.fancyComponent.serialize(),
view: this.someView.serialize()
};
}
};
All objects you want to serialize need the serialize()
method. It must return a "serializable object", and a deserializer
key with the name of a registered deserializer. According to Atom, "it usually is the name of the class itself". Additional to that, a class also needs the static deserialize()
method. This method converts an object from a previous state to a genuine object.
Continue reading %How to Write Atom Packages Using Vanilla JavaScript%
by Moritz Kröger via SitePoint
No comments:
Post a Comment