Re JXA - when to use app.add / app.remove vs array_variable.push


#1

When using JXA to script OmniFocus, it seems one needs to “push” new task or project, but use the add function to add tags. So tasks are arrays, and to add a new item to an array one uses the push method. Tags are also arrays and should one not be able to add using the push method? Have I understood something wrong?

So here is example code to illustrate my question - the code snippet below with * works, but snippet with ** does not work.

Thank you.

function run() {
    const of = Application("OmniFocus");
    const oDoc = of .defaultDocument;
    const oWin = oDoc.documentWindows[0];
    const seln = oWin.content.selectedTrees.value();
    todayTags = oDoc.flattenedTags.whose({
        name: {
            _contains: 'Today'
        }
    })(); // this will result in an array of tag objects

    todayTag = todayTags[0]; //choosing the first element of array
    firstSelectedTask = seln[0];

    // now say I want to add todayTag to firstSelectedTask

    // This below code works *
    of.add(todayTags, {
        to: firstSelectedTask.tags
    });

    // but this does not seem to work. Should it not? **
    firstSelectedTask.tags.push(todayTag);
    firstSelectedTask.tags().push(todayTag); // Neither does this work. Added the parents after tags, to get the tags array.
};

#2

.value() property of selectedTrees returns an array of tasks. As you found out, it’s not possible to “push” tags into the task’s tag element array. Also, we can’t “pop” an element out of it.

For reference, add method takes reference or list of reference as a direct parameter and reference or location specifier in the to: parameter.


#3

but that is exactly my question - in one can push into an array of tasks, then should one not be able to push into a task’t tag element array?

Just confirming this - reference or location specifier are both the same things right? And it can also be referred to as object specifier, am I right?

Thank you for your help.


#4

How can you push into an array of tasks? Could you give an example ?

This document addresses clarifies this issue.

AppleScript Fundamentals

In Applescript, examples of location specifiers would be: beginning of tasks, end of tasks.

Taken from the document I referenced above:

Parameters That Specify Locations
Many commands have parameters that specify locations. A location can be either an insertion point or another object. An insertion point is a location where an object can be added.
In the following example, the toparameter specifies the location to which to move the first paragraph. The value of the toparameter of the duplicate command is the relative object specifier before paragraph 4, which is an insertion point. AppleScript completes the specifier with the target of the tell statement, front document of application "TextEdit" .


#5

A Code snippet from Brandon Pittman omnifocuslibrary.js… See how in this one a task object is created and then pushed into taskProject.tasks (which taskProject.tasks is an array I presume).

function makeTask(text, context, deferDate, dueDate, project, flagTask) {
  const taskProject = typeof project === 'string' ? getProject(project) : project;
	const taskObject = app.Task({name: text, context: context || null, deferDate: deferDate || null, dueDate: dueDate || null, flagged: flagTask});
	if (project)  {
    taskProject.tasks.push(taskObject);
  } else {
    doc.inboxTasks.push(taskObject);
  }
}

Will read through the document. From the snippet, it seems that location specifier can be used to construct or maybe get an object specifier, and location specifier can also be used to specify where in an array to add or find something.

Thank you!


#6

I think there is a misunderstanding. taskProject.tasks retrieves a reference to an array of tasks. taskProject.tasks(), on the other hand, would return an array of tasks (with the trailing parenthesis).


#7

It is likely that I may have caused some confusion then, because I am still not as well versed with Reference vs Value that one gets when the parenthesis are added.

But what I meant to ask originally is that should not this – firstSelectedTask.tags.push(todayTag); from the code in the first post add the todayTag to the tags array of the task?
firstSelectedTask is the reference to a task, and therefore firstSelectedTask.tags is a reference to the array of tags of the task, and therefore should not the push work?


#8

