JXA - Classes, Subclasses / Libraries, Sub-Libraries. Is it possible?

For the past week I’ve “found” JXA and have been exploring what is available in it. I’m trying to make a library for other scripters to automate an application (screenflow).

After a week of coding and a library with 10 functions I started to think “this is a huge mess and is super innefficient” e.g. To change the SMPTE setting I have:

function setSMPTE(value) {
	var system = Application('System Events')
	var ScreenFlow = system.processes['ScreenFlow']	
	preferencesOpen()
	ScreenFlow.windows[0].toolbars[0].buttons['Timeline'].click()
	ScreenFlow.menuBars[0].menus['ScreenFlow'].menuItems['Preferences…'].click()
	
	//Get SMPTE element
	var obj = ScreenFlow.windows[0].checkboxes["Use SMPTE timecode"]
	
	//Set checkbox value
	setCheckbox(obj,value)
	
	//Close ScreenFlow preferences
	ScreenFlow.windows[0].buttons[1].click()
}

Thing is, this isn’t the only preference I might want to change… I might have a whole list of preferences to change. And I probably want to save the original state to reload the initial state afterwards (because the user will likely want their preferences the same as they were…)

Ultimately I would need:

preferences.open()
state = preferences.getState()`
preferences.setState({SMPTE:1,...})
preferences.close()

//do other code

preferences.open()
preferences.setState(state)
preferences.close()

So initially I set out to make a class:

class preferences {

}

But I get an error - Error on line 1: SyntaxError: Unexpected use of reserved word 'class'. A few iterations later I went even more basic:

function preferences(){
  var object = {};
  object.isOpen = false
  object.open = function(){
  	this.isOpen = true
  }
  return object;
}

Great! Now I can call:

preferences = sf.preferences()
preferences.isOpen

And the script returns false, as expected! Sadly, when I try to call a method:

preferences = sf.preferences()
preferences.isOpen
preferences.open()
preferences.isOpen

I get the error - Error on line 3: TypeError: Object is not a function (evaluating 'preferences.open()')

After this I figured “Well maybe classes and objects weren’t implemented properly in JXA. Why don’t I use libraries! They seem to work fine!”

So now I made 2 libraries: “screenflow” and “properties” with the following code:

"screenflow": {
  preferences = Library('preferences')
}

"preferences": {
  this.isOpen = false
  getOpen = function(){
    return this.isOpen
  }
  setOpen = function(){
    this.isOpen = true
  }
}

And made the following 2 test scripts:

"test1": {
  function notify(msg){
    //Setup app for notifications.
    var app = Application.currentApplication()
    app.includeStandardAdditions = true
    app.displayAlert(msg, {
      soundName: 'Sosumi'
    })
  }
  preferences = Library('preferences')
  notify(preferences.isOpen.toString()) //Msgbox = false
  preferences.setOpen()
  notify(preferences.isOpen.toString()) //Msgbox = true
}

"test2": {
  function notify(msg){
    //Setup app for notifications.
    var app = Application.currentApplication()
    app.includeStandardAdditions = true
    app.displayAlert(msg, {
      soundName: 'Sosumi'
    })
  }
  screenflow = Library('screenflow')
  preferences = screenflow.preferences()
  notify(preferences.isOpen.toString()) //Msgbox = false
  preferences.setOpen()     //Error on line 54: TypeError: Object is not a function (evaluating 'preferences.setOpen()')
  notify(preferences.isOpen.toString())
}

After executing test1 I was getting very excited! It worked flawlessly and I assumed this meant everything was prepared for the next test where I include a library in another library! But alas, I was apparently a fool to think so. You can’t access Sub-Library methods from Libraries. But you can access Sub-Library properties from Libraries.

Is this generally the case? Or is there a way to do sub-classes that I haven’t tried yet?

EDIT: Fixed some errors in copying

That’s kind of a long post, and I’m no expert, but I’ll see what I can do to help.

This shows how to make a “class” in JavaScript - JS doesn’t really have classes, but this is close:

    function Preferences() {
        this.isOpen = false;
        this.open = function() {
            this.isOpen = true;
        };
    }

    var prefs = new Preferences();
    console.log(prefs.isOpen);
    prefs.open();
    console.log(prefs.isOpen);

Let me know if this helps, and where you want to go from here. I can discuss subclassing if you want.

I'm just a JavaScript novice myself, but you might find this blog interesting and helpful:
A Touch of Class: Inheritance in JavaScript

Save that class as a library in “~/Library/Script Libraries/” named “lib”

Now, how do you execute prefs.open?

I would assume:

lib = Library('lib')
var prefs = new lib.Preferences()
prefs.isOpen

right? Nope Error on line 2: Error: Preferences is not a valid class for application lib. Okay okay, that’s a little unfair… Renaming Preferences() to preferences() should do the trick here:

function preferences() {
    this.isOpen = false;
    this.open = function() {
        this.isOpen = true;
    };
}

and

lib = Library('lib')
var prefs = new lib.preferences()
prefs.isOpen

But nope… Error on line 2: TypeError: Objective-C blocks called as constructors must return an object.

I was going off of this initially. But then I started going off of other examples from an experienced javascript developer. From what they can tell, class methods just do not work in libraries. - Which is why I started trying to use strictly libraries.

That link/page refers to "JavaScript classes introduced in ECMAScript 2015", which is ECMA 6, and not fully supported by JXA, even in Sierra. I don't think macOS El Capitan supports it at all, or very little at the most.

I’ve not had great experiences with trying to have one library reference objects from another library, if that’s what you’re talking about.

Don’t forget that a lot of examples you’ll find may assume you’re using npm or something like that to handle imports.


I have some examples on my GitHub site that might interest you. Check out this link:

Ignore the subfolders. Just look at the various files in that folder, like “KMEngine.md” or “FileUtils.md”.

Indeed, I notice that now, but I still would have imagined the other examples I used in OP would work, as all of those depend on function objects rather than classes. Notably:

Oh... interesting... I guess this is how most native JXA objects are actually built... So with FileUtils for instance, if I were to call chooseFile would I use FileUitls['chooseFile'](prompt,type)?

I wouldn't count on that. I'm using a particular design pattern called the Module Pattern. See Learning JavaScript Design Patterns.

You'd call it like this: FileUtils.chooseFile(...);

Technically, the method you showed would work, because you can always access an object's property using the [ "name" ] syntax. But generally you only do that when you have to, like when the name is actually contained in a variable, or it has spaces in it, or something like that. Hope that's not confusing. :slight_smile:

Really? How odd. In my year or so using javascript I have never seen this pattern for constructing classes... typically I have seen The Constructor Pattern and then the full on class myClass {...} method. Most of these patterns are new to me even. Perhaps the constructor pattern just doesn't work when called from a library in JXA...

No, the constructor pattern should work, although oddly I have had instances where it told me I was assigning something to a readonly property, and I couldn’t figure out why.

The Module pattern is nice because it allows you to have “private” members. Nice, but by no means necessary.

And to be honest, there’s some issues with the pattern. For one thing, unit testing is difficult, because you can’t use mock functions with the private members. The more I use it, the more I wonder if it’s the best solution.

But it’s what I started with, so for now I’m keeping it. I do like private members. (No jokes, please. :open_mouth:)

Why? It is for boys only? LOL

Hey! What part of “No jokes, please” didn’t you understand?!?

LOL.

I wouldn't have even thought about a joke if you hadn't mentioned it.
But with such an invitation, . . .

1 Like

A post was merged into an existing topic: MACRO: Macro Repository (i.e. Version Control) Beta

Hey Dan,

This is what I was talking about:

https://puu.sh/to3nb/d78f7b94fc.png

You can't use the FileUtils class in a library, on my mac anyways... Any idea why?

P.S. lib is saved in ~/Library/Script Libraries/ so can act as a library as specified here.

Nope, no ideas. These are the exact kinds of issues I ran into when I tried using libraries myself, and why I gave up on them.

If memory serves, the standard Constructor pattern works. If so, it’s probably a nail in the coffin of the Module pattern, at least for uses like this.

That explains it. That's actually what this thread is about... The inability to do such things (even with Constructor Pattern). I probably didn't explain this well (i was very frustrated last night after spending the whole weekend on the library! xD).

You can call a class from the same document as the class definition. But as soon as you put the class into a seperate library everything grinds to a hault.

Libraries are not something I personally associate with projects as small and ephemeral as scripts, particularly as dependencies of any kind add friction to script-sharing – I tend to just paste the boiler-plate in, and let the file stand alone.

If what you are building is not so much transient throwaway scriptlets (built around automation glue) as larger JS tools that may sometimes want to sling a few AE events about, then it might be worth looking at using Node.js / npm approaches, and do things like this:

(The Library mechanism provided by Apple for JavaScript for Automation was probably never intended for scale or complexity, and probably isn’t all that deeply tested or maintained either)

1 Like

On the particular issue you report, however, the thing to notice is that the Automation.Library method exports only method names from the referenced library file. (See the Library section in JS for Automation release notes). (It doesn't export names which are bound to objects which are not themselves functions).

To obtain a reference to Dan's FileUtils module, you would need to call a method, at the top level of the library file, which returns a reference to the module you want to use.

If, for example we add such a method to the top of Dan's file:

We can then, from a client script, use the Automation.Library method like this: