Embeddable Todoist
By Seva Zaikov
Disclaimer: this is not an official project
I always wanted to build a checklist-type application, which would allow me to quickly write down a list and send a link to someone else. The options boiled down to big applications requiring everyone to set up accounts, or adopting someone else’s custom solution. I’ve even seen people embedding PDFs with checklists, which at least has a great advantage of being print-friendly. As a part of a personal exploration at work, I built the following application:
Go ahead, play with it. It’s all running locally on your browser. If you want to keep your changes, you can optionally import your project into your Todoist account.
In sum, I wanted to replicate our existing application on a smaller scale. While Todoist provides all the functionality I was interested in, we still require an account to provide a richer experience and additional features. But what I really wanted to build was a much simpler and self-hosted application.
Bootstrapping the app
Creating an application from scratch was an option, but it would lead to a lot of unnecessary work. Since we already have great UI in Todoist, it was just needed to pick the right components and encapsulate their functionality. Besides, it would be a great test to see how modular our application is (I can gladly confirm, it is highly modular). We’re using React to compose our UI from different components, like the task, the scheduler popup, task editor, section header or the task list itself.
Initially, I created a new application and started to copy components 1 by 1 where needed, but it quickly became a rabbit hole to fulfill all relevant imports. Instead, I cloned the application locally and started from scratch by creating a new Webpack entry point , which became the backbone of the future application. From there, I was able to import only relevant parts and, because the application was cloned, it became much easier to remove or modify unnecessary parts.
The first step was to render a list of tasks, which was relatively easy. We use Redux for our data layer, and it was simple enough to remove properties which we receive from the Redux store, and pass them individually from their parent component: the new application is pretty small, so it made sense to store all the data at the root component. Since I cloned the application, I was able simply to remove all usage of react-redux connectors . This worked great!
// I removed existing react-redux bindings
function mapStateToProps(state) {
return {
user: getUser(state),
weekStartDay: getWeekStartDay(state),
}
}
// and passed properties directly. Some properties,
// like user, were removed completely, because
// the application didn't have users
return <Task weekStartDay={1} />
Todoist has a lot of features I had no interest in supporting and ended up removing. Thanks to our code separation and component independence, I was able to remove the ones I didn’t need and it solved most of the problems. To use components in different contexts, sometimes it’s worth having a wrapper that passes all relevant properties to a simpler component. This way, it’s possible to reuse the same low-level component while simply passing different properties.
At this point, I implemented deployment process. For the application to be standalone, all I needed to do was compile all static assets and deploy the resulting folder to a server. I did it early and often, which allowed me to collect early feedback, and it helped a lot with testing and developing the idea further.
Storing data
Now came one of the most important tasks — how to store data. A primary goal was for the application itself to just be static assets on a server, without any backend work, which both simplifies my life and the application itself: there is no need for security, database, monitoring, and other things to ensure that the service works smoothly. The idea was for the app to let you create a list and then pass a link around to somebody, so that they can immediately see it. The most obvious solution was to leverage a URL shortener service, which would create a short unique URL that I could match to the user’s data. However, that would require building a service with database to store shortened URLs and matching full version, and I didn’t want to go this route, so I had to look somewhere else.
The only thing that is preserved when you share a URL is the URL itself, which can be at least up to 2000 characters . I really liked the idea of putting information in the URL query parameter. 2000 symbols proved to be quite a lot in my testing. Querying my own tasks, their average length is 20 symbols. Using 20 more symbols for additional metadata, it means this approach would support close to 50 tasks, which is great! So, I decided to serialize data into the URL. To shorten it, I gave up on using JSON to avoid property names, quotes, and other ceremonial symbols: it might look like a small decision, but it saved 15% of space on average. Instead, I just listed all parameters separated by :
. Later, when starting to add new properties like support for completed tasks, due dates, sections, and nested tasks, it proved to be easily extendable as long as new properties were appended at the end.. The only trick is that I couldn’t delete old properties or compatibility would be broken. In my case, I kept all original data and added new, and it worked great. To give you an example, here is a URL with one task:
https://embed-todoist.bloomca.me/embed.html?1:passport:0:0:no:0;2:some%20cash:0:0:no:0;3:credit%20card:0:0:2021-05-14r0t0May%2014:0;4:rainproof%20jacket:0:1:no:0;5:good%20walking%20shoes:0:1:no:1;1:Clothes;p:Travel%20checklist;
You can see the data right after the query, with the following structure: id:content:parent_id:section_id:due:checked;
.
After being able to render a list relying only on the URL, it was time to send it to someone else. They should see the same list! That was a great moment. To illustrate this example, this link will open the same application as embedded below:
Connecting to Todoist
I was doing pretty good on time, so I decided to expand the scope a bit, and connect it to actual Todoist, not by using the live data, but instead providing the user an opportunity to create a real Todoist project from the current list. To do so, we need to authorize the application to perform actions in Todoist on behalf of the user. Todoist supports OAuth protocol , and is a standard across applications requesting permissions. The only problem is that it requires to have a server page, which sends a request to Todoist with client ID and client secret, and they have to be stored on our server so that nobody can access them. This was an extra functionality and not a core feature, and it does not require to maintain a database as well, so I decided to add this additional feature. To make it work, I needed to register my application and save callback URL, and open a new window in the browser with the url “ https://todoist.com/oauth/authorize?client_id=${CLIENT_ID}&scope=data:read_write ”. Todoist will put a special code as a query parameter and redirect to that page, and on our server we’ll make a request to “ https://todoist.com/oauth/access_token ” to exchange the code for actual user token. Here is the backend code to make a request and receive user token:
const postData = querystring.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: query.get('code'),
})
const options = {
hostname: 'todoist.com',
port: 443,
path: '/oauth/access_token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData),
},
}
let response = ''
const tokenRequest = https.request(options, (tokenResponse) => {
// save all byte data
tokenResponse.on('data', (d) => (response += d))
tokenResponse.on('close', () => {
// object with `access_token` property
const jsonResponse = JSON.parse(response)
})
})
As you know by now, the application is embeddable. All I needed was to detect an iframe , and add some iframe-specific styles and markup. Here is a function to detect whether we are inside an iframe or not:
function inIframe() {
try {
return window.self !== window.top
} catch (e) {
return true
}
}
// After I detected that we are in an iframe
// environment, I added a class to the page:
if (inIframe()) {
document.body.classList.add('embed-iframe')
}
Embedding Todoist’s editor
The last big milestone was to make the task editor work. Potentially, it would be the most challenging part, because the editor supports tons of options, some of which I did not need, like:
- assigning a task to a collaborator
- moving a task to a different project
- adding labels
- adding reminders
While I did not need these specific features, there were others that were helpful to retain. For example, due dates , to make it possible to attach a specific date to tasks. With natural date parsing , it became possible to type tasks like send a letter in 5 days and have it automatically pick up the correct due date. To keep scope in check, language support was limited to English.
All these features are connected to the editor via plugins, with properties to enable/disable them depending on the context the editor is rendering in:
<RichTextInput
...
isAssigneeDisabled={isAssigneeParseDisabled}
isDateistDisabled={isDateParseDisabled}
isPriorityDisabled={isPriorityParseDisabled}
isLabelsDisabled={!canUseLabels}
...
/>
With this, I could easily disable unwanted properties and remove related imports. All features inside the component already checked whether they were enabled or not, which made my job much easier. This design allows fine grained control over the feature set of the editor in specific situations, like this one.
Conclusion
Overall, it was a great exercise, I got a chance to bend our codebase, which proved to be quite flexible, not a light achievement after 10+ years of development. And I was able to use components I hadn’t a chance before. I am really pleased with the result and I am glad I had this opportunity! As a takeaway, I am very impressed with how modern frameworks make it easy to create components which can be slightly modified and reused in different contexts. And since adding another entry point is very easy, together they facilitate building different applications using a single codebase. In fact, we have used this approach with great success to create new Quick Add page, and a dedicated Electron application.