firstSelectedTask is the task object (not a reference because you use trailing parenthesis on the .value property of selectedTrees to get the actual object. And firstSelectedTask.tags is, as you say, a reference — you didn’t use parenthesis there.


#9

Did some experimentation on this topic and tested scripts in AppleScript and JXA. This is what I found.

If we want to make a new object, say a new task, in AppleScript would be.

make new task with properties {name: "Sample Task"} at end of tasks of oProject

Apparently, in JXA, we use the push method to emulate it. For example,

oProject.tasks.push(oTask)

On the other hand, if we want to add an existing object to a given container, in AppleScript would be:

add existingTag to end of tags of oProj

In JXA…

Application(‘OmniFocus’).add(oTask, {
                to: oProject.tasks
            })

Obviously, every variable needs to be defined in advance and, in AppleScript, we need to be in the context of the appropriate tell block.


#10

Understood that for adding a task, in javascript it is a push into what appears to be an array / list of tasks.
And, deciphering from your language, it seems the difference between pushing a new task and adding a tag to an existing task is

  • that the new task is just a dictionary that is being used to create a new task (and so a simple push method),
  • whereas adding a tag to a task entails internal logic that goes beyond adding a dictionary to the task’s tags array / list

So got that. Who am I to argue how it should work, that is for Omni gods to decide :), was just trying to learn to read the dictionary right and to understand why push would not work in what appears to be adding an item to a list.


On another note, I experimented with this below

function run() {

    const of = Application("OmniFocus");
    const oDoc = of .defaultDocument;
    const oWin = oDoc.documentWindows[0];

    // return oWin.content.selectedTrees[0]; // 1. Application("OmniFocus").defaultDocument.documentWindows.at(0).content.selectedTrees.at(0)
    // return oWin.content.selectedTrees.value()[0]; // 2. Application("OmniFocus").defaultDocument.tasks.byId("afbVj5Cjlm6")

Trying to clarify for myself the difference between a reference and value.

So I understand that #1 refers to an object in a window, where as #2 refers to the value of the object.

To paraphrase, say people are standing in a formation, then a pointer to the selected person in that formation is what #1 is, whereas #2 is a pointer to that person in real life. #2 will always point to that person whereas what #1 points to whoever stands in that place in the next formation.

Upon experimentation, both deliver the name(), but only #2 allows me to change the name by assigning a new value to task name.

So this works:

    // code below changed the task name
    const seln = oWin.content.selectedTrees.value();
    firstSelectedTask = seln[0];
    console.log(firstSelectedTask.name());
    firstSelectedTask.name = "new task name ver2"; // this works

    // but the below code does not change the name
    const seln1 = oWin.content.selectedTrees;
    firstSelectedTask = seln1[0];
    console.log(firstSelectedTask.name()); // name of task is printed
    firstSelectedTask.name = "new task name ver3"; // Error: Can't set that. 
    // Access not allowed. (-10003). Looked up error code using google - and saw this - -10003
    // The specified object cannot be modified. 
    // so to say that you cannot change attributes when an object is referred to in a formation...

Did I understand that right?


#11

Again, thank you for the help understanding this. :)


#12

FWIW, full examples here (it is not possible to “push” an existing tag into the collection of tags of the default document):

JXA

Making a new tag in the Database:

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const
            oApp = Application('OmniFocus'),
            oDoc = oApp.defaultDocument,
            oProject = oDoc.flattenedProjects.byName('Test'),
            newTag = oApp.Tag({
                name: 'Sample Tag'
            })
        return (
            oDoc.tags.push(newTag)
        )
    };

    // MAIN ----------------------------------------------------------------
    return main();
})();

Adding an existing tag to a given container (just created, in this case):

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const
            oApp = Application('OmniFocus'),
            oDoc = oApp.defaultDocument,
            oProject = oDoc.flattenedProjects.byName('Test'),
            newTag = oApp.Tag({
                name: 'Sample Tag'
            })
        return (
            oDoc.tags.push(newTag),
            oApp.add(newTag, {
                to: oProject.tags
            })
        )
    };

    // MAIN ----------------------------------------------------------------
    return main();
})();

#13

The dictionary represents a single task properties. Here:

