My JXA problem
August 24, 2017 at 11:43 PM by Dr. Drang
I was optimistic when Apple introduced JavaScript for Automation (curiously abbreviated JXA) a few years ago. Even though I’m not a fan of JavaScript, it is at least a “normal” language, unlike JXA’s much older cousin, AppleScript. And because JavaScript is the language of the web, the web itself can provide the answer to almost any JavaScript question at the top of Google’s search results. While I didn’t expect JXA to fully displace AppleScript—there’s just too much AppleScript history for that—I did expect an explosion of JXA example code that I could read and learn from (and steal).
But things haven’t worked out that way. JXA often seems weird and unnatural in how it interacts with apps and in the results it returns. It reminds me of appscript, which worked well but never really felt Pythonic.
But Sal Soghoian keeps telling us JXA is good, so I feel compelled to keep trying. The other day, I wanted to write a script that extracted text from a Reminders list and manipulate it into another form for printing. Because text manipulation in AppleScript can be a horror show,1 I gave JXA a shot. The text manipulation part was breeze, but the extraction of the text from Reminders—the interaction with an app that’s the heart of JXA—made no sense to me. But after lots of experimentation with a stripped-down version of the code, I’m starting to understand. At least I think so.
Let’s assume we have a Reminders list called “Test” that looks like this
and we want to put the text of all the items in “Test” into an array.
This JXA script will do it:
javascript:
Application('Reminders').lists.byName('Test').reminders.name()
Executing it in the Script Editor gives the result
["First", "Second", "Third", "Fourth", "Fifth"]
This is nicely compact, but how does it work, and why?
Most JXA tutorials suggest that the equivalent of AppleScript’s
applescript:
tell application "Reminders"
whatever
end tell
is
javascript:
rem = Application('Reminders')
rem.whatever
so the Application('Reminders')
part makes sense. What about the next part, lists
?
Let’s look at an excerpt from the JavaScript dictionary for Reminders
It says the Reminders application contains lists, but if we try to run
javascript:
Application('Reminders').lists
we’ll get this unhelpful result in the lower pane of the Script Editor window:
Application("Reminders").lists
Nice tautology. Let’s try this:
javascript:
typeof(Application('Reminders').lists)
Now our result is "function"
. Well, if it’s a function, maybe we should see what happens if we execute it.
javascript:
Application('Reminders').lists()
This gives us the more useful result
[Application("Reminders").lists.byId("47012FD8-87CB-490D-8C27-D957EBCB2C86"),
Application("Reminders").lists.byId("0DAC8B42-EF53-4F50-AF6B-EE8B9D98BE99"),
Application("Reminders").lists.byId("24611E1A-712A-4FD3-9908-AD30E9CD9825"),
Application("Reminders").lists.byId("FBC65F34-1F32-460F-B11B-C4AAFCFD3F75")]
We’d get essentially the same result from this AppleScript:
applescript:
tell application "Reminders" to get every list
OK, so now we see that lists
is a function, but doesn’t the dictionary say lists are objects? Yes, but in JavaScript functions are objects and they can have their own properties and functions (or methods, if you prefer). The documentation in the dictionary isn’t lying to us, but it isn’t being entirely forthcoming, either.
It was when I realized that I could execute some of the things identified as objects in the dictionary that JXA started to make some sense to me.
The result indicates that we can access individual Reminders lists by their ID through the cleverly named byID
function. Is there a similar byName
function? Yep. This will get the list we want:
javascript:
Application('Reminders').lists.byName('Test')
And if we want the reminders in that list, we run
javascript:
Application('Reminders').lists.byName('Test').reminders()
which returns
[Application("Reminders").reminders.byId("x-apple-reminder://5F123EA1-681B-4A70-822B-734B23145A3C"),
Application("Reminders").reminders.byId("x-apple-reminder://A017D812-A6A5-4E7D-A8CE-57F08B711DA8"),
Application("Reminders").reminders.byId("x-apple-reminder://356AA6D9-E7B0-40F5-B46C-07697E6E2249"),
Application("Reminders").reminders.byId("x-apple-reminder://B15B20C7-5DDD-4BB2-B985-EADD33628803"),
Application("Reminders").reminders.byId("x-apple-reminder://68AE8F47-2D73-45CF-AAAB-2CD65E3374F6")]
And if we want to get the names of these reminders, we’re back to where we started:
javascript:
Application('Reminders').lists.byName('Test').reminders.name()
Suppose we’ve completed the fourth reminder and want only the names of the reminders that haven’t been completed. In AppleScript, this would mean adding a whose
clause in the middle of the specification, and JXA works basically the same way.
javascript:
Application('Reminders').lists.byName('Test').reminders.whose({completed: false}).name()
yields
["First", "Second", "Third", "Fifth"]
What you pass to whose
is a property/value pair in curly braces. If you need to do a more complicated comparison (not just a simple equality), I suggest you look at the Filtering Arrays section of the JXA 10.10 release notes. For example, to show all the reminders completed before a certain date, you’d do something like this:
javascript:
targetDate = new Date(2017, 8, 26, 0, 0, 0, 0)
Application('Reminders').lists.byName('Test').reminders.whose({completionDate: {_lessThan: targetDate}}).name()
For our example, that would yield the result
["Fourth"]
This is too long for comfortable reading, so it would be better to break it up into at least two parts:
javascript:
targetDate = new Date(2017, 8, 26, 0, 0, 0, 0)
rem = Application('Reminders').lists.byName('Test').reminders
rem.whose({completionDate: {_lessThan: targetDate}}).name()
The intent of this is easy to work out, but I don’t think I’ll ever be able to carry this kind of syntax in my head. I’m OK with the braces within braces, because that’s basically JSON, but the leading underscore just seems stupid. But now I have a blog post that’ll help me find it the next time I need it.
-
If I never have to set and reset
AppleScript text item delimiters
again, I’ll die happy. ↩