Application('OmniFocus').Task({name: "Sample Task"}

We are making an instance of the Task class.

When we use parenthesis, as in .name(), we are making a function call — opening one Apple Event. .name, is just a reference to a function.

In order to change a task property, for example, its name, it’s necessary to get the actual object. In your case, a task object.

Will address the rest of your questions tomorrow.


#14

Understood that the dictionary is used to create an instance of the Task class.
Also, understood the difference between .name() and .name/

Got it. Thank you for sharing.

Wow. Thank you for sharing the scripts. Understood that a new tag cannot be pushed it needs to be added using the add verb of the application.

And by the way, it is a joy to read the scripts you write. The way the constants are created one after the other with commas, and then the execution. This one was so easy to understand and will try to write scripts like this.

Not clear as to reason behind writing the code that is making changes in the return statement, unless from a structure perspective might as well report the changes that the function is making and each statement generates a result I suppose.

With this said, I am already overwhelmed with your help in understanding the concepts, so not asking just observing. :)


#15

Thanks for your kind words.

with comments in each case and why I use return in Atom

OmniFocus Object Model contains a class Tree, that represents the tree of objects in the main window content (GUI). Getting**.value** property on a member of this class, returns the object being represented by this tree (task, tag, project, folder, etc).

For example, if I select this element in the GUI,

and run this code, I get…

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const
            oApp = Application('OmniFocus'),
            oDoc = oApp.defaultDocument,
            oWin = oDoc.documentWindows[0],
            oTrees = oWin.content.selectedTrees();
        return oTrees[0] 
       // --> Application("OmniFocus").defaultDocument
       //    .documentWindows.byId(1366)
       //.   .content.trees.at(0).trees.at(0)

    };

    // MAIN ----------------------------------------------------------------
    return main();
})();

As .selectedTrees property retrieves an array of trees, I could apply a transform — map— that array to an array of task objects.

`

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const
            oApp = Application('OmniFocus'),
            oDoc = oApp.defaultDocument,
            oWin = oDoc.documentWindows[0],
            oTrees = oWin.content.selectedTrees();
        return oTrees.map(x => x.value())
        // --> Application("OmniFocus").defaultDocument
        //.    .tasks.byId("ekmsbi-OePt")

    };

    // MAIN ----------------------------------------------------------------
    return main();
})();
```

#16

It’s a useful construction, I think, because you can combine Effects and Value (here, for example)

If we have two or more bracketed expressions, separated by commas, the return value is that of the last expression in the comma-delimited series.

For example,

const greet = strName => {
	return (
		'Good Morning, ' + strName,
		'Good Evening, ' + strName,
		'Good Night, ' + strName
	)
}

When greet('John Adams') is called, only Good Night, John Adams is being returned.


#17

Thank you for the explanation. All clear now - as far as references and value is concerned.

Understood as to how the return value of the last expression is returned. However, not sure I understand the “combine Effects and Value” idea. I guess what you are saying is that this way it is clear as what part of the code is changing something outside the function.

I have still not completed the Haskell link and if this will become clear there, then I will get it in due course of time.

Thank you!


#18

Perhaps this example clarifies a little this issue. I added comments to indicate where does effects and value occur.

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const
            oApp = Application('OmniFocus'),
            oDoc = oApp.defaultDocument,
            oProject = oDoc.flattenedProjects.byName('Test'),
            oTask = oApp.Task({
                name: 'Sample Task'
            });
        return (
            oProject.tasks.push(oTask), // EFFECT
            oTask                       // VALUE
        )

    }; 

    // MAIN ----------------------------------------------------------------
    return main();
})();

The idea is using one return statement to:

  • Produce effects: in this case, adding one task to the array of tasks of project ‘Test’.
  • Return a value: in this case, a task object.

#19

I think I understand now.

At a simplistic level, I am inferring that you structure your code and your functions such that

  • constants / variables are setup in one block, and
  • effects and value are in a separate block (in this case in the return block)

And I can see how that helps in understanding, debugging and being aware of where to be careful (effects).

Thank you